android: the repair sheet, browse, every long-press sheet, You's last sections, the lyrics tools — and the walk's audio check made honest
The playlist repair (Find a local match: the library searched by the row's name, a pick relinks it through playlist_relink_item; an unknown row takes the track's identity). A peer's sheet on the raw results (play or save the folder, browse their files, message, buddy) and the Browse screen — the hub's Browse tab: a user's whole share with sizes and totals, a folder played or saved whole; a share can hold thousands of folders, so it filters and renders 80 at a time. "Look up a user" on Community → Buddies. The sheets the phone lacked: a queue row's (Play now, Move to next, Remove from queue), a catalog song's, a catalog card's and the record page's own, a raw file's (in the results and in Browse), a room name's, a buddy's, the now-playing Save (the desktop's Save menu for a streamed track or record). The artwork viewer with pinch zoom, one layer the back gesture peels. Search's recent searches and its spelling suggestion from the names this phone knows. You → Streaming, Downloads, Sharing, Lyrics, Album artwork, and Clear play history; a record's Re-fetch the cover. The lyrics tools on the sheet's face: the offset nudged and kept per track, another LRCLIB candidate chosen, the lookup forgotten and done again (lyrics.svelte.js, shared by the face and the lyrics sheet). The harness: every CDP call times out and the reload step re-attaches (a WebView that stopped answering hung a walk for ten minutes); the audio check counts SoulStream's own tracks' underruns from the mixer dump instead of the thread-wide counters that any app's notification moves — the afternoon's "dropouts" were other apps' sounds, and the browse of a 49,544-file share measured at zero; __settle resets every tab to its root through the nav; an adb step kind; the walks cover all of the above on the mock and on the phone.
This commit is contained in:
1 parent
6e39dc43f3
commit
26928b706d
21 files changed
+1294
-92
No files matched your search
@@ -54,10 +54,17 @@ export class CDP {
|
||||
throw new Error("could not attach to the page on port " + port
|
||||
+ (phone ? " (a WebView in the background does not answer; is SoulStream on screen?)" : ""));
|
||||
}
|
||||
send(method, params = {}) {
|
||||
/* a page that never answers (frozen, or replaced by a reload) must fail
|
||||
the call, not the whole walk */
|
||||
send(method, params = {}, timeoutMs = 45000) {
|
||||
const id = ++this.id;
|
||||
this.ws.send(JSON.stringify({ id, method, params }));
|
||||
return new Promise((res) => this.waits.set(id, res));
|
||||
return new Promise((res, rej) => {
|
||||
const t = setTimeout(() => {
|
||||
if (this.waits.delete(id)) rej(new Error("the page did not answer " + method + " in " + Math.round(timeoutMs / 1000) + "s — a frozen or replaced WebView?"));
|
||||
}, timeoutMs);
|
||||
this.waits.set(id, (m) => { clearTimeout(t); res(m); });
|
||||
});
|
||||
}
|
||||
async evaluate(expr) {
|
||||
const r = await this.send("Runtime.evaluate", { expression: expr, awaitPromise: true, returnByValue: true });
|
||||
|
||||
+226
-11
@@ -54,6 +54,11 @@ window.__settle = function () {
|
||||
if (s) { var c = s.querySelector('.np-head .iconbtn'); if (c) c.click(); }
|
||||
var h = document.querySelector('.halfsheet.up');
|
||||
if (h) { var sc = document.querySelector('.scrim'); if (sc) sc.click(); }
|
||||
/* every tab back to its root, through the nav's own door (the history mirror stays true) */
|
||||
if (window.__ss && __ss.go && __ss.nav) {
|
||||
var tabs = Object.keys(__ss.nav.stacks);
|
||||
for (var i = 0; i < tabs.length; i++) { if (__ss.nav.stacks[tabs[i]].length > 1) { __ss.go(tabs[i]); __ss.go(tabs[i]); } }
|
||||
}
|
||||
var home = document.querySelectorAll('.nav button')[0];
|
||||
if (home) { home.click(); home.click(); }
|
||||
return true;
|
||||
@@ -114,6 +119,20 @@ const PROBE = `(function () {
|
||||
impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',
|
||||
keepOn: !!document.querySelector('.imp-form .sw.on'),
|
||||
bulkline: (document.querySelector('.bulkline') || {}).textContent || '',
|
||||
brwDirs: document.querySelectorAll('.brw-dir').length,
|
||||
qrows: document.querySelectorAll('.q-face .row').length,
|
||||
viewer: !!document.querySelector('.viewer img'),
|
||||
lyrLines: document.querySelectorAll('.lyr .ln').length,
|
||||
sheetTitle: (document.querySelector('.halfsheet .sheet-title') || {}).textContent || '',
|
||||
recents: document.querySelectorAll('.recent-chips .chip').length,
|
||||
dym: (document.querySelector('.dym') || {}).textContent || '',
|
||||
sq: (document.querySelector('.sfield input') || {}).value || '',
|
||||
slots: (document.querySelector('.slots .stepper .n') || {}).textContent || '',
|
||||
wordfill: (function () { var r = [].slice.call(document.querySelectorAll('.screen .row')).find(function (x) { return /Word-by-word/.test(x.textContent); }); return r ? (r.querySelector('.sw.on') ? 'on' : 'off') : ''; })(),
|
||||
artEdge: (function () { var ps = document.querySelectorAll('.prefs[data-mode="albumart"] .pref'); var c = ps[1] && ps[1].querySelector('.chip.on'); return c ? c.textContent.trim() : ''; })(),
|
||||
brwFiles: document.querySelectorAll('.brw-dir .peer-file').length,
|
||||
brwHead: (document.querySelector('.brw-head .m') || {}).textContent || '',
|
||||
keep: (function () { var g = document.querySelector('.prefs[data-mode="streamcache"]'); var c = g && g.querySelector('.pref .chip.on'); return c ? c.textContent.trim() : ''; })(),
|
||||
impErr: [].some.call(document.querySelectorAll('.screen .stub'), function (x) { return /didn't import/.test(x.textContent); }),
|
||||
srcMax: (function () { var g = document.querySelector('.prefs[data-mode="stream"]'); var c = g && g.querySelector('.pref .chip.on'); return c ? c.textContent.trim() : ''; })(),
|
||||
rowArt: document.querySelectorAll('.screen .row .art img').length,
|
||||
@@ -132,9 +151,12 @@ const MOCK_STEPS = [
|
||||
{ name: "home", wait: 1600 },
|
||||
/* a long press on a card is its sheet */
|
||||
{ do: `document.querySelector('.grid2 .release').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "hold-sheet", wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `__click('.halfsheet .row', 'Re-fetch the cover')`, name: "cover-refetched", wait: 800 },
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, name: "search-empty", wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'shpo'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "search", wait: 900 },
|
||||
/* a misspelt library name: the catalog has nothing, the library's own names correct it */
|
||||
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'shpongel'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "search-dym", wait: 900 },
|
||||
{ do: `__click('.dym', null)`, name: "search-dym-pick", wait: 700 },
|
||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, name: "library", wait: 800 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, name: "artists", wait: 600 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[2].click()`, name: "tracks", wait: 900 },
|
||||
@@ -144,14 +166,35 @@ const MOCK_STEPS = [
|
||||
{ do: `document.querySelector('.trk .r').click()`, name: "track-sheet", wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 600 },
|
||||
{ do: `document.querySelector('.mini').click()`, name: "sheet", wait: 1100 },
|
||||
/* the art opens the viewer; the back gesture closes it */
|
||||
{ do: `document.querySelector('.np-art').click()`, name: "viewer", wait: 900 },
|
||||
{ do: `history.back()`, name: "viewer-closed", wait: 700 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[2].click()`, name: "queue", wait: 600 },
|
||||
/* a queue row's sheet: the row removed */
|
||||
{ do: `document.querySelector('.q-face .row').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "queue-row-sheet", wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Remove from queue')`, name: "queue-removed", wait: 600 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[1].click()`, name: "lyrics", wait: 900 },
|
||||
/* the lyrics tools: nudged half a second, another candidate chosen */
|
||||
{ do: `__click('.lyr-tools', null)`, name: "lyrics-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Lyrics later')`, name: "lyrics-nudged", wait: 500 },
|
||||
{ do: `__click('.halfsheet .row', 'Search again')`, name: "lyrics-candidates", wait: 900 },
|
||||
{ do: `__click('.halfsheet .row', 'Live')`, name: "lyrics-chosen", wait: 700 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 300 },
|
||||
{ do: `history.back()`, name: "after-back", wait: 900 },
|
||||
{ do: `document.querySelector('.rec-artist').click()`, name: "artist", wait: 900 },
|
||||
{ do: `history.back()`, wait: 700 },
|
||||
{ do: `history.back()`, name: "library-again", wait: 800 },
|
||||
{ do: `document.querySelectorAll('.nav button')[3].click()`, name: "community", wait: 800 },
|
||||
/* a buddy's sheet: browse, message, remove */
|
||||
{ do: `__click('.segtabs button', 'Buddies')`, name: "buddies", wait: 600 },
|
||||
{ do: `document.querySelectorAll('.screen button.row')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "buddy-sheet", wait: 600 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
/* a user looked up by name: the peer sheet */
|
||||
{ do: `__click('.screen .row', 'Look up a user')`, wait: 600 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'dusty_crates'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Open')`, name: "user-lookup", wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `__click('.segtabs button', 'Rooms')`, wait: 500 },
|
||||
/* the fake network: a room with a few lines in it */
|
||||
{ do: `__click('.screen button.row', 'Lounge')`, name: "room", wait: 2400 },
|
||||
{ do: `history.back()`, wait: 700 },
|
||||
@@ -161,6 +204,11 @@ const MOCK_STEPS = [
|
||||
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'daft punk discovery'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "cat-results", wait: 1400 },
|
||||
/* the hero's artist: the desktop's page — tabs, Popular's bars, Follow,
|
||||
the latest release, fans also like — then a fan-also-likes page */
|
||||
{ do: `document.querySelector('.cat-song').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "cat-song-sheet", wait: 600 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
/* a Records-rail card's long press: the record's sheet */
|
||||
{ do: `(function () { var sec = [].slice.call(document.querySelectorAll('.sec')).find(function (x) { return /^\\s*Records/.test(x.textContent); }); var rail = sec && sec.nextElementSibling; rail.querySelector('.release').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); })()`, name: "cat-record-sheet", wait: 600 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `__click('.top button', 'Open')`, name: "cat-artist", wait: 2200 },
|
||||
{ do: `__click('.stats .fb', 'Follow')`, name: "cat-artist-follow", wait: 900 },
|
||||
{ do: `__click('.disco-tabs button', 'Singles')`, name: "cat-artist-tab", wait: 700 },
|
||||
@@ -170,12 +218,31 @@ const MOCK_STEPS = [
|
||||
{ do: `document.querySelector('.cat-song').click()`, name: "net-playing", wait: 5500 },
|
||||
{ do: `document.querySelector('.mini').click()`, name: "net-sheet", wait: 3000 },
|
||||
{ name: "net-arrived", wait: 7000 },
|
||||
/* the now-playing sheet's ⋯: the streamed track saved into the library
|
||||
(its cache is complete by now, so it files at once) */
|
||||
{ do: `document.querySelectorAll('.sheet .np-head .iconbtn')[1].click()`, name: "np-more", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Save to library')`, name: "np-saved", wait: 900 },
|
||||
/* the raw peers: a peer's sheet → their shares browsed → a folder opened
|
||||
→ one of its files streams */
|
||||
{ do: `__settle()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 },
|
||||
{ do: `__click('.net-ask', null)`, name: "net-peers", wait: 1800 },
|
||||
{ do: `document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "peer-file-sheet", wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `document.querySelector('.peer-head .peer-menu').click()`, name: "peer-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Browse their files')`, name: "browse", wait: 1400 },
|
||||
{ do: `__click('.brw-dir .peer-folder', 'Homework')`, name: "browse-dir", wait: 600 },
|
||||
{ do: `__click('.brw-dir .peer-file', 'Aerodynamic')`, name: "browse-play", wait: 2500 },
|
||||
{ do: `history.back()`, wait: 700 },
|
||||
/* a catalog record: a row plays through the resolver with the rest of
|
||||
the record queued behind it, the next track finds its own copy on
|
||||
its turn, and the disc plays the whole record from one folder */
|
||||
{ do: `__settle()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 900 },
|
||||
{ do: `__click('.rail .release', 'Discovery')`, name: "cat-album", wait: 1500 },
|
||||
/* the record page's own sheet from the bar */
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "cat-album-sheet", wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `document.querySelectorAll('.tracks .trk')[1].click()`, name: "cat-album-row", wait: 5500 },
|
||||
{ do: `__ss.next()`, name: "cat-album-next", wait: 7000 },
|
||||
{ do: `document.querySelector('.rec-acts .disc').click()`, name: "cat-album-disc", wait: 6500 },
|
||||
@@ -216,6 +283,11 @@ const MOCK_STEPS = [
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Download all tracks')`, name: "playlist-bulk-download", wait: 1500 },
|
||||
{ do: `__click('.bulkline .bulkx', null)`, wait: 400 },
|
||||
/* the repair: the unknown row finds a library track and takes its identity */
|
||||
{ do: `__click('.pl-rows .trk', 'ID')`, name: "playlist-unknown-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Find a local match')`, name: "playlist-match", wait: 500 },
|
||||
{ do: `(function () { var i = document.querySelector('.plmatch input'); i.value = 'caribou track 4'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "playlist-match-results", wait: 700 },
|
||||
{ do: `__click('.halfsheet .sheet-rows .row', 'Track 4')`, name: "playlist-matched", wait: 1000 },
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "playlist-more", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Unpin')`, name: "playlist-unpinned", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 },
|
||||
@@ -263,6 +335,23 @@ const MOCK_STEPS = [
|
||||
{ do: `__click('.screen .row', 'Volume normalization')`, name: "you-norm-on", wait: 500 },
|
||||
{ do: `__click('.prefs[data-mode="stream"] .chip', 'Lossy only')`, name: "you-lossy", wait: 500 },
|
||||
{ do: `__click('.prefs[data-mode="stream"] .chip', 'No limit')`, wait: 400 },
|
||||
{ do: `__click('.prefs[data-mode="streamcache"] .chip', '1 week')`, name: "you-keep", wait: 500 },
|
||||
{ do: `__click('.prefs[data-mode="streamcache"] .chip', '1 month')`, wait: 400 },
|
||||
{ do: `document.querySelector('.slots .stepper .iconbtn:last-child').click()`, name: "you-slots", wait: 500 },
|
||||
{ do: `document.querySelector('.slots .stepper .iconbtn').click()`, wait: 400 },
|
||||
{ do: `__click('.screen .row', 'Word-by-word')`, name: "you-words", wait: 500 },
|
||||
{ do: `__click('.screen .row', 'Word-by-word')`, wait: 400 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1000 px')`, name: "you-artedge", wait: 500 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1500 px')`, wait: 400 },
|
||||
{ do: `__click('.screen .row', 'Clear play history')`, name: "history-confirm", wait: 600 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Clear')`, name: "history-cleared", wait: 800 },
|
||||
/* recent searches on the empty page: the ones this walk used */
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 500 },
|
||||
/* the tab again pops its stack to the root */
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 },
|
||||
{ do: `__click('.sfield .iconbtn', null)`, name: "search-recent", wait: 600 },
|
||||
{ do: `__click('.recent-chips .chip', null)`, name: "search-recent-pick", wait: 1400 },
|
||||
{ do: `__settle()`, wait: 600 },
|
||||
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-again", wait: 700 },
|
||||
/* the page overflow → the sleep timer */
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "home-more", wait: 700 },
|
||||
@@ -272,24 +361,48 @@ const MOCK_STEPS = [
|
||||
const MOCK_EXPECT = {
|
||||
tracks: (i) => i.title.startsWith("Your library"),
|
||||
community: (i) => i.title.startsWith("Community"),
|
||||
buddies: (i) => i.title.startsWith("Community"),
|
||||
"buddy-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Remove buddy/.test(i.halfText),
|
||||
"user-lookup": (i) => i.half.includes("up") && /dusty_crates/.test(i.halfText) && /Browse their files/.test(i.halfText) && /Add buddy/.test(i.halfText),
|
||||
"home-more": (i) => i.half.includes("up"),
|
||||
you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit",
|
||||
"you-norm-off": (i) => i.norm === "off",
|
||||
"you-norm-on": (i) => i.norm === "on",
|
||||
"you-lossy": (i) => i.srcMax === "Lossy only",
|
||||
"you-keep": (i) => i.keep === "1 week",
|
||||
"you-slots": (i) => i.slots === "3",
|
||||
"you-words": (i) => i.wordfill === "on",
|
||||
"you-artedge": (i) => i.artEdge === "1000 px",
|
||||
"history-confirm": (i) => i.half.includes("up") && /Clear the play history/.test(i.halfText),
|
||||
"history-cleared": (i) => !i.half.includes("up") && /history cleared/i.test(i.notice),
|
||||
"search-recent": (i) => i.title.startsWith("Search") && i.recents >= 1,
|
||||
"search-recent-pick": (i) => i.title.startsWith("Search") && (i.songs >= 4 || !!i.top),
|
||||
"sleep-sheet": (i) => i.half.includes("up"),
|
||||
"sleep-set": (i) => !i.half.includes("up"),
|
||||
home: (i) => /^(Good |Still)/.test(i.title),
|
||||
"hold-sheet": (i) => i.half.includes("up"),
|
||||
"hold-sheet": (i) => i.half.includes("up") && /Re-fetch the cover/.test(i.halfText),
|
||||
"cover-refetched": (i) => !i.half.includes("up") && /Cover re-fetched/.test(i.notice),
|
||||
"search-empty": (i) => i.title.startsWith("Search") && !i.half.includes("up"),
|
||||
"search-dym": (i) => i.dym === "shpongle",
|
||||
"search-dym-pick": (i) => i.sq === "shpongle" && !i.dym,
|
||||
search: (i) => i.title.startsWith("Search"),
|
||||
library: (i) => i.title.startsWith("Your library"),
|
||||
album: (i) => i.screen.includes("tr-push") && !i.mini,
|
||||
lyrics: (i) => i.sheet.includes("up") && i.lyrLines >= 3,
|
||||
"lyrics-sheet": (i) => i.half.includes("up") && /Lyrics later/.test(i.halfText) && /Search again/.test(i.halfText),
|
||||
"lyrics-nudged": (i) => /\+0\.5 s/.test(i.sheetTitle),
|
||||
"lyrics-candidates": (i) => /Choose the right lyrics/.test(i.halfText) && /Live/.test(i.halfText),
|
||||
"lyrics-chosen": (i) => !i.half.includes("up") && i.lyrLines >= 3,
|
||||
viewer: (i) => i.viewer,
|
||||
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
||||
"album-playing": (i) => i.mini,
|
||||
"track-sheet": (i) => i.half.includes("up"),
|
||||
sheet: (i) => i.sheet.includes("up"),
|
||||
"after-back": (i) => !i.sheet.includes("up"),
|
||||
artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img"),
|
||||
queue: (i) => { globalThis.__qrows = i.qrows; return i.sheet.includes("up") && i.qrows >= 2; },
|
||||
"queue-row-sheet": (i) => i.half.includes("up") && /Remove from queue/.test(i.halfText) && /Play now/.test(i.halfText),
|
||||
"queue-removed": (i) => !i.half.includes("up") && i.qrows === globalThis.__qrows - 1,
|
||||
"library-again": (i) => i.screen.includes("tr-pop"),
|
||||
/* after the follow: the feed's lead ("Coming · ALBUM" — the upcoming
|
||||
record leads), the news, the back catalogue, recommendations,
|
||||
@@ -299,6 +412,8 @@ const MOCK_EXPECT = {
|
||||
/* the artist with a picture shows it on the hero and its card; the
|
||||
namesake under Deezer's empty-hash placeholder shows the app's own */
|
||||
"cat-results": (i) => i.songs >= 4 && i.top.startsWith("Daft Punk") && i.heroImg && i.cardImgs === 1 && i.cardPhs === 1,
|
||||
"cat-song-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText) && /Add to playlist/.test(i.halfText) && /Go to record/.test(i.halfText),
|
||||
"cat-record-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText) && /Go to record/.test(i.halfText) && /Go to artist/.test(i.halfText),
|
||||
"cat-artist": (i) => i.screen.includes("has-hero") && i.title.startsWith("Daft Punk") && i.tabs >= 4 && i.pop >= 5 && i.socs === 2,
|
||||
"cat-artist-follow": (i) => i.fb.includes("Following"),
|
||||
"cat-artist-tab": (i) => i.title.startsWith("Daft Punk") && i.cardImgs === 2 && i.cardPhs === 2,
|
||||
@@ -306,7 +421,16 @@ const MOCK_EXPECT = {
|
||||
"net-playing": (i) => i.mini && i.playing.includes("One More Time") && /streaming from|copy/.test(i.live),
|
||||
"net-sheet": (i) => i.sheet.includes("up") && i.lane.includes("rising"),
|
||||
"net-arrived": (i) => i.sheet.includes("up") && !i.lane.includes("rising"),
|
||||
"np-more": (i) => i.half.includes("up") && /Save to library/.test(i.halfText),
|
||||
"np-saved": (i) => !i.half.includes("up") && /Saved to the library/.test(i.notice),
|
||||
"net-peers": (i) => i.title.startsWith("Search") && i.peers >= 3,
|
||||
"peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),
|
||||
"peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),
|
||||
browse: (i) => i.screen.includes("tr-push") && i.brwDirs >= 2 && /folders/.test(i.brwHead),
|
||||
"browse-dir": (i) => i.brwFiles >= 8,
|
||||
"browse-play": (i) => i.playing.includes("Aerodynamic") && /streaming from/.test(i.live),
|
||||
"cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,
|
||||
"cat-album-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText) && /Add to playlist/.test(i.halfText) && /Go to artist/.test(i.halfText),
|
||||
"cat-track-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText),
|
||||
"cat-track-save": (i) => /Saving from /.test(i.halfText) || !i.half.includes("up"),
|
||||
"cat-album-save": (i) => /^Saving \d+ from /.test(i.save),
|
||||
@@ -324,6 +448,10 @@ const MOCK_EXPECT = {
|
||||
"playlist-undone": (i) => i.plrows === 6 && !i.notice,
|
||||
"playlist-bulk-cached": (i) => /1 saved to library/.test(i.bulkline) && !i.half.includes("up"),
|
||||
"playlist-bulk-download": (i) => /1 saved to library/.test(i.bulkline),
|
||||
"playlist-unknown-sheet": (i) => i.half.includes("up") && /Find a local match/.test(i.halfText),
|
||||
"playlist-match": (i) => i.half.includes("up") && /Find a local match/.test(i.halfText) && !/Remove from playlist/.test(i.halfText),
|
||||
"playlist-match-results": (i) => /Track 4/.test(i.halfText) && /Caribou/.test(i.halfText),
|
||||
"playlist-matched": (i) => !i.half.includes("up") && !/unresolved/.test(i.pltags) && /Linked to/.test(i.notice),
|
||||
"playlist-more": (i) => i.half.includes("up") && /Unpin/.test(i.halfText) && /Rename/.test(i.halfText) && /Delete playlist/.test(i.halfText),
|
||||
"playlist-unpinned": (i) => !i.half.includes("up") && !/Pinned/.test(i.plkind),
|
||||
"playlist-rename": (i) => i.half.includes("up") && i.promptVal === "Late night drive",
|
||||
@@ -366,8 +494,14 @@ const PHONE_STEPS = [
|
||||
{ do: `document.querySelector('.gbar .iconbtn').click()`, name: "library-ambience", wait: 1400 },
|
||||
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-ambience", wait: 1200 },
|
||||
{ do: `document.querySelector('.mini').click()`, name: "sheet", wait: 1400 },
|
||||
{ do: `document.querySelector('.np-art').click()`, name: "viewer", wait: 1200 },
|
||||
{ do: `history.back()`, name: "viewer-closed", wait: 800 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[2].click()`, name: "queue", wait: 800 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[1].click()`, name: "lyrics", wait: 3500 },
|
||||
{ do: `(function () { var b = document.querySelector('.lyr-tools'); if (b) b.click(); else __ss.openHalf('lyrics'); })()`, name: "lyrics-sheet", wait: 800 },
|
||||
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.halfsheet .row')).find(function (x) { return /Lyrics later/.test(x.textContent); }); if (r) r.click(); })()`, name: "lyrics-nudged", wait: 700 },
|
||||
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.halfsheet .row')).find(function (x) { return /Lyrics earlier/.test(x.textContent); }); if (r) r.click(); })()`, wait: 700 },
|
||||
{ do: `document.querySelector('.scrim').click()`, wait: 500 },
|
||||
{ do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 400 },
|
||||
{ do: `history.back()`, name: "after-back", wait: 1000 },
|
||||
/* a reload keeps the audio thread: the session comes back LIVE */
|
||||
@@ -403,9 +537,14 @@ const PHONE_STEPS = [
|
||||
in the library, reviewed, created, deleted again */
|
||||
{ do: `__click('.newpl', 'Import')`, wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Paste a tracklist')`, wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Daft Punk - One More Time\\nDaft Punk - Aerodynamic'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Daft Punk - One More Time\\nDaft Punk - Aerodynamic\\nID - ID'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Import')`, name: "import-review", wait: 3500 },
|
||||
{ do: `__click('.imp-foot .btn-fill', 'Create playlist')`, name: "import-created", wait: 2000 },
|
||||
/* the unknown row repaired from the real library */
|
||||
{ do: `__click('.pl-rows .trk', 'ID')`, name: "import-unknown-sheet", wait: 800 },
|
||||
{ do: `__click('.halfsheet .row', 'Find a local match')`, wait: 600 },
|
||||
{ do: `(function () { var i = document.querySelector('.plmatch input'); i.value = 'aerodynamic'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "import-match-results", wait: 1200 },
|
||||
{ do: `__click('.halfsheet .sheet-rows .row', 'Aerodynamic')`, name: "import-matched", wait: 1500 },
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 800 },
|
||||
{ do: `__click('.halfsheet .row', 'Delete playlist')`, wait: 700 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "import-deleted", wait: 1500 },
|
||||
@@ -422,6 +561,12 @@ const PHONE_STEPS = [
|
||||
{ do: `__click('.screen .row', 'Volume normalization')`, name: "you-norm-on", wait: 900 },
|
||||
{ do: `__click('.prefs[data-mode="stream"] .chip', 'Lossy only')`, name: "you-lossy", wait: 900 },
|
||||
{ do: `__click('.prefs[data-mode="stream"] .chip', 'No limit')`, name: "you-nolimit", wait: 900 },
|
||||
{ do: `document.querySelector('.slots .stepper .iconbtn:last-child').click()`, name: "you-slots", wait: 900 },
|
||||
{ do: `document.querySelector('.slots .stepper .iconbtn').click()`, name: "you-slots-back", wait: 900 },
|
||||
{ do: `__click('.screen .row', 'Word-by-word')`, name: "you-words", wait: 900 },
|
||||
{ do: `__click('.screen .row', 'Word-by-word')`, name: "you-words-off", wait: 900 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1000 px')`, name: "you-artedge", wait: 900 },
|
||||
{ do: `__click('.prefs[data-mode="albumart"] .chip', '1500 px')`, name: "you-artedge-back", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 700 },
|
||||
{ do: `document.querySelectorAll('.nav button')[3].click()`, name: "community", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 600 },
|
||||
@@ -438,6 +583,10 @@ const PHONE_STEPS = [
|
||||
const PHONE_EXPECT = {
|
||||
album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img"),
|
||||
"album-playing": (i) => i.mini && !i.toast,
|
||||
"lyrics-sheet": (i) => i.half.includes("up") && /Search again/.test(i.halfText),
|
||||
"lyrics-nudged": (i) => i.half.includes("up") && !i.toast,
|
||||
viewer: (i) => i.viewer,
|
||||
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
||||
"home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
|
||||
sheet: (i) => i.sheet.includes("up"),
|
||||
"after-back": (i) => !i.sheet.includes("up"),
|
||||
@@ -451,6 +600,12 @@ const PHONE_EXPECT = {
|
||||
"you-norm-on": (i) => i.norm === "on" && !i.toast,
|
||||
"you-lossy": (i) => i.srcMax === "Lossy only" && !i.toast,
|
||||
"you-nolimit": (i) => i.srcMax === "No limit" && !i.toast,
|
||||
"you-slots": (i) => /^\d+$/.test(i.slots) && !i.toast,
|
||||
"you-slots-back": (i) => /^\d+$/.test(i.slots) && !i.toast,
|
||||
"you-words": (i) => i.wordfill === "on" && !i.toast,
|
||||
"you-words-off": (i) => i.wordfill === "off" && !i.toast,
|
||||
"you-artedge": (i) => i.artEdge === "1000 px" && !i.toast,
|
||||
"you-artedge-back": (i) => i.artEdge === "1500 px" && !i.toast,
|
||||
playlists: (i) => i.title.startsWith("Your library"),
|
||||
"playlist-new": (i) => i.screen.includes("tr-push") && i.title.startsWith("Walk test") && i.plrows === 0 && !i.toast,
|
||||
"playlist-pick": (i) => i.half.includes("up") && /Walk test/.test(i.halfText),
|
||||
@@ -459,8 +614,11 @@ const PHONE_EXPECT = {
|
||||
"playlist-playing": (i) => i.mini && !!i.playing && !i.toast,
|
||||
"playlist-more": (i) => i.half.includes("up") && /Delete playlist/.test(i.halfText),
|
||||
"playlist-deleted": (i) => i.title.startsWith("Your library") && !/Walk test/.test(i.plnames) && !i.toast,
|
||||
"import-review": (i) => i.title.startsWith("Import") && i.imp === 2 && !i.toast,
|
||||
"import-created": (i) => i.title.startsWith("Imported playlist") && i.plrows === 2 && !i.toast,
|
||||
"import-review": (i) => i.title.startsWith("Import") && i.imp === 3 && !i.toast,
|
||||
"import-created": (i) => i.title.startsWith("Imported playlist") && i.plrows === 3 && !i.toast,
|
||||
"import-unknown-sheet": (i) => i.half.includes("up") && /Find a local match/.test(i.halfText),
|
||||
"import-match-results": (i) => /Aerodynamic/.test(i.halfText),
|
||||
"import-matched": (i) => !i.half.includes("up") && !/unresolved/.test(i.pltags) && /Linked to/.test(i.notice) && !i.toast,
|
||||
"import-deleted": (i) => i.title.startsWith("Your library") && !/Imported playlist/.test(i.plnames) && !i.toast,
|
||||
"share-import": (i) => i.title.startsWith("Import") && (i.imp > 0 || i.impErr),
|
||||
"share-cancel": (i) => i.title.startsWith("Your library"),
|
||||
@@ -487,12 +645,29 @@ function mediaSessionState() {
|
||||
/* The audio mixer's underrun count for the app's fast track: a dropout the
|
||||
* ear hears is a number here. Read before and after the walk; a rise during
|
||||
* it is a finding. */
|
||||
let appUidCache = null;
|
||||
function appUid() {
|
||||
if (appUidCache) return appUidCache;
|
||||
try { appUidCache = (adb("shell", "dumpsys", "package", "com.soulstream.app").toString().match(/\buid=(\d+)/) || [])[1] || null; } catch { appUidCache = null; }
|
||||
return appUidCache;
|
||||
}
|
||||
/* SoulStream's own tracks only: the mixer's thread counters are system-wide
|
||||
and any app's notification sound moves them. Each track row of the dump
|
||||
carries its client "pid/ uid" and an Underruns column (third from the
|
||||
end, before Flushed and BitPerfect). */
|
||||
function audioUnderruns() {
|
||||
try {
|
||||
const uid = appUid();
|
||||
if (!uid) return null;
|
||||
const dump = adb("shell", "dumpsys", "media.audio_flinger").toString();
|
||||
let n = 0;
|
||||
for (const m of dump.matchAll(/Fifo frame underruns:[^\n]*n:\s*(\d+)/g)) n += +m[1];
|
||||
for (const m of dump.matchAll(/numTracks=\d+ writeErrors=\d+ underruns=(\d+)/g)) n += +m[1];
|
||||
for (const line of dump.split("\n")) {
|
||||
const m = line.match(/(\d+)\/\s+(\d+)\s+\d+\s+\d+\s/);
|
||||
if (!m || m[2] !== uid) continue;
|
||||
const t = line.trim().split(/\s+/);
|
||||
const u = Number(t[t.length - 3]);
|
||||
if (Number.isFinite(u)) n += u;
|
||||
}
|
||||
return n;
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -502,6 +677,7 @@ async function main() {
|
||||
const problems = [];
|
||||
let cdp = null; // reassigned by the restart phase
|
||||
let underrunsBefore = null;
|
||||
let underrunRise = 0; /* summed across the restart: a relaunch starts fresh tracks at zero */
|
||||
let chrome = null;
|
||||
let steps, expect, still, prefix;
|
||||
|
||||
@@ -568,11 +744,25 @@ async function main() {
|
||||
cdp.on["Runtime.exceptionThrown"] = (p) => problems.push("EXC " + (p.exceptionDetails?.exception?.description || p.exceptionDetails?.text));
|
||||
cdp.on["Runtime.consoleAPICalled"] = (p) => {
|
||||
if (p.type !== "error" && p.type !== "warning") return;
|
||||
/* the reload step's own consequence: a pending IPC answer with nobody left to receive it */
|
||||
if (p.args.some((x) => /Couldn't find callback id/.test(String(x.value ?? "")))) return;
|
||||
const frames = (p.stackTrace?.callFrames || []).slice(0, 5)
|
||||
.map((f) => (f.functionName || "?") + "@" + (f.url || "").split("/").pop() + ":" + (f.lineNumber + 1) + ":" + (f.columnNumber + 1));
|
||||
problems.push(p.type.toUpperCase() + " " + p.args.map((a) => a.value ?? a.description).join(" ") + (frames.length ? "\n at " + frames.join("\n at ") : ""));
|
||||
};
|
||||
cdp.on["Log.entryAdded"] = (p) => { if (p.entry.level === "error") problems.push("LOG " + p.entry.text + " " + (p.entry.url || "")); };
|
||||
/* after a reload the WebView may present a NEW page target: the old
|
||||
socket then never answers. Attach again, keeping the handlers. */
|
||||
const reattach = async () => {
|
||||
const on = cdp.on;
|
||||
try { cdp.close(); } catch { /* already gone */ }
|
||||
const a = PHONE ? await attachPhone(PORT) : { cdp: await CDP.attach(PORT) };
|
||||
cdp = a.cdp;
|
||||
cdp.on = on;
|
||||
await cdp.send("Page.enable");
|
||||
await cdp.send("Runtime.enable");
|
||||
await cdp.send("Log.enable");
|
||||
};
|
||||
if (!PHONE) {
|
||||
await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: `try { localStorage.setItem('mockLibrary', '1'); } catch (e) {}` });
|
||||
await cdp.send("Emulation.setDeviceMetricsOverride", { width: 411, height: 914, deviceScaleFactor: 2, mobile: true });
|
||||
@@ -592,6 +782,9 @@ async function main() {
|
||||
} catch (e) { problems.push("STEP " + (s.name || s.do) + ": " + e.message); }
|
||||
}
|
||||
await sleep(s.wait || 500);
|
||||
if (s.do === "location.reload()") {
|
||||
try { await reattach(); } catch (e) { problems.push("STEP " + (s.name || "reload") + ": could not re-attach after the reload: " + e.message); }
|
||||
}
|
||||
if (!s.name) continue;
|
||||
await still(path.join(OUT, `${prefix}${s.name}.png`));
|
||||
const info = await cdp.evaluate(PROBE);
|
||||
@@ -608,9 +801,11 @@ async function main() {
|
||||
const PLAYER = `(function () { var p = window.__ss && __ss.player; return p && p.track ? { title: p.track.title, pos: Math.round(p.position), status: p.status, needs: !!p.needsResource } : null; })()`;
|
||||
const before = await cdp.evaluate(PLAYER);
|
||||
cdp.close();
|
||||
if (underrunsBefore != null) { const now = audioUnderruns(); if (now != null) underrunRise += Math.max(0, now - underrunsBefore); }
|
||||
adb("shell", "am", "force-stop", "com.soulstream.app");
|
||||
await sleep(800);
|
||||
adb("shell", "am", "start", "-n", "com.soulstream.app/.MainActivity");
|
||||
underrunsBefore = null; /* re-read once the relaunch plays */
|
||||
/* a relaunch restores a PAUSED track, which starts the media service
|
||||
as a foreground service that must promote itself at once — on
|
||||
2026-09-13 it did not, and Android killed the app ten seconds
|
||||
@@ -618,6 +813,7 @@ async function main() {
|
||||
resuming, and the pid must be the same one afterwards. */
|
||||
adb("logcat", "-c");
|
||||
await sleep(13000);
|
||||
underrunsBefore = audioUnderruns();
|
||||
const pid2 = adb("shell", "pidof", "com.soulstream.app").toString().trim();
|
||||
const fatal = (adb("logcat", "-d", "-s", "AndroidRuntime:E").toString().match(/FATAL EXCEPTION|DidNotStartInTime/g) || []).length;
|
||||
const alive = !!pid2 && !fatal;
|
||||
@@ -754,6 +950,24 @@ async function main() {
|
||||
const peersOk = !!netInfo && netInfo.peers >= 1;
|
||||
if (!peersOk) problems.push("EXPECT net-peers " + JSON.stringify(netInfo));
|
||||
console.log((peersOk ? " ok " : " FAIL ") + "net-peers".padEnd(16) + " " + JSON.stringify(netInfo));
|
||||
/* a peer's sheet → their shares browsed (one request; a big
|
||||
share takes seconds) → back. A share that will not open
|
||||
is reported by the screen, not a failure of the port. */
|
||||
try {
|
||||
await cdp.evaluate(`document.querySelector('.peer-head .peer-menu').click()`);
|
||||
await sleep(700);
|
||||
await cdp.evaluate(HELPERS);
|
||||
await cdp.evaluate(`__click('.halfsheet .row', 'Browse their files')`);
|
||||
const BRW = `(function () { return { title: (document.querySelector('.gb-t') || {}).textContent || '', dirs: document.querySelectorAll('.brw-dir').length, busy: !!document.querySelector('.imp-wait'), err: [].some.call(document.querySelectorAll('.screen .stub'), function (x) { return /shares/.test(x.textContent); }), head: (document.querySelector('.brw-head .m') || {}).textContent || '' }; })()`;
|
||||
let brw = null;
|
||||
for (let i = 0; i < 40; i++) { await sleep(500); brw = await cdp.evaluate(BRW); if (brw.dirs > 0 || brw.err) break; }
|
||||
await still(path.join(OUT, `${prefix}net-browse.png`));
|
||||
const brwOk = !!brw && (brw.dirs > 0 || brw.err);
|
||||
if (!brwOk) problems.push("EXPECT net-browse " + JSON.stringify(brw));
|
||||
console.log((brwOk ? " ok " : " FAIL ") + "net-browse".padEnd(16) + " " + JSON.stringify(brw));
|
||||
await cdp.evaluate(`history.back()`);
|
||||
await sleep(800);
|
||||
} catch (e) { problems.push("STEP net-browse: " + e.message); }
|
||||
await cdp.evaluate(HELPERS);
|
||||
await cdp.evaluate(`__settle()`);
|
||||
await sleep(800);
|
||||
@@ -775,11 +989,12 @@ async function main() {
|
||||
if (cdp) cdp.close();
|
||||
if (chrome) chrome.kill();
|
||||
}
|
||||
if (PHONE && underrunsBefore != null) {
|
||||
if (PHONE) {
|
||||
const after = audioUnderruns();
|
||||
const grew = after != null && after > underrunsBefore;
|
||||
if (grew) problems.push(`AUDIO underruns rose during the walk: ${underrunsBefore} → ${after}`);
|
||||
console.log((grew ? " FAIL " : " ok ") + "audio".padEnd(16) + ` mixer underruns ${underrunsBefore} → ${after}`);
|
||||
const rise = underrunRise + (underrunsBefore != null && after != null ? Math.max(0, after - underrunsBefore) : 0);
|
||||
const grew = rise > 0;
|
||||
if (grew) problems.push(`AUDIO SoulStream's tracks underran ${rise} time${rise === 1 ? "" : "s"} during the walk`);
|
||||
console.log((grew ? " FAIL " : " ok ") + "audio".padEnd(16) + ` SoulStream's tracks: ${rise} underrun${rise === 1 ? "" : "s"} during the walk`);
|
||||
}
|
||||
/* the two toasts a Tauri-less page always shows are not findings */
|
||||
const real = PHONE ? problems : problems.filter((p) => !/transformCallback|reading 'invoke'/.test(p));
|
||||
|
||||
Reference in new issue
Block a user