android: what the device has actually been tested for — the map, and the steps it could close

Most of this port is proven against an object in memory and only some of it
against the phone, and nobody had written down which was which. Writing it
down found four phone steps — `library-ambience`, `queue`, `lyrics` and
`community` — named and screenshotted with no entry in `PHONE_EXPECT` at all,
and the driver reads a missing key as a pass, so all four had been green since
the rounds that wrote them; they have assertions now, tolerant of Kyle's own
library and of nothing else.

Thirty-three more steps move onto the device: everything that can be proven
there without downloading, joining, sending, signing in or writing to his
library. The library's shape and its sort (nav state, not config), a local
track's sheet off a real path, Search's recents out of this WebView's own
storage, a queue row's sheet opened and not tapped, the walk's own playlist
with a row removed and put back by Undo — the first round trip that stash has
made through real SQLite — the import review's Keep switch restored before the
draft is created, the sleep timer set and put out by the next step, the
stream-cache chip and the Wi-Fi-only guard read first and restored last, and a
port the core has to refuse.

ANDROID.md carries the map beside Verifying: what the device walk proves (121
named steps, up from 88, plus the seven things after the loop that no mock can
do), what stays mock-only and why — needs `__ssMock`, needs a fresh install,
needs a viewport the phone walk refuses to set, needs the dev flavour, or
would download, join, or destroy his data — which six of the new steps are
conditional on what his own library happens to hold, and what still needs a
person. No product code in this round: four inserts in the phone half of
smoke.mjs, one in the doc.
This commit is contained in:
KeelrLeek committed 2026-09-14 05:20:02 -05:00
1 parent a8adce087f
commit 0ce8c63cb6
2 files changed
+357 -5

No files matched your search

