android: what the phone says when things go wrong
Fifty-nine places put a raw Rust string into a field a person reads, and the red toast drew it verbatim — a phone in a lift showed "could not reach server: io: failed to lookup address information" under the password box. One translator, src/mobile/trouble.js, called where an error is DRAWN rather than where it is caught: the stores keep what Rust said and only the sentence on screen is ours, so every sink is fixed at once. Five screens that drew a failed read as an empty answer stop lying — Tracks no longer reports a broken database as an empty library, a playlist that could not be read is not an empty playlist, the lyrics face says a lookup failed instead of "no lyrics" and finally carries the ⋯ that reaches Paste lyrics, Gaps says why it could not reach the catalogue, and Search signed out says so where it used to render nothing at all. The room list, which is fire-and-forget over the wire, gets a twelve-second end and a Try again. rescan_library ran prune_missing() first, which deletes every track row whose file does not exist — so one tap on You → Rescan library with the folder unreachable emptied the whole index and returned Ok(()). It now makes the root if it is merely not there yet and refuses if it cannot be read, and You draws the path with what to do about it. The mock grew a weather system so the walk can drive all of this: fail, answer, offline and clear, checked before dispatch so a plugin command arms like any other. Nine mock steps and one on the phone — the first steps in thirty rounds that ever made a command reject or saw the client signed out — and the last of them puts the weather away and leaves the app as it found it.
This commit is contained in:
1 parent
44eda8dff5
commit
052e15302b
22 files changed
+537
-35
No files matched your search
@@ -251,6 +251,26 @@ const PROBE = `(function () {
|
||||
halfSub: ((document.querySelector('.halfsheet .sheet-head .m') || {}).textContent || '').trim(),
|
||||
halfSw: [].filter.call(document.querySelectorAll('.halfsheet .row'), function (r) { return !!r.querySelector('.sw.on'); }).map(function (r) { return ((r.querySelector('.t') || {}).textContent || '').trim(); }).join('|'),
|
||||
contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),
|
||||
/* ── the weather: what the screen says when a read fails, an answer
|
||||
is empty, or nobody is signed in. Every one of these is a line
|
||||
this round put there; before it they were an empty box, a spinner
|
||||
with no end, or a Rust string in the red toast. */
|
||||
slskOn: (function () { try { return __ss.slsk.connection; } catch (e) { return ''; } })(),
|
||||
signin: !!document.querySelector('.signin'),
|
||||
signinErr: (document.querySelector('.signin-err') || {}).textContent || '',
|
||||
why: (document.querySelector('.stub.why') || {}).textContent || '',
|
||||
/* the toast's one action, by its words: "Try again" or "Sign in" */
|
||||
retry: (document.querySelector('.toast:not(.notice) .na') || {}).textContent || '',
|
||||
again: !![].slice.call(document.querySelectorAll('.imp-foot .btn')).find(function (b) { return /Try again/.test(b.textContent); }),
|
||||
roomsNote: (document.querySelector('.rooms-note') || {}).textContent || '',
|
||||
offlineNote: (document.querySelector('.offline-note') || {}).textContent || '',
|
||||
netAsk: !!document.querySelector('.net-ask'),
|
||||
folderGone: (document.querySelector('.folder-gone') || {}).textContent || '',
|
||||
lyrNote: (document.querySelector('.lyr-note') || {}).textContent || '',
|
||||
lyrTools: !!document.querySelector('.lyr-tools'),
|
||||
trks: document.querySelectorAll('.screen .trk').length,
|
||||
/* the phone's trouble-words step leaves its eleven sentences here */
|
||||
trouble: (function () { try { return String(window.__trouble || ''); } catch (e) { return ''; } })(),
|
||||
syStrip: document.querySelectorAll('.sy-strip .sy-col').length,
|
||||
syTicks: document.querySelectorAll('.sy-strip .sy-tick').length,
|
||||
syEst: (document.querySelector('.sy-est') || {}).textContent || '',
|
||||
@@ -745,6 +765,62 @@ const MOCK_STEPS = [
|
||||
{ view: [411, 914], wait: 800 },
|
||||
{ do: `document.querySelector('.mini').click()`, name: "portrait-again", wait: 1200 },
|
||||
{ do: `__ss.closeSheet()`, wait: 600 },
|
||||
/* ── the weather ────────────────────────────────────────────────────
|
||||
Everything above this line is the happy path; nothing in thirty
|
||||
rounds of walk had ever made a command reject or taken the client
|
||||
offline, so none of these lines had ever been drawn. The mock's own
|
||||
doors (__ssMock.fail / answer / offline / clear) are the weather, and
|
||||
every assertion below is on SETTLED state — an armed failure stays
|
||||
armed, a sign-in card stays up, the folder-gone row stays — because
|
||||
the probe lands after the screenshot, well past the step's own wait.
|
||||
The one timed assertion is the room list, and its timeout is shortened
|
||||
to 1.2 s against the 3.1 s of waits between arming it and reading it.
|
||||
The last step puts the weather away and
|
||||
leaves the app exactly as it found it — anything appended after this
|
||||
block inherits a signed-in app on Home.
|
||||
|
||||
The first step settles FIRST, and it has to: the landscape block above
|
||||
leaves the library tab three screens deep (record → artist), so one tap
|
||||
on the Library tab lands on the artist page and `.segtabs button` would
|
||||
be its discography tabs, not Albums/Artists/Tracks. It also wipes any
|
||||
error the happy path left standing, so `!i.toast` below means this
|
||||
block's toast and no other. */
|
||||
{ do: `(function () { __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; return __settle(); })()`, wait: 800 },
|
||||
{ do: `__ssMock.fail('library_track_index', 'unable to open database file')`, wait: 300 },
|
||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 700 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[2].click()`, name: "weather-tracks", wait: 1300 },
|
||||
{ do: `__ssMock.fail('library_track_index', '')`, wait: 200 },
|
||||
{ do: `__click('.imp-foot .btn', 'Try again')`, name: "weather-tracks-ok", wait: 1300 },
|
||||
/* signed out, in a lift: the card is back and nothing says why in Rust */
|
||||
{ do: `__ssMock.offline(true)`, wait: 600 },
|
||||
{ do: `document.querySelectorAll('.nav button')[3].click()`, name: "weather-signedout", wait: 1100 },
|
||||
/* the single most visible raw-Rust site on the phone, twice over */
|
||||
{ do: `__ssMock.fail('connect_and_login', 'login failed: INVALIDPASS')`, wait: 200 },
|
||||
{ do: `(function () { var p = document.querySelectorAll('.signin .field')[1]; p.value = 'hunter2'; p.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`, wait: 400 },
|
||||
{ do: `__click('.signin .btn-fill', 'Sign in')`, name: "weather-badpass", wait: 1200 },
|
||||
{ do: `__ssMock.fail('connect_and_login', 'could not reach server: io: failed to lookup address information: no address associated with hostname')`, wait: 200 },
|
||||
{ do: `__click('.signin .btn-fill', 'Sign in')`, name: "weather-noserver", wait: 1200 },
|
||||
/* Search signed out with a query typed showed NOTHING where the network
|
||||
section lives — no row, no reason. Now it says so and offers the door */
|
||||
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 },
|
||||
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'daft punk'; i.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`, name: "weather-search-out", wait: 1800 },
|
||||
/* the room list is fire-and-forget over the wire (the client sends it and
|
||||
nothing answers if the server will not), so the wait is ours: an armed
|
||||
answer that resolves and emits nothing is exactly that server. Community
|
||||
is left on Buddies by the walk above, so the Rooms tab is asked for by
|
||||
name — the note lives under that one. */
|
||||
{ do: `__ssMock.fail('connect_and_login', ''); __ssMock.offline(false)`, wait: 1200 },
|
||||
{ do: `(function () { __ss.slsk.roomsWaitMs = 1200; __ss.slsk.rooms = []; __ssMock.answer('slsk_room_list', null); __ss.refreshRooms(); return true; })()`, wait: 200 },
|
||||
{ do: `document.querySelectorAll('.nav button')[3].click()`, wait: 900 },
|
||||
{ do: `__click('.segtabs button', 'Rooms')`, name: "weather-rooms-late", wait: 2200 },
|
||||
/* one tap on Rescan library used to empty the whole index and say Ok */
|
||||
{ do: `__ssMock.fail('rescan_library', 'library-folder-missing: /storage/1A2B-3C4D/Music')`, wait: 200 },
|
||||
{ do: `document.querySelectorAll('.nav button')[4].click()`, wait: 800 },
|
||||
{ do: `__click('.screen .row', 'Rescan library')`, name: "weather-rescan", wait: 1500 },
|
||||
/* and the weather clears */
|
||||
{ do: `(function () { __ss.slsk.roomsWaitMs = 12000; __ssMock.clear(); __ss.refreshRooms(); __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; return true; })()`, wait: 900 },
|
||||
{ do: `__settle()`, wait: 900 },
|
||||
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "weather-clear", wait: 1000 },
|
||||
];
|
||||
const MOCK_EXPECT = {
|
||||
tracks: (i) => i.title.startsWith("Your library"),
|
||||
@@ -791,6 +867,24 @@ const MOCK_EXPECT = {
|
||||
"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"),
|
||||
/* ── the weather ────────────────────────────────────────────────────
|
||||
Each of these fails if the round's fix is undone: the stub if a failed
|
||||
read goes back to being drawn as an empty library, the sign-in lines if
|
||||
the toast or the card stops translating, the search note if the block
|
||||
goes back to rendering nothing at all signed out, the room note if the
|
||||
spinner loses its end, the folder row if the rescan stops refusing.
|
||||
Every one also asserts the RAW string is gone — that is the round. */
|
||||
"weather-tracks": (i) => /^Couldn't read the library index/.test(i.why) && /library folder isn't readable/.test(i.why)
|
||||
&& i.again && i.trks === 0 && !/database file/.test(i.why + i.toast),
|
||||
"weather-tracks-ok": (i) => !i.why && !i.again && i.trks > 0 && !i.toast,
|
||||
"weather-signedout": (i) => i.slskOn === "offline" && i.signin && !i.signinErr && !i.toast,
|
||||
"weather-badpass": (i) => i.signinErr === "Soulseek refused that password." && !/INVALIDPASS/.test(i.signinErr),
|
||||
"weather-noserver": (i) => /^Couldn't reach the Soulseek server/.test(i.signinErr) && !/io:|address information/.test(i.signinErr),
|
||||
"weather-search-out": (i) => i.title.startsWith("Search") && /Community/.test(i.offlineNote) && !i.netAsk,
|
||||
"weather-rooms-late": (i) => i.slskOn === "online" && /hasn't sent its room list/.test(i.roomsNote) && i.again,
|
||||
"weather-rescan": (i) => /1A2B-3C4D/.test(i.folderGone) && /isn't there any more/.test(i.toast) && /1A2B-3C4D/.test(i.toast)
|
||||
&& !/library-folder-missing/.test(i.toast + i.folderGone),
|
||||
"weather-clear": (i) => !i.toast && !i.why && !i.folderGone && !i.roomsNote && i.slskOn === "online" && /^(Good |Still)/.test(i.title),
|
||||
/* sideways: the chrome budget, then every surface that was clipped. The
|
||||
before-value of each is in the rig's now.json — 46% of the height as
|
||||
chrome, a 879px cover with the transport 687px below the fold, a
|
||||
@@ -1119,6 +1213,12 @@ const PHONE_STEPS = [
|
||||
{ do: `history.back()`, wait: 800 },
|
||||
{ do: `history.back()`, wait: 800 },
|
||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 },
|
||||
/* the translator, on the real build: the backend's own vocabulary in,
|
||||
plain sentences out. No side effects — nothing is signed out (the
|
||||
login throttle is a remembered trap), nothing is asked of the network,
|
||||
nothing is written. It also catches a build where trouble.js simply
|
||||
did not ship: __ss.say would not be there and the line stays empty. */
|
||||
{ do: `(function () { var raw = ['not connected to server', 'not connected to Soulseek', 'io: failed to lookup address information: no address associated with hostname', 'protocol: unexpected end of frame', 'login failed: INVALIDPASS', 'could not reach server: io: connection refused', 'timed out', 'user dusty_crates is offline or unknown', 'no stored credentials', 'unable to open database file', 'library-folder-missing: /storage/1A2B-3C4D/Music']; window.__trouble = raw.map(function (r) { return __ss.say(r); }).join(' || '); return window.__trouble; })()`, name: "trouble-words", wait: 400 },
|
||||
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "end", wait: 800 },
|
||||
];
|
||||
const PHONE_EXPECT = {
|
||||
@@ -1197,6 +1297,11 @@ const PHONE_EXPECT = {
|
||||
&& /^[a-z]+:(ready\+art|none):(ready|error):\d+$/.test(i.artFast),
|
||||
"artist-more": (i) => i.half.includes("up") && /Choose hero photo/.test(i.halfText),
|
||||
"catalog-album": (i) => i.screen.includes("tr-push") && !!i.title,
|
||||
/* eleven real backend strings through the one translator on the device:
|
||||
every output a sentence, and not one of them still carrying Rust */
|
||||
"trouble-words": (i) => i.trouble.split(" || ").length === 11
|
||||
&& i.trouble.split(" || ").every(function (s) { return s.length > 12; })
|
||||
&& !/io:|protocol:|os error|INVALIDPASS|not connected|library-folder-missing|no stored credentials/i.test(i.trouble),
|
||||
end: (i) => /^(Good |Still)/.test(i.title),
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user