android: playlists, the import doors, the share sheet, You → Sources — and three bugs the walks found
The desktop's two playlist pages on the phone: the shelf (pinned first, mosaic covers, badges, New playlist, Import) and the page (kind, Pinned chip, description, the meta line's warnings, Play / Shuffle / more, a filter, rows tagged unresolved / set audio / file missing). Sheets for a card, the page's overflow, a row, pick-a-playlist, a prompt and a confirm; Add to playlist on every track and record sheet; a removal undoable for seven seconds from the neutral toast (notice.svelte.js); stream rows play through the resolver as path-less entries, a set slice starts and ends at its cues; Add to library / Download all tracks with the desktop's bulk rules; nav.replace for a review that became the thing it made. The import doors that need no file — a link (YouTube · Spotify · Apple Music · TIDAL · Deezer), a pasted tracklist, a YouTube DJ set — with the review screen and the desktop's catalog verification pass. A share plugin crate (crates/tauri-plugin-share): Share → SoulStream from any app keeps the text in a ShareActivity and the WebView hands it to the import when it comes back to the front. You → Sources: the source-quality window and the priority per mode. Found by the walks and fixed: set_settings takes camelCase arguments, so the phone's normalization and library-folder saves never landed; Cover.svelte's non-eager wrapper has no layout box, so its intersection observer never fired and row thumbnails never loaded (the Artists tab too); a history push while a history.go was still in flight left the nav mirror one entry ahead and the page blank after a confirmed delete — the mirror now applies once per microtask and never pushes during a pending traversal. The harness names unmocked commands, carries console warnings' frames (the smoke bundle is built unminified), probes the history mirror on every step, mocks app_status / play_local / waveform / lyrics_get / player_state and applies settings patches, runs adb steps on the phone (the share intent), and walks playlists, the imports, the share and the settings on both the mock and the phone. ANDROID.md documents all of it.
This commit is contained in:
1 parent
bb828ff2dc
commit
6e39dc43f3
38 files changed
+2383
-88
No files matched your search
+211
-5
@@ -73,7 +73,7 @@ const PROBE = `(function () {
|
||||
accent: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(),
|
||||
amb: [].map.call(document.querySelectorAll('.app-bg .amb'), function (a) { return a.className + ':' + (a.style.backgroundImage ? 'img' : '-'); }).join(' '),
|
||||
playing: (document.querySelector('.mini .t') || {}).textContent || '',
|
||||
toast: (document.querySelector('.toast') || {}).textContent || '',
|
||||
toast: (document.querySelector('.toast:not(.notice)') || {}).textContent || '',
|
||||
live: (document.querySelector('.mini .m.live') || {}).textContent || '',
|
||||
peers: document.querySelectorAll('.peer').length,
|
||||
songs: document.querySelectorAll('.cat-song').length,
|
||||
@@ -98,6 +98,30 @@ const PROBE = `(function () {
|
||||
news: document.querySelectorAll('.row.news').length,
|
||||
secs: [].map.call(document.querySelectorAll('.screen .sec h2'), function (h) { return h.firstChild && h.firstChild.textContent ? h.firstChild.textContent.trim() : ''; }).join('|'),
|
||||
xlive: (document.querySelector('.screen .sec h2') || {}).textContent || '',
|
||||
plcards: document.querySelectorAll('.plgrid .release').length,
|
||||
plnames: [].map.call(document.querySelectorAll('.plgrid .release .title'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||
plpinned: !!document.querySelector('.plsec.pinned'),
|
||||
plrows: document.querySelectorAll('.pl-rows .trk').length,
|
||||
pltags: [].map.call(document.querySelectorAll('.pl-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||
plkind: (document.querySelector('.rec-head.pl .kind') || {}).textContent || '',
|
||||
plfilter: !!document.querySelector('.plfilter input'),
|
||||
plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,
|
||||
imp: document.querySelectorAll('.imp-rows .trk').length,
|
||||
impTags: [].map.call(document.querySelectorAll('.imp-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||
impCounts: (document.querySelector('.imp-counts') || {}).textContent || '',
|
||||
impFmt: (document.querySelector('.imp-head .stats .s') || {}).textContent || '',
|
||||
impWait: !!document.querySelector('.imp-wait'),
|
||||
impBtn: (document.querySelector('.imp-foot .btn-fill') || {}).textContent || '',
|
||||
keepOn: !!document.querySelector('.imp-form .sw.on'),
|
||||
bulkline: (document.querySelector('.bulkline') || {}).textContent || '',
|
||||
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,
|
||||
notice: (document.querySelector('.toast.notice') || {}).textContent || '',
|
||||
promptVal: (document.querySelector('.halfsheet .prompt .field') || {}).value || '',
|
||||
/* the history mirror: the entry's depth against the layers the app says the back gesture can peel */
|
||||
hd: (history.state && history.state.d != null) ? history.state.d : -1,
|
||||
hwant: (function () { try { var n = __ss.nav; return (n.sheet ? 1 : 0) + (n.half ? 1 : 0) + (n.stacks[n.tab].length - 1) + (n.tab !== 'home' ? 1 : 0); } catch (e) { return -1; } })(),
|
||||
cardImgs: document.querySelectorAll('.rail .acard img').length,
|
||||
cardPhs: document.querySelectorAll('.rail .acard svg').length,
|
||||
};
|
||||
@@ -170,9 +194,75 @@ const MOCK_STEPS = [
|
||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 700 },
|
||||
{ do: `__click('.segtabs button', 'Gaps')`, name: "gaps", wait: 800 },
|
||||
{ do: `__click('.gapline', null)`, name: "gaps-open", wait: 1600 },
|
||||
/* Playlists: the shelf (pinned first), a page holding every row kind,
|
||||
a local row playing, the stream row through the resolver, the
|
||||
filter, a removal and its undo, the page's sheet — unpin, rename —
|
||||
a new playlist from the shelf, a record's tracks added to it from
|
||||
the record's sheet, and its deletion */
|
||||
{ do: `__click('.segtabs button', 'Playlists')`, name: "playlists", wait: 900 },
|
||||
{ do: `__click('.plgrid .release', 'Late night')`, name: "playlist", wait: 1200 },
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[1].click()`, name: "playlist-playing", wait: 1200 },
|
||||
{ do: `__click('.pl-rows .trk', 'One More Time')`, name: "playlist-stream", wait: 5000 },
|
||||
{ do: `(function () { var i = document.querySelector('.plfilter input'); i.value = 'track 4'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "playlist-filter", wait: 500 },
|
||||
{ do: `(function () { var i = document.querySelector('.plfilter input'); i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 400 },
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "playlist-row-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Remove from playlist')`, name: "playlist-removed", wait: 900 },
|
||||
{ do: `__click('.toast.notice .na', 'Undo')`, name: "playlist-undone", wait: 1000 },
|
||||
/* the bulk actions from the page's sheet: Add to library files the
|
||||
streamed row (its cache is complete); Download all finds it cached too */
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Add to library')`, name: "playlist-bulk-cached", wait: 1500 },
|
||||
{ do: `__click('.bulkline .bulkx', null)`, wait: 400 },
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
{ do: `__click('.halfsheet .row', 'Rename')`, name: "playlist-rename", wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Late night drive II'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Rename')`, name: "playlist-renamed", wait: 1000 },
|
||||
{ do: `history.back()`, name: "playlists-again", wait: 900 },
|
||||
{ do: `__click('.newpl', 'New playlist')`, name: "playlist-new-prompt", wait: 700 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Create')`, name: "playlist-new", wait: 1200 },
|
||||
{ do: `history.back()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 },
|
||||
{ do: `document.querySelector('.grid2 .release').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Add to playlist')`, name: "playlist-pick", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'New Playlist')`, name: "playlist-added", wait: 1200 },
|
||||
{ do: `__click('.segtabs button', 'Playlists')`, wait: 700 },
|
||||
{ do: `__click('.plgrid .release', 'New Playlist')`, name: "playlist-filled", wait: 1200 },
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 },
|
||||
{ do: `__click('.halfsheet .row', 'Delete playlist')`, name: "playlist-delete-confirm", wait: 700 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "playlist-deleted", wait: 1200 },
|
||||
/* the import doors: a pasted tracklist reviewed and created with the
|
||||
unresolved row kept out; then a link — its stream rows verified
|
||||
against the catalog while the review is up — created as it came */
|
||||
{ do: `__click('.newpl', 'Import')`, name: "import-sheet", wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Paste a tracklist')`, name: "import-paste-prompt", wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Daft Punk - One More Time\\nAphex Twin - Track 1\\nID - ID'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Import')`, name: "import-review", wait: 2200 },
|
||||
{ do: `__click('.imp-form .row', 'Keep unresolved')`, name: "import-keep-off", wait: 500 },
|
||||
{ do: `__click('.imp-foot .btn-fill', 'Create playlist')`, name: "import-created", wait: 1500 },
|
||||
{ do: `history.back()`, wait: 900 },
|
||||
{ do: `__click('.newpl', 'Import')`, wait: 700 },
|
||||
{ do: `__click('.halfsheet .row', 'Playlist from a link')`, wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Import')`, name: "import-link-wait", wait: 500 },
|
||||
{ name: "import-link-review", wait: 3400 },
|
||||
{ do: `__click('.imp-foot .btn-fill', 'Create playlist')`, name: "import-link-created", wait: 1500 },
|
||||
{ do: `history.back()`, name: "playlists-after-import", wait: 900 },
|
||||
/* a share from another app: the plugin hands the text over when the
|
||||
app comes to the front, and it lands in the import */
|
||||
{ do: `__ssMock.share = 'Check this out https://music.apple.com/us/playlist/late/pl.u-mock'; document.dispatchEvent(new Event('visibilitychange'))`, name: "share-import", wait: 3600 },
|
||||
{ do: `__click('.imp-foot .btn-ghost', null)`, name: "share-cancel", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 },
|
||||
{ do: `__settle()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.nav button')[4].click()`, name: "you", wait: 700 },
|
||||
{ do: `__click('.screen .row', 'Volume normalization')`, name: "you-norm-off", wait: 500 },
|
||||
{ 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: `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 },
|
||||
@@ -183,7 +273,10 @@ const MOCK_EXPECT = {
|
||||
tracks: (i) => i.title.startsWith("Your library"),
|
||||
community: (i) => i.title.startsWith("Community"),
|
||||
"home-more": (i) => i.half.includes("up"),
|
||||
you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow,
|
||||
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",
|
||||
"sleep-sheet": (i) => i.half.includes("up"),
|
||||
"sleep-set": (i) => !i.half.includes("up"),
|
||||
home: (i) => /^(Good |Still)/.test(i.title),
|
||||
@@ -220,6 +313,41 @@ const MOCK_EXPECT = {
|
||||
transfers: (i) => i.title.startsWith("Transfers") && i.xfers >= 11,
|
||||
gaps: (i) => i.title.startsWith("Your library") && i.gaplines >= 1,
|
||||
"gaps-open": (i) => i.gapCards >= 3,
|
||||
playlists: (i) => i.title.startsWith("Your library") && i.plcards === 3 && i.plpinned,
|
||||
artists: (i) => i.title.startsWith("Your library") && i.rowArt >= 3,
|
||||
playlist: (i) => i.screen.includes("tr-push") && i.title.startsWith("Late night drive") && i.plrows === 6 && i.plthumbs >= 3 && /unresolved/.test(i.pltags) && /file missing/.test(i.pltags) && /^Playlist/.test(i.plkind) && /Pinned/.test(i.plkind) && i.plfilter,
|
||||
"playlist-playing": (i) => i.mini && i.playing.includes("Track 2") && !i.toast,
|
||||
"playlist-stream": (i) => i.playing.includes("One More Time") && /streaming from/.test(i.live),
|
||||
"playlist-filter": (i) => i.plrows === 1,
|
||||
"playlist-row-sheet": (i) => i.half.includes("up") && /Remove from playlist/.test(i.halfText),
|
||||
"playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice),
|
||||
"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-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",
|
||||
"playlist-renamed": (i) => i.title.startsWith("Late night drive II"),
|
||||
"playlists-again": (i) => i.plcards === 3 && !i.plpinned && /Late night drive II/.test(i.plnames),
|
||||
"playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \d+$/.test(i.promptVal),
|
||||
"playlist-new": (i) => i.screen.includes("tr-push") && /^New Playlist/.test(i.title) && i.plrows === 0,
|
||||
"playlist-pick": (i) => i.half.includes("up") && /New playlist/.test(i.halfText) && /Late night drive II/.test(i.halfText),
|
||||
"playlist-added": (i) => /Added \d+ tracks to New Playlist/.test(i.notice),
|
||||
"playlist-filled": (i) => /^New Playlist/.test(i.title) && i.plrows >= 5,
|
||||
"playlist-delete-confirm": (i) => i.half.includes("up") && /Delete/.test(i.halfText),
|
||||
"playlist-deleted": (i) => i.title.startsWith("Your library") && i.plcards === 3 && !/New Playlist/.test(i.plnames),
|
||||
"import-sheet": (i) => i.half.includes("up") && /Playlist from a link/.test(i.halfText) && /Paste a tracklist/.test(i.halfText),
|
||||
"import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "",
|
||||
"import-review": (i) => i.title.startsWith("Import") && i.imp === 3 && /1 in library/.test(i.impCounts) && /tags/.test(i.impTags) && /unresolved/.test(i.impTags) && /catalog/.test(i.impTags) && i.keepOn && /Create playlist \(3 tracks\)/.test(i.impBtn),
|
||||
"import-keep-off": (i) => !i.keepOn && /Create playlist \(2 tracks\)/.test(i.impBtn),
|
||||
"import-created": (i) => i.title.startsWith("Imported playlist") && i.plrows === 2,
|
||||
/* the wait card is transient: the still can take longer than the mock's fetch */
|
||||
"import-link-wait": (i) => i.title.startsWith("Import") && (i.impWait || i.imp === 5),
|
||||
"import-link-review": (i) => i.imp === 5 && /Spotify/.test(i.impFmt) && /catalog/.test(i.impTags) && !/matching/.test(i.impTags) && /Create playlist \(5 tracks\)/.test(i.impBtn),
|
||||
"import-link-created": (i) => i.title.startsWith("Late Night Mix") && i.plrows === 5,
|
||||
"playlists-after-import": (i) => i.title.startsWith("Your library") && i.plcards === 5,
|
||||
"share-import": (i) => i.title.startsWith("Import") && /Apple Music/.test(i.impFmt) && i.imp === 5,
|
||||
"share-cancel": (i) => i.title.startsWith("Your library"),
|
||||
"cat-album-row": (i) => i.playing.includes("Aerodynamic") && /streaming from/.test(i.live),
|
||||
"cat-album-next": (i) => i.playing.includes("Digital Love") && /streaming from/.test(i.live),
|
||||
"cat-album-disc": (i) => i.playing.includes("One More Time") && /streaming from/.test(i.live),
|
||||
@@ -253,6 +381,48 @@ const PHONE_STEPS = [
|
||||
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, wait: 600 },
|
||||
/* the artist page: the library's shell at once, the catalog dissolving in */
|
||||
/* button.row: the page header is a .row too, and it is a div */
|
||||
/* playlists on the real core: one made here, a record's tracks added
|
||||
from the record's sheet, a row played, the playlist deleted again —
|
||||
the phone's library is left as it was */
|
||||
{ do: `__click('.segtabs button', 'Playlists')`, name: "playlists", wait: 900 },
|
||||
{ do: `__click('.newpl', 'New playlist')`, wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Walk test'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Create')`, name: "playlist-new", wait: 1500 },
|
||||
{ do: `history.back()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 500 },
|
||||
{ do: `(function () { var all = document.querySelectorAll('.grid2 .release'); var c = null; for (var i = 0; i < all.length; i++) { if (!${albumText} || all[i].textContent.indexOf(${albumText}) >= 0) { c = all[i]; break; } } if (!c) throw new Error('no album card'); c.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); })()`, wait: 900 },
|
||||
{ do: `__click('.halfsheet .row', 'Add to playlist')`, name: "playlist-pick", wait: 800 },
|
||||
{ do: `__click('.halfsheet .row', 'Walk test')`, name: "playlist-added", wait: 1500 },
|
||||
{ do: `__click('.segtabs button', 'Playlists')`, wait: 800 },
|
||||
{ do: `__click('.plgrid .release', 'Walk test')`, name: "playlist-filled", wait: 1500 },
|
||||
{ do: `document.querySelectorAll('.pl-rows .trk')[0].click()`, name: "playlist-playing", wait: 3000 },
|
||||
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "playlist-more", wait: 800 },
|
||||
{ do: `__click('.halfsheet .row', 'Delete playlist')`, wait: 700 },
|
||||
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "playlist-deleted", wait: 1500 },
|
||||
/* the paste door on the real core: two lines that resolve to a record
|
||||
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: `__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 },
|
||||
{ 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 },
|
||||
/* the share sheet: a Spotify playlist link sent by "another app" (an
|
||||
intent on the plugin's activity) lands in the import — the review is
|
||||
left without creating anything */
|
||||
{ adb: ["shell", "am", "start", "-a", "android.intent.action.SEND", "-t", "text/plain", "--es", "android.intent.extra.TEXT", "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M", "-n", "com.soulstream.app/com.soulstream.share.ShareActivity"], name: "share-import", wait: 9000 },
|
||||
{ do: `__click('.imp-foot .btn-ghost', null)`, name: "share-cancel", wait: 900 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 500 },
|
||||
/* You: the settings save for real (camelCase args) — the switch and a
|
||||
chip, each read back from the config, each restored */
|
||||
{ do: `document.querySelectorAll('.nav button')[4].click()`, name: "you", wait: 900 },
|
||||
{ do: `__click('.screen .row', 'Volume normalization')`, name: "you-norm-off", wait: 900 },
|
||||
{ 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.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 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, wait: 600 },
|
||||
@@ -276,6 +446,24 @@ const PHONE_EXPECT = {
|
||||
"hold-sheet": (i) => i.half.includes("up"),
|
||||
tracks: (i) => i.title.startsWith("Your library"),
|
||||
"artist-cold": (i) => i.screen.includes("has-hero"),
|
||||
you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax,
|
||||
"you-norm-off": (i) => i.norm === "off" && !i.toast,
|
||||
"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,
|
||||
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),
|
||||
"playlist-added": (i) => /Added .* to Walk test/.test(i.notice) && !i.toast,
|
||||
"playlist-filled": (i) => i.title.startsWith("Walk test") && i.plrows >= 1,
|
||||
"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-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"),
|
||||
artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && i.tabs >= 1,
|
||||
"catalog-album": (i) => i.screen.includes("tr-push") && !!i.title,
|
||||
end: (i) => /^(Good |Still)/.test(i.title),
|
||||
@@ -349,7 +537,8 @@ async function main() {
|
||||
} else {
|
||||
if (!CHROME) throw new Error("Chrome not found");
|
||||
console.log("building dist-smoke/ …");
|
||||
const b = spawnSync("npx", ["vite", "build", "--outDir", "dist-smoke", "--base", "./"], { cwd: ROOT, shell: true, encoding: "utf8" });
|
||||
/* unminified, so a warning's frames name the function */
|
||||
const b = spawnSync("npx", ["vite", "build", "--outDir", "dist-smoke", "--base", "./", "--minify", "false"], { cwd: ROOT, shell: true, encoding: "utf8" });
|
||||
if (b.status !== 0) { console.error(b.stdout, b.stderr); throw new Error("vite build failed"); }
|
||||
/* a profile per run: a Chrome left behind by an interrupted run keeps
|
||||
its crashpad file open, and removing that profile throws EBUSY */
|
||||
@@ -378,7 +567,10 @@ async function main() {
|
||||
await cdp.send("Log.enable");
|
||||
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") problems.push(p.type.toUpperCase() + " " + p.args.map((a) => a.value ?? a.description).join(" "));
|
||||
if (p.type !== "error" && p.type !== "warning") 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 || "")); };
|
||||
if (!PHONE) {
|
||||
@@ -388,6 +580,10 @@ async function main() {
|
||||
await sleep(600);
|
||||
}
|
||||
for (const s of steps) {
|
||||
/* a phone step can be an adb command — an intent, a key */
|
||||
if (s.adb) {
|
||||
try { adb(...s.adb); } catch (e) { problems.push("ADB " + (s.name || s.adb.join(" ")) + ": " + e.message); }
|
||||
}
|
||||
if (s.do) {
|
||||
try {
|
||||
/* the helpers live in the page; a reload step wipes them */
|
||||
@@ -415,8 +611,18 @@ async function main() {
|
||||
adb("shell", "am", "force-stop", "com.soulstream.app");
|
||||
await sleep(800);
|
||||
adb("shell", "am", "start", "-n", "com.soulstream.app/.MainActivity");
|
||||
await sleep(6000);
|
||||
/* 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
|
||||
later (a Save died with it). Wait past that window BEFORE
|
||||
resuming, and the pid must be the same one afterwards. */
|
||||
adb("logcat", "-c");
|
||||
await sleep(13000);
|
||||
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;
|
||||
if (!alive) problems.push("EXPECT survives-paused-restore " + JSON.stringify({ pid: pid2, fatal }));
|
||||
console.log((alive ? " ok " : " FAIL ") + "paused-restore".padEnd(16) + " " + JSON.stringify({ pid: pid2 || null, fatal }));
|
||||
adb("forward", "--remove-all");
|
||||
adb("forward", `tcp:${PORT}`, `localabstract:webview_devtools_remote_${pid2}`);
|
||||
cdp = await CDP.attach(PORT);
|
||||
|
||||
Reference in new issue
Block a user