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:
1 parent
a8adce087f
commit
0ce8c63cb6
2 files changed
+357
-5
No files matched your search
+253
-5
@@ -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
|
||||
|
||||
Reference in new issue
Block a user