Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
394 lines
29 KiB
JavaScript
394 lines
29 KiB
JavaScript
// Round twenty-eight: what a screen reader says. No new Tauri command and no
|
|
// backend call at all, so src/lib/api.js and src/lib/mockLibrary.js are
|
|
// deliberately untouched — nothing here can warn about an unmocked command.
|
|
// Nothing visible changes either: every edit is an attribute, a role, one
|
|
// live region clipped to a pixel, and the probe that holds it all at zero.
|
|
const fs = require('fs');
|
|
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
|
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
|
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
|
|
let p, s;
|
|
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => fs.writeFileSync(root + p, s);
|
|
const once = (a) => {
|
|
const i = s.indexOf(a);
|
|
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
|
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
|
return i;
|
|
};
|
|
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
|
const NL = String.fromCharCode(10);
|
|
|
|
/* Two mechanical sweeps below are regex replace-alls (aria-pressed on the
|
|
choice chips, aria-checked on the switch rows) and would happily apply
|
|
themselves a second time. Refuse a re-run before anything is written:
|
|
a11yAnon is created by this round and exists nowhere in the tree today. */
|
|
if (fs.readFileSync(root + 'tests/mobile/smoke.mjs', 'utf8').includes('a11yAnon')) {
|
|
throw new Error('round 28 is already applied in ' + root + ' — reset the tree rather than re-running');
|
|
}
|
|
|
|
/* the thirteen choice chips: one uniform line, so one regex — with the
|
|
count asserted first, so a chip added by a later round is noticed here
|
|
rather than quietly skipped */
|
|
const CHIP = /<button class="chip" class:on=\{([^}]*)\} class:quiet=\{[^}]*\} onclick=/g;
|
|
const chips = (expected) => {
|
|
const n = (s.match(CHIP) || []).length;
|
|
if (n !== expected) throw new Error(p + ': expected ' + expected + ' choice chips, found ' + n);
|
|
s = s.replace(CHIP, (m, on) => m.replace('onclick=', 'aria-pressed={' + on + '} onclick='));
|
|
};
|
|
|
|
// ── the shell: the nav is a tab set, the mini says what is playing, and
|
|
// one live region is mounted for the life of the app ────────────────
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);`,
|
|
` const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);` + NL + ins('ins-r28-mobile-script.txt'));
|
|
rep(` <nav class="nav" class:gone={nav.sheet}>`,
|
|
` <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -- the
|
|
five destinations ARE a tab set, and the desktop's own segmented
|
|
control says so too; wrapping them in an inner div to keep <nav>'s
|
|
implicit role would change the bar's flex layout -->
|
|
<nav class="nav" class:gone={nav.sheet} role="tablist" aria-label="Sections" aria-hidden={nav.sheet ? "true" : undefined}>`);
|
|
rep(` <button class:on={nav.tab === id} onclick={() => go(id)}>`,
|
|
` <button class:on={nav.tab === id} onclick={() => go(id)} role="tab" aria-selected={nav.tab === id} aria-label={tabName(id, label)}>`);
|
|
/* the digits are in the tab's name now, so the dot itself is decoration */
|
|
rep(`{#if id === "you" && activeTransfers().length}<span class="nb">{activeTransfers().length}</span>{/if}`,
|
|
`{#if id === "you" && activeTransfers().length}<span class="nb" aria-hidden="true">{activeTransfers().length}</span>{/if}`);
|
|
rep(`{#if id === "community" && unreadTotal()}<span class="nb">{unreadTotal()}</span>{/if}`,
|
|
`{#if id === "community" && unreadTotal()}<span class="nb" aria-hidden="true">{unreadTotal()}</span>{/if}`);
|
|
/* the mini bar swallowed the title and the artist that are visibly in it;
|
|
and while a sheet is over it, it leaves the tree rather than sitting
|
|
off-screen in it. aria-hidden only, never inert — the walk clicks it */
|
|
rep(`role="button" tabindex="0" onkeydown={() => {}} aria-label="Now playing"`,
|
|
`role="button" tabindex="0" onkeydown={() => {}} aria-hidden={nav.sheet ? "true" : undefined} aria-label={"Now playing: " + (player.track.title || "") + (player.track.artist || player.album?.artist ? " · " + (player.track.artist || player.album?.artist) : "")}`);
|
|
rep(` <div class="scrim" class:on={!!nav.half} onclick={closeHalf} role="presentation"></div>`,
|
|
` <div class="scrim" class:on={!!nav.half} onclick={closeHalf} aria-hidden="true"></div>`);
|
|
rep(` {#if player.error || lib.error}
|
|
<div class="toast" onclick={() => { player.error = ""; lib.error = ""; }} role="status">{player.error || lib.error}</div>
|
|
{/if}
|
|
{#if notice.text}
|
|
<div class="toast notice" role="status">
|
|
<span class="nt">{notice.text}</span>
|
|
{#if notice.action}<button class="na" onclick={() => { const f = notice.action.fn; clearNote(); f?.(); }}>{notice.action.label}</button>{/if}
|
|
</div>
|
|
{/if}`, ins('ins-r28-toasts.txt'));
|
|
save();
|
|
|
|
// ── the look: the live region, and nothing else ─────────────────────────
|
|
load('src/mobile/shell.css');
|
|
s = s.replace(/\s*$/, NL) + `
|
|
/* The one live region: never drawn, never laid out, always there — so an
|
|
announcement is a change of text inside it rather than a node arriving
|
|
with its words already in it (PreviewOverlay.svelte's .sr-announce, at
|
|
phone scale) */
|
|
.sr-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
`;
|
|
save();
|
|
|
|
// ── the player sheet: a dialog, with somewhere to land ──────────────────
|
|
load('src/mobile/Sheet.svelte');
|
|
rep(`<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}>`,
|
|
`<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}
|
|
role="dialog" aria-modal="true" aria-label={"Now playing" + (player.track ? ": " + player.track.title : "")}
|
|
aria-hidden={nav.sheet ? undefined : "true"} inert={!nav.sheet}>`);
|
|
rep(` function flip(a) {`,
|
|
` /* Somewhere sensible to land when the sheet opens: its own Close. The
|
|
gate is nav.sheet and not "shown" — "shown" is false for a frame at
|
|
the start of a morph arrival and the dialog has to be live by then —
|
|
and preventScroll because that arrival is measuring rects in the
|
|
same frames. */
|
|
$effect(() => {
|
|
if (!nav.sheet) return;
|
|
const t = setTimeout(() => {
|
|
const el = document.querySelector(".sheet.up .np-head .iconbtn");
|
|
if (el && !el.closest(".sheet")?.contains(document.activeElement)) el.focus({ preventScroll: true });
|
|
}, 160);
|
|
return () => clearTimeout(t);
|
|
});
|
|
|
|
function flip(a) {`);
|
|
/* the artwork opens the viewer, so it is a control and not decoration */
|
|
rep(` <div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)} role="presentation">`,
|
|
` <div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)}
|
|
role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork" + (player.album?.title ? " — " + player.album.title : "")}>`);
|
|
rep(` <div class="np-faces">`, ` <div class="np-faces" role="tablist" aria-label="Player faces">`);
|
|
rep(` <button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)}><Icon name={ic} size={17} /><span>{lb}</span></button>`,
|
|
` <button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)} role="tab" aria-selected={face === f}><Icon name={ic} size={17} /><span>{lb}</span></button>`);
|
|
save();
|
|
|
|
// ── the half sheet: a dialog that names itself, and a grab that closes ──
|
|
load('src/mobile/HalfSheet.svelte');
|
|
rep(` import Icon from "../components/Icon.svelte";`,
|
|
` import { tick } from "svelte";` + NL + ` import Icon from "../components/Icon.svelte";`);
|
|
rep(` $effect(() => { if (nav.half) shown = nav.half; });`,
|
|
` $effect(() => { if (nav.half) shown = nav.half; });
|
|
|
|
/* What this sheet is called, for a screen reader: whatever its own
|
|
first title says. Every kind writes one — a .sheet-title, or the
|
|
head's .t — so read it back out of the DOM rather than keeping a
|
|
list of twenty-seven names in step with the markup. */
|
|
let hsName = $state("");
|
|
let grabEl = $state(null);
|
|
$effect(() => {
|
|
if (!nav.half) return;
|
|
void shown;
|
|
tick().then(() => {
|
|
const el = document.querySelector(".halfsheet .sheet-title, .halfsheet .sheet-head .t");
|
|
hsName = el ? (el.textContent || "").replace(/\\s+/g, " ").trim() : "Options";
|
|
});
|
|
});
|
|
/* the grab handle is where the focus lands — unless the sheet has a
|
|
field, which takes it for itself a moment later (focusLater) */
|
|
$effect(() => {
|
|
if (!nav.half) return;
|
|
const t = setTimeout(() => {
|
|
if (!grabEl || document.querySelector(".halfsheet .field")) return;
|
|
if (grabEl.closest(".halfsheet")?.contains(document.activeElement)) return;
|
|
grabEl.focus({ preventScroll: true });
|
|
}, 160);
|
|
return () => clearTimeout(t);
|
|
});`);
|
|
/* the scrim was the only tap-dismissal and a screen reader could not reach
|
|
it; the grab handle is the first one that can be. The scrim still works */
|
|
rep(`<div class="halfsheet" class:up={!!nav.half}>
|
|
<div class="grab"></div>`,
|
|
`<div class="halfsheet" class:up={!!nav.half} role="dialog" aria-modal="true" aria-label={hsName || "Options"}
|
|
aria-hidden={nav.half ? undefined : "true"} inert={!nav.half}>
|
|
<button class="grab" bind:this={grabEl} onclick={closeHalf} aria-label="Close this sheet"></button>`);
|
|
rep(`{#snippet swrow(icon, t, m, on, fn)}
|
|
<button class="row" onclick={fn}>`,
|
|
`{#snippet swrow(icon, t, m, on, fn)}
|
|
<button class="row" onclick={fn} role="switch" aria-checked={on}>`);
|
|
rep(` <button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })}>`,
|
|
` <button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })} role="switch" aria-checked={!!m.favorite}>`);
|
|
rep(` <button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })}>`,
|
|
` <button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })} role="switch" aria-checked={!!m.trusted}>`);
|
|
/* a placeholder stops being a name the moment there is text in the field,
|
|
so both prompts take the sheet's own title instead */
|
|
rep(` <textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" use:focusLater></textarea>`,
|
|
` <textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" aria-label={a.title} use:focusLater></textarea>`);
|
|
rep(`inputmode={a.numeric ? "numeric" : undefined} enterkeyhint="done"`,
|
|
`inputmode={a.numeric ? "numeric" : undefined} aria-label={a.title} enterkeyhint="done"`);
|
|
chips(3);
|
|
save();
|
|
|
|
// ── the viewer: its own dismissal, named (ArtworkViewer.svelte's words) ─
|
|
load('src/mobile/Viewer.svelte');
|
|
rep(` <div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up} role="presentation">`,
|
|
` <div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}
|
|
role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") closeViewer(); }} aria-label="Close artwork viewer">`);
|
|
save();
|
|
|
|
// ── the tap-sync room is a dialog too ───────────────────────────────────
|
|
load('src/mobile/SyncRoom.svelte');
|
|
rep(` <div class="syncroom">`, ` <div class="syncroom" role="dialog" aria-modal="true" aria-label="Tap in time">`);
|
|
save();
|
|
|
|
// ── the waveform: the app's one position control, so a slider ───────────
|
|
load('src/mobile/Lane.svelte');
|
|
rep(` function pu() { down = false; }`,
|
|
` function pu() { down = false; }
|
|
/* a seek from the keyboard, or from TalkBack: two per cent a press —
|
|
the fraction the pointer path already speaks */
|
|
function key(e) {
|
|
if (!onseek) return;
|
|
const step = e.key === "ArrowRight" ? 0.02 : e.key === "ArrowLeft" ? -0.02 : 0;
|
|
if (!step) return;
|
|
e.preventDefault();
|
|
onseek(Math.max(0, Math.min(1, played + step)));
|
|
}`);
|
|
rep(`<span
|
|
class="lane {cls}"`,
|
|
`<!-- svelte-ignore a11y_no_noninteractive_tabindex -- the role and the
|
|
tabindex arrive together: a lane that can seek is a slider, and one
|
|
that cannot is out of the tree entirely -->
|
|
<span
|
|
class="lane {cls}"`);
|
|
rep(` onpointercancel={pu}
|
|
>`,
|
|
` onpointercancel={pu}
|
|
onkeydown={key}
|
|
role={onseek ? "slider" : undefined}
|
|
tabindex={onseek ? 0 : undefined}
|
|
aria-hidden={onseek ? undefined : "true"}
|
|
aria-label={onseek ? "Seek" : undefined}
|
|
aria-valuemin={onseek ? 0 : undefined}
|
|
aria-valuemax={onseek ? 100 : undefined}
|
|
aria-valuenow={onseek ? Math.round(played * 100) : undefined}
|
|
>`);
|
|
save();
|
|
|
|
// ── the release card: the badge belongs in the name, not glued to the
|
|
// title ("FLAC 24Discovery2001 · 14 tracks" was the whole name) ───────
|
|
load('src/mobile/Release.svelte');
|
|
rep(`<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path}>`,
|
|
`<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path} aria-label={[title, sub, badge].filter(Boolean).join(", ")}>`);
|
|
rep(` {#if badge}<span class="ed-badge">{badge}</span>{/if}`,
|
|
` {#if badge}<span class="ed-badge" aria-hidden="true">{badge}</span>{/if}`);
|
|
save();
|
|
|
|
// ── a record: the cover opens the viewer, and each row says which row ───
|
|
load('src/mobile/screens/Album.svelte');
|
|
rep(` <div class="rec-cover" onclick={() => openViewer(album.art_path)} role="presentation"><Cover path={album.art_path} eager size={48} /></div>`,
|
|
` <div class="rec-cover" onclick={() => openViewer(album.art_path)} role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork — " + album.title}><Cover path={album.art_path} eager size={48} /></div>`);
|
|
rep(` <button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }}>`,
|
|
` <button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }} aria-label={(t.track_no ?? i + 1) + ". " + t.title + (various && t.artist ? " · " + t.artist : "")}>`);
|
|
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}}`,
|
|
` <span class="r" role="button" tabindex="0" aria-label={"More for " + t.title} onkeydown={() => {}}`);
|
|
save();
|
|
|
|
// ── the library: three segmented tab groups are tab groups ──────────────
|
|
load('src/mobile/screens/Library.svelte');
|
|
rep(`<div class="segtabs">
|
|
{#each TABS as [k, l] (k)}
|
|
<button class:on={tab === k} onclick={() => patch({ tab: k })}>{l}</button>`,
|
|
`<div class="segtabs" role="tablist" aria-label="Library sections">
|
|
{#each TABS as [k, l] (k)}
|
|
<button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k}>{l}</button>`);
|
|
rep(` <button class="gapline" onclick={() => toggleGaps(ar.name)}>`,
|
|
` <button class="gapline" onclick={() => toggleGaps(ar.name)} aria-expanded={!!gapOpen[key]}>`);
|
|
chips(1);
|
|
save();
|
|
|
|
load('src/mobile/screens/Community.svelte');
|
|
rep(` <div class="segtabs" style="padding-top:12px">`,
|
|
` <div class="segtabs" style="padding-top:12px" role="tablist" aria-label="Community sections">`);
|
|
rep(` <button class:on={tab === k} onclick={() => patch({ tab: k })}>
|
|
{l}{#if k === "chat" && unreadTotal()} <span class="count">{unreadTotal()}</span>{/if}`,
|
|
` <button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k} aria-label={l + (k === "chat" && unreadTotal() ? ", " + unreadTotal() + " unread" : "")}>
|
|
{l}{#if k === "chat" && unreadTotal()} <span class="count" aria-hidden="true">{unreadTotal()}</span>{/if}`);
|
|
rep(` <input class="field" bind:value={user} placeholder="Username" autocomplete="username"`,
|
|
` <input class="field" bind:value={user} placeholder="Username" aria-label="Soulseek username" autocomplete="username"`);
|
|
rep(` <input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" autocomplete="current-password"`,
|
|
` <input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" aria-label="Soulseek password" autocomplete="current-password"`);
|
|
save();
|
|
|
|
load('src/mobile/screens/Artist.svelte');
|
|
rep(` <div class="segtabs disco-tabs">`, ` <div class="segtabs disco-tabs" role="tablist" aria-label="Discography">`);
|
|
rep(` <button class:on={tab === t.key} onclick={() => (tab = t.key)}>{t.label}<span class="count">{t.items.length}</span></button>`,
|
|
` <button class:on={tab === t.key} onclick={() => (tab = t.key)} role="tab" aria-selected={tab === t.key} aria-label={t.label + ", " + t.items.length}>{t.label}<span class="count" aria-hidden="true">{t.items.length}</span></button>`);
|
|
/* the biography IS the button's name — that is why the probe lets an
|
|
expander off its long-name check */
|
|
rep(` <p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" onkeydown={() => {}}>{extras.bio}</p>`,
|
|
` <p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}</p>`);
|
|
save();
|
|
|
|
load('src/mobile/screens/CatalogAlbum.svelte');
|
|
rep(` <p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" onkeydown={() => {}}>{desc.rest}</p>`,
|
|
` <p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" aria-expanded={descOpen} onkeydown={() => {}}>{desc.rest}</p>`);
|
|
save();
|
|
|
|
// ── the fields that had nothing but a placeholder ───────────────────────
|
|
load('src/mobile/screens/Room.svelte');
|
|
rep(` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" autocomplete="off"`,
|
|
` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" aria-label={"Message #" + name} autocomplete="off"`);
|
|
/* the roster button's whole name was the digits on it */
|
|
rep(` <button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })}>{room.users.length} <Icon name="users" size={14} /></button>`,
|
|
` <button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })} aria-label={room.users.length + " people in this room"}>{room.users.length} <Icon name="users" size={14} /></button>`);
|
|
save();
|
|
|
|
load('src/mobile/screens/Thread.svelte');
|
|
rep(` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" autocomplete="off"`,
|
|
` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" aria-label={"Message " + display(username)} autocomplete="off"`);
|
|
save();
|
|
|
|
// ── the per-row ⋯ says which row it belongs to ──────────────────────────
|
|
load('src/mobile/screens/Playlist.svelte');
|
|
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>`,
|
|
` <span class="r" role="button" tabindex="0" aria-label={"More for " + item.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>`);
|
|
save();
|
|
|
|
load('src/mobile/screens/Search.svelte');
|
|
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>`,
|
|
` <span class="r" role="button" tabindex="0" aria-label={"More for " + row.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>`);
|
|
chips(2);
|
|
save();
|
|
|
|
load('src/mobile/screens/Tracks.svelte');
|
|
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>`,
|
|
` <span class="r" role="button" tabindex="0" aria-label={"More for " + r.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>`);
|
|
save();
|
|
|
|
// ── the switch rows say on or off ───────────────────────────────────────
|
|
load('src/mobile/screens/Import.svelte');
|
|
rep(` <button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)}>`,
|
|
` <button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)} role="switch" aria-checked={!!importer.includeUnresolved}>`);
|
|
save();
|
|
|
|
load('src/mobile/screens/You.svelte');
|
|
rep(` <button class="row" onclick={() => setSetting(k, !settings[k])}>`,
|
|
` <button class="row" onclick={() => setSetting(k, !settings[k])} role="switch" aria-checked={!!settings[k]}>`);
|
|
rep(` <button class="row" onclick={toggleNormalization}>`,
|
|
` <button class="row" onclick={toggleNormalization} role="switch" aria-checked={normalization}>`);
|
|
rep(` <button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)}>`,
|
|
` <button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)} role="switch" aria-checked={!!settings.wifiOnly}>`);
|
|
rep(` <button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)}>`,
|
|
` <button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)} role="switch" aria-checked={!!cfg?.word_timing}>`);
|
|
rep(` <button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))}>`,
|
|
` <button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))} role="switch" aria-checked={!!(cfg?.write_folder_cover ?? true)}>`);
|
|
chips(7);
|
|
save();
|
|
|
|
// ── the walk: four numbers, held at zero on ten screens ─────────────────
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` var e = document.querySelector('.screen');`,
|
|
` var e = document.querySelector('.screen');` + NL + ins('ins-r28-probe.txt'));
|
|
rep(` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`,
|
|
` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),` + NL + ins('ins-r28-fields.txt'));
|
|
// the mock walk
|
|
rep(` home: (i) => /^(Good |Still)/.test(i.title),`,
|
|
` home: (i) => /^(Good |Still)/.test(i.title) && /^Home\\*\\|Search\\|Library\\|Community(, \\d+ unread)?\\|You(, \\d+ transfers?)?$/.test(i.navTabs) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
|
|
rep(` library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline),`,
|
|
` library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
|
|
rep(` album: (i) => i.screen.includes("tr-push") && !i.mini,`,
|
|
` album: (i) => i.screen.includes("tr-push") && !i.mini && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
|
|
rep(` "track-sheet": (i) => i.half.includes("up"),
|
|
sheet: (i) => i.sheet.includes("up"),`,
|
|
` "track-sheet": (i) => i.half.includes("up") && /^dialog\\/true\\/.+\\/focus$/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
|
|
sheet: (i) => i.sheet.includes("up") && /^dialog\\/true\\/Now playing/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
|
|
rep(` viewer: (i) => i.viewer,
|
|
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
|
"album-playing": (i) => i.mini,`,
|
|
` viewer: (i) => i.viewer && /^button\\/-\\/Close artwork viewer/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost,
|
|
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
|
"album-playing": (i) => i.mini,`);
|
|
rep(` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,`,
|
|
` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10 && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
|
|
rep(` "cat-album-save": (i) => /^Saving \\d+ from /.test(i.save) && /^\\d+$/.test(i.navBadges),`,
|
|
` "cat-album-save": (i) => /^Saving \\d+ from /.test(i.save) && /^\\d+$/.test(i.navBadges) && /\\|You, \\d+ transfers?$/.test(i.navTabs),`);
|
|
rep(` you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit",`,
|
|
` you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit" && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
|
|
rep(` "you-norm-off": (i) => i.norm === "off",`,
|
|
` "you-norm-off": (i) => i.norm === "off" && i.a11yState === "",`);
|
|
rep(` taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4,`,
|
|
` taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4 && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
|
|
rep(` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played),`,
|
|
` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
|
|
/* the six fields this round names are a room's message box and the two
|
|
shapes of prompt; without these three the count is held at zero on ten
|
|
screens that never had one of them. The roster button's whole name was
|
|
the digits on it, so the room takes the junk count too. */
|
|
rep(` room: (i) => i.screen.includes("tr-push") && i.msgs >= 2,`,
|
|
` room: (i) => i.screen.includes("tr-push") && i.msgs >= 2 && !i.a11yAnon && !i.a11yBad,`);
|
|
rep(` "playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \\d+$/.test(i.promptVal),`,
|
|
` "playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \\d+$/.test(i.promptVal) && !i.a11yAnon,`);
|
|
rep(` "import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "",`,
|
|
` "import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "" && !i.a11yAnon,`);
|
|
rep(` "playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice),`,
|
|
` "playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice) && /Removed/.test(i.announce) && /Undo/.test(i.announce),`);
|
|
// the phone walk: the markup is the same, so carry three over conservatively
|
|
rep(` album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img"),`,
|
|
` album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img") && !i.a11yAnon && !i.a11yGhost,`);
|
|
rep(` "home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
|
|
sheet: (i) => i.sheet.includes("up"),`,
|
|
` "home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
|
|
sheet: (i) => i.sheet.includes("up") && /^dialog\\/true\\/Now playing/.test(i.dlg) && !i.a11yGhost,`);
|
|
rep(` you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax,`,
|
|
` you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax && !i.a11yAnon && !i.a11yGhost && !i.a11yState,`);
|
|
save();
|
|
|
|
// ── the doc ─────────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(`## Performance`, ins('ins-r28-doc.txt') + NL + NL + `## Performance`);
|
|
save();
|
|
console.log('round 28 wired');
|