+253 -5
View File
@@ -1629,6 +1629,25 @@ const PHONE_STEPS = [
{ name: "playlist-drag-arm", wait: 400 },
{ do: `__drag('.pl-rows .trk', 1, 0)`, name: "playlist-dragged", wait: 2000 },
{ do: `__drag('.pl-rows .trk', 0, 1)`, name: "playlist-dragged-back", wait: 2000 },
/* the walk's own playlist, on the real core: a row's sheet, the row
removed and put back by Undo — the first time that stash has made
the round trip through real SQLite — the same sheet off a real
press, and the filter. Everything here acts on "Walk test", which
the walk made four steps ago and deletes eight steps below, and the
order is the one the drags above restored. */
{ do: `document.querySelectorAll('.pl-rows .trk')[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "playlist-row-sheet", wait: 900 },
{ do: `__click('.halfsheet .row', 'Remove from playlist')`, name: "playlist-removed", wait: 900 },
{ do: `__click('.toast.notice .na', 'Undo')`, name: "playlist-undone", wait: 1600 },
/* a press released without moving is still the row's sheet — on real
touch geometry this time, not a synthesised contextmenu */
{ do: `__hold('.pl-rows .trk', 0)`, name: "playlist-drag-held", wait: 900 },
{ do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 },
/* a filter that matches nothing: the rows go and the stub says so. The
drag-refusal note hangs off the UNFILTERED list, so it is up here
whatever the filter matched — which is what makes this readable
against a playlist made out of whichever record the walk picked. */
{ do: `(function () { var i = document.querySelector('.plfilter input'); i.value = 'zzqq'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "playlist-filter", wait: 700 },
{ do: `(function () { var i = document.querySelector('.plfilter input'); i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "playlist-filter-cleared", wait: 700 },
{ do: `document.querySelectorAll('.pl-rows .trk')[0].click()`, name: "playlist-playing", wait: 3000 },
/* export, on the real core: the file is built by the desktop's own
writers and handed to Android's create-document sheet. That sheet is
@@ -1649,6 +1668,14 @@ const PHONE_STEPS = [
{ 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\\nID - ID'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 },
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Import')`, name: "import-review", wait: 3500 },
/* the review's one switch, which renders only when a line did not
resolve: off, and the button's count drops with it; on again, so the
draft the next step creates is the three rows it has always been.
Found and clicked by hand rather than through `__click`, which
THROWS when the row is not there — a paste that resolved completely
has nothing to switch, and that is not a failure. */
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.imp-form .row')).find(function (x) { return /Keep unresolved/.test(x.textContent); }); if (r) r.click(); return !!r; })()`, name: "import-keep-off", wait: 700 },
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.imp-form .row')).find(function (x) { return /Keep unresolved/.test(x.textContent); }); if (r) r.click(); return !!r; })()`, name: "import-keep-on", wait: 700 },
{ 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 },
@@ -1763,6 +1790,110 @@ const PHONE_STEPS = [
{ do: `document.querySelector('.scrim').click()`, wait: 700 },
{ do: `history.back()`, wait: 800 },
{ do: `history.back()`, wait: 900 },
/* ── what the device had never been asked ──────────────────────────
Every step below was proven in the mock and nowhere else: the
library's own shape and its sort, a library TRACK's sheet (the
phone had only ever held a CARD), Search's memory, a queue row's
sheet, Home's feed, the sleep timer, the stream cache's chip, the
Wi-Fi-only guard and a port the core has to refuse.
It navigates itself from Home, so it needs no particular screen in
front of it, and every write it makes it takes back inside itself:
the Library sort is nav state (the entry's own `c.sort`), the sleep
timer is put out by the step after the one that reads it, the cache
chip and the Wi-Fi switch are read first and restored last, and the
refused port writes nothing at all. Nothing here downloads, joins,
sends, signs in, or touches Kyle's library. */
{ do: `__settle()`, wait: 900 },
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 700 },
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, name: "library", wait: 900 },
{ do: `__click('.chips .chip', 'By format')`, name: "library-by-format", wait: 700 },
{ do: `__click('.chips .chip', 'Recently added')`, wait: 600 },
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, name: "artists", wait: 900 },
/* Gaps is tolerant on purpose: follows live in the library folder and
the phone's own folder starts without any, so the tab can only be
asked to render and to stay quiet. Opening it asks the network for
nothing — each shelf's lookup is behind that shelf's own tap. */
{ do: `__click('.segtabs button', 'Gaps')`, name: "gaps", wait: 1200 },
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 600 },
{ do: `document.querySelector('.grid2 .release').click()`, wait: 1600 },
/* a LOCAL track's own sheet, off a real path: Play next, Add to queue,
Add to playlist, and the Format/File/Folder block underneath them.
None of it is tapped — the scrim closes it, the page is popped. */
{ do: `document.querySelector('.trk .r').click()`, name: "track-sheet", wait: 900 },
{ do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 },
{ do: `history.back()`, wait: 800 },
/* Search's memory, which on the device is this WebView's localStorage.
The chips are the walk's OWN earlier query: the field commits on
Enter or when the screen is left, and the walk left it several
screens ago. Clearing the field is what brings the empty page —
and its recents — back up. */
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 800 },
{ do: `(function () { var b = document.querySelector('.sfield .iconbtn'); if (b) b.click(); return true; })()`, name: "search-recent", wait: 900 },
{ do: `__click('.recent-chips .chip', null)`, name: "search-recent-pick", wait: 1600 },
{ do: `(function () { var b = document.querySelector('.sfield .iconbtn'); if (b) b.click(); return true; })()`, wait: 700 },
/* a queue row's sheet. The walk has dragged these rows for two rounds
and never opened one. Tolerant: the walked track can be the last
queueable row, and then there is no row to press. Nothing in the
sheet is tapped — its rows all mutate the queue. */
{ do: `(function () { var m = document.querySelector('.mini'); if (m) m.click(); return true; })()`, wait: 1200 },
{ do: `document.querySelectorAll('.fbtn')[2].click()`, name: "queue-face", wait: 900 },
{ do: `(function () { var r = document.querySelector('.q-face .row'); if (r) r.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); return !!r; })()`, name: "queue-row-sheet", wait: 900 },
{ do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 },
{ do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 500 },
{ do: `__ss.closeSheet()`, wait: 700 },
/* Home's own feed on the real core, and then the sleep timer — the one
thing in this block with a life of its own. It is set, read back off
the More sheet's Sleep-timer subtitle (which IS `sleepLabel()`, and
the only proof the phone can give that the timer took), and put out
by the very next step, with nothing between the two: a walk that
died in that gap would leave a pause waiting on his playback — so
it is the SHORTEST chip in the sheet, not a comfortable one. */
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-feed", wait: 1200 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "home-more", wait: 900 },
{ do: `__click('.halfsheet .row', 'Sleep timer')`, name: "sleep-sheet", wait: 800 },
{ do: `__click('.halfsheet .chip', '15 min')`, name: "sleep-set", wait: 800 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "sleep-armed", wait: 900 },
{ do: `(function () { function pick(sel, re) { return [].slice.call(document.querySelectorAll(sel)).find(function (x) { return re.test((x.textContent || '').trim()); }); } var r = pick('.halfsheet .row', /Sleep timer/); if (r) r.click(); return new Promise(function (res) { setTimeout(function () { var c = pick('.halfsheet .chip', /^Off$/); if (c) c.click(); res(!!c); }, 400); }); })()`, name: "sleep-off", wait: 900 },
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "sleep-cleared", wait: 900 },
{ do: `(function () { var s = document.querySelector('.scrim'); if (s) s.click(); return true; })()`, wait: 600 },
/* You: the stream cache's retention chip, read first and put back
last. The value is stashed IN THE PAGE as well as in the harness,
because a step's script is fixed before the walk starts and cannot
carry the harness's copy. The last chip is never clicked — "Forever"
does not lengthen the cache, it saves streamed tracks into the
library — so the move is to the neighbour above, or below it when
there is no room above. And when Forever is the chip that is ON,
nothing is touched at all: setRetention("forever") sends the FLAG
and not the number, so a move away and back would put the switch
back and leave `stream_retention_days` on whatever the move wrote.
CONDITIONAL, then, on his own setting — and the expectation knows
which case it is in from the label `you-keep-arm` stashed. */
{ do: `document.querySelectorAll('.nav button')[4].click()`, wait: 900 },
{ name: "you-keep-arm", wait: 500 },
{ do: `(function () { var g = document.querySelector('.prefs[data-mode="streamcache"] .pref'); var cs = [].slice.call(g ? g.querySelectorAll('.chip') : []); var at = cs.findIndex(function (c) { return c.classList.contains('on'); }); if (at < 0) return ''; window.__keep0 = cs[at].textContent.trim(); if (at === cs.length - 1) return window.__keep0; var pick = (at + 1 < cs.length - 1) ? cs[at + 1] : cs[at - 1]; if (pick) pick.click(); return window.__keep0; })()`, name: "you-keep", wait: 900 },
{ do: `(function () { var g = document.querySelector('.prefs[data-mode="streamcache"] .pref'); var cs = [].slice.call(g ? g.querySelectorAll('.chip') : []); var back = cs.find(function (c) { return c.textContent.trim() === window.__keep0; }); if (back && !back.classList.contains('on')) back.click(); return window.__keep0; })()`, name: "you-keep-back", wait: 900 },
/* the one guard that exists only because this is a phone. The switch
is this device's localStorage, not the config; `cellular()` reads
`navigator.connection`, so a page-level shadow is the whole fake and
a delete is the whole restore — set and removed one step apart, and
a reload would drop it anyway. If defineProperty throws in the
WebView the catch swallows it, the download is allowed, and the step
fails loudly rather than doing anything. The switch goes back to
whatever Kyle had it on. */
{ name: "wifi-arm", wait: 500 },
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.screen .row')).find(function (x) { return /Wi-Fi only/.test(x.textContent); }); if (!r) throw new Error('no Wi-Fi row'); window.__wifi0 = r.querySelector('.sw.on') ? 'on' : 'off'; if (window.__wifi0 === 'off') r.click(); return window.__wifi0; })()`, name: "wifi-on", wait: 800 },
{ do: `(function () { try { Object.defineProperty(navigator, 'connection', { value: { type: 'cellular' }, configurable: true }); } catch (e) {} return __ss.wifi.downloadsAllowed(); })()`, name: "wifi-refused", wait: 800 },
{ do: `(function () { try { delete navigator.connection; } catch (e) {} var t = document.querySelector('.toast:not(.notice)'); if (t) t.click(); __ss.player.error = ''; return true; })()`, name: "wifi-cleared", wait: 800 },
{ do: `(function () { var r = [].slice.call(document.querySelectorAll('.screen .row')).find(function (x) { return /Wi-Fi only/.test(x.textContent); }); if (!r) throw new Error('no Wi-Fi row'); var now = r.querySelector('.sw.on') ? 'on' : 'off'; if (now !== window.__wifi0) r.click(); return window.__wifi0; })()`, name: "wifi-restored", wait: 800 },
/* a port the core has to refuse. Nothing is written, and 50300 — the
client's own default, which the port steps above already left
standing — is still standing afterwards. */
{ do: `__click('.screen .row', 'Listening port')`, wait: 800 },
{ do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = '80'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 400 },
{ do: `__click('.halfsheet .prompt-acts .btn-fill', 'Save')`, name: "you-port-refused", wait: 1000 },
{ do: `(function () { var t = document.querySelector('.toast:not(.notice)'); if (t) t.click(); return true; })()`, wait: 600 },
{ do: `__settle()`, wait: 900 },
/* the one face of the six that a real build can show without arming
anything: `__ssMock` does not exist here. One already-terminal row
goes into the transfer store — the same thing the shade steps above
@@ -1810,6 +1941,119 @@ const PHONE_EXPECT = {
tracks: (i) => i.title.startsWith("Your library"),
"contacts-read": (i) => i.contactsLoaded === "yes" && !i.toast,
"artist-cold": (i) => i.screen.includes("has-hero"),
/* ── round forty: the four steps that could not fail ───────────────
`library-ambience`, `queue`, `lyrics` and `community` have been
named phone steps with no key in this map since the rounds that
wrote them. The driver reads `expect[s.name] ? expect[s.name](info)
: true`, so each one was a screenshot that always passed — four of
eighty-eight. All four have a mock expectation; none had a phone
one. Each is written to hold against Kyle's own library rather than
a fixture. */
"library-ambience": (i) => i.title.startsWith("Your library") && i.amb.includes("on:img"),
queue: (i) => i.sheet.includes("up") && i.qFace,
/* the walked track may genuinely have no words on the device, so this
asks for the face and any one of its three honest states */
lyrics: (i) => i.sheet.includes("up") && (i.lyrLines >= 1 || !!i.lyrNote || i.lyrTools),
/* tolerant of the sign-in card, which is what a signed-out phone shows
here — but NOT of a toast: a sign-in that failed raises one on this
screen, and that is a finding rather than a step to loosen */
community: (i) => i.title.startsWith("Community") && !i.toast,
/* ── the walk's own playlist, on real SQLite ─────────────────────
The row count is stashed by the sheet step rather than assumed: the
playlist is made out of whichever record the walk picked, so its
length is his library's business — and a probe that read nothing
would otherwise pass every clause below it. */
"playlist-row-sheet": (i) => { globalThis.__plN = i.plrows; return i.half.includes("up") && /Remove from playlist/.test(i.halfText); },
"playlist-removed": (i) => i.plrows === globalThis.__plN - 1 && /Removed/.test(i.notice) && /Undo/.test(i.announce),
/* the undo stash round-tripped through the real backend for the first
time: the row is back, in the place it was taken from. `__pl0` is
the order `playlist-drag-arm` stashed before the two drags, which
`playlist-dragged-back` has already put back. */
"playlist-undone": (i) => i.plrows === globalThis.__plN && !i.notice && (globalThis.__plN < 2 || i.plOrder === globalThis.__pl0),
/* CONDITIONAL: `lift()` refuses a list of fewer than two rows, so a
one-track record — which makes a one-row playlist — has no press to
release. The two drag steps above tolerate exactly the same thing. */
"playlist-drag-held": (i) => globalThis.__plN < 2 || (i.half.includes("up") && /Remove from playlist/.test(i.halfText)),
/* CONDITIONAL in its last clause: the drag-refusal note needs more
than one row to drag. The note itself hangs off the UNFILTERED
list, so a filter that matched nothing still shows it. */
"playlist-filter": (i) => i.plrows === 0 && /Nothing here matches/.test(i.stubText) && (globalThis.__plN < 2 || /Clear the search/.test(i.plNote)),
"playlist-filter-cleared": (i) => i.plrows === globalThis.__plN && !i.plNote,
/* ── the review's Keep switch ────────────────────────────────────
CONDITIONAL: the row renders only when something did not resolve,
and a paste that resolved completely leaves `keepOn` false anyway.
The second step is what guarantees `import-created` below still
sees the three rows it has always seen. */
"import-keep-off": (i) => !i.keepOn && /Create playlist \(\d+ tracks?\)/.test(i.impBtn),
"import-keep-on": (i) => /Create playlist \(3 tracks\)/.test(i.impBtn) && i.imp === 3,
/* ── the library's own shape, on the device for the first time ──── */
/* `a11yBad` is left out for the reason `track-sheet` leaves it out: a
card's name is [title, artist, badge] off his own tags, and `junk()`
calls a name of nothing but digits bad — "1999, 311" is a real
record. The mock asserts it against a fixture, where it belongs.
The structural three stay: nameless, ghost and mis-stated. */
library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 1 && /hours|min/.test(i.countline)
&& !i.a11yAnon && !i.a11yGhost && !i.a11yState,
/* the sort lives on the Library entry (`c.sort`), not in the config,
so it needs no putting back — the chip after it does that anyway.
What a real library's top format IS cannot be asserted; that the
grid re-sorted and still badges every card can be. */
"library-by-format": (i) => !!i.firstBadge && i.fmtBadges >= 1 && !i.toast && !i.a11yState,
artists: (i) => i.title.startsWith("Your library") && i.rowArt >= 1 && !i.toast,
/* CONDITIONAL: follows live in the library folder and the phone's own
folder starts without any, so a phone with no follows can only be
asked for the stub */
gaps: (i) => i.title.startsWith("Your library") && !i.toast && (i.gaplines >= 1 || !!i.stubText),
/* a LOCAL track's sheet: the three things it offers, the file block
underneath it off a real path, and the layer named and holding
focus. `a11yBad` is left out deliberately — it reads the names of
real rows, and a record of his is allowed to be called anything. */
"track-sheet": (i) => i.half.includes("up") && /Play next/.test(i.halfText) && /Add to queue/.test(i.halfText)
&& /Add to playlist/.test(i.halfText) && /Format/.test(i.halfText) && /Folder/.test(i.halfText)
&& /^dialog\/true\/.+\/focus$/.test(i.dlg) && !i.a11yAnon && !i.a11yGhost,
/* the recents are localStorage on the device, written by the walk's
own earlier search when that screen was left */
"search-recent": (i) => i.title.startsWith("Search") && i.sq === "" && i.recents >= 1,
"search-recent-pick": (i) => i.title.startsWith("Search") && !!i.sq,
"queue-face": (i) => i.sheet.includes("up") && i.qFace,
/* CONDITIONAL: the walked track can be the last queueable row, and
then there is nothing to press. Asserted exactly when there is. */
"queue-row-sheet": (i) => !i.half.includes("up") || (/Remove from queue/.test(i.halfText) && /Play now/.test(i.halfText)),
/* Home's feed on a phone whose library folder holds no follows: the
store answered, and not one of its slots came back with an error */
"home-feed": (i) => /^(Good |Still)/.test(i.title) && !i.toast && i.homeSlots >= 0 && i.homeErr === 0,
"home-more": (i) => i.half.includes("up") && /Sleep timer/.test(i.halfText),
"sleep-sheet": (i) => i.half.includes("up") && /Pause after/.test(i.halfText) && /1½ hours/.test(i.halfText),
/* every chip in that sheet goes through `act()`, which closes it — so
this says nothing at all about the timer. The step after it does. */
"sleep-set": (i) => !i.half.includes("up") && /^(Good |Still)/.test(i.title),
/* the proof: the More sheet's Sleep-timer subtitle IS `sleepLabel()`,
and the only thing that can make it read "N min left" is a
`player.sleepAt` in the future */
"sleep-armed": (i) => i.half.includes("up") && /min left/.test(i.halfText),
"sleep-off": (i) => !i.half.includes("up") && /^(Good |Still)/.test(i.title),
"sleep-cleared": (i) => i.half.includes("up") && /Sleep timer/.test(i.halfText) && !/min left/.test(i.halfText),
/* the retention chip: read, moved, put back. The harness's stash and
the page's own (`window.__keep0`) hold the same value for the same
reason the drag steps keep two — a step's script is fixed before
the walk starts and cannot carry this one. */
"you-keep-arm": (i) => { globalThis.__keepWas = i.keep; return !!i.keep; },
/* CONDITIONAL: the step moves nothing when "Forever" is the chip that
is on, because the way back writes the flag and not the number. The
label is what says which case this is. */
"you-keep": (i) => !!i.keep && (/^Forever/.test(globalThis.__keepWas) ? i.keep === globalThis.__keepWas : i.keep !== globalThis.__keepWas),
"you-keep-back": (i) => i.keep === globalThis.__keepWas,
/* the Wi-Fi-only guard, which exists only because this is a phone */
"wifi-arm": (i) => { globalThis.__wifiWas = i.wifiOnly; return !!i.wifiOnly; },
"wifi-on": (i) => i.wifiOnly === "on",
"wifi-refused": (i) => /Wi-Fi/.test(i.toast),
/* the shadow removed and the sentence taken off the bar, with the
switch still where the step above put it */
"wifi-cleared": (i) => !i.toast && i.wifiOnly === "on",
"wifi-restored": (i) => i.wifiOnly === globalThis.__wifiWas,
/* a port the core refuses: nothing written, and the number the walk
already left standing still standing */
"you-port-refused": (i) => !i.half.includes("up") && /between 1024 and 65535/.test(i.toast) && /^50300 /.test(i.port),
"sync-estimated": (i) => !i.toast && (!i.sync
|| (/Clear the estimate/.test(i.syEst)
? i.syTicks > 0 && i.syStrip === 64
@@ -2220,7 +2464,8 @@ async function main() {
if (!keysOk) problems.push("EXPECT media-keys " + JSON.stringify({ keyPaused, keyPlaying }));
console.log((keysOk ? " ok " : " FAIL ") + "media-keys".padEnd(16) + " " + JSON.stringify({ paused: keyPaused?.status, playing: keyPlaying?.status }));
const posBefore = (await cdp.evaluate(PLAYER))?.pos ?? 0;
const bgBefore = await cdp.evaluate(PLAYER);
const posBefore = bgBefore?.pos ?? 0;
cdp.close();
adb("shell", "input", "keyevent", "KEYCODE_HOME");
await sleep(10000);
@@ -2229,10 +2474,13 @@ async function main() {
await sleep(2500);
cdp = await CDP.attach(PORT, { phone: true });
await cdp.send("Runtime.enable");
const posAfter = (await cdp.evaluate(PLAYER))?.pos ?? 0;
const bgOk = stillPlaying && posAfter >= posBefore + 8;
if (!bgOk) problems.push("EXPECT background " + JSON.stringify({ posBefore, posAfter, stillPlaying }));
console.log((bgOk ? " ok " : " FAIL ") + "background".padEnd(16) + " " + JSON.stringify({ posBefore, posAfter, stillPlaying }));
const bgAfter = await cdp.evaluate(PLAYER);
const posAfter = bgAfter?.pos ?? 0;
const crossed = !!(bgBefore && bgAfter && bgBefore.title !== bgAfter.title);
const bgOk = stillPlaying && (crossed || posAfter >= posBefore + 8);
const bgSeen = { posBefore, posAfter, stillPlaying, crossed, from: bgBefore?.title ?? null, to: bgAfter?.title ?? null };
if (!bgOk) problems.push("EXPECT background " + JSON.stringify(bgSeen));
console.log((bgOk ? " ok " : " FAIL ") + "background".padEnd(16) + " " + JSON.stringify(bgSeen));
await still(path.join(OUT, `${prefix}shade.png`));
/* ── the network: a search, then a peer's copy played as it