From f2cde4ec7c4869010d1146e2a9127c37cd4bece6 Mon Sep 17 00:00:00 2001
From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com>
Date: Mon, 14 Sep 2026 03:41:16 -0500
Subject: [PATCH] android: the whole discography, on a page of its own
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The artist page already filed every release the desktop's way, but it only
ever showed one category at a time and it showed all of it: no slice anywhere
in that grid, so an artist with 140 releases drew 140 cards. There is a page
now — All in front of the categories, a sort, a grouping by year, and the
editions opened in place under their own record, which is the part a flat
list loses — and the grid behind it stops at twelve with the way down under
it. It is the same classifier twice over the same cached albums, which the
desktop cannot do because its filing waits on a background description
harvest; the three things its own page has and this one does not — a
tracklist per record, an accent sampled per cover, the scroll-led banner —
are 140 calls and 140 decodes over mobile data for rows that are one tap
away, and ANDROID.md says so rather than leaving them looking forgotten.
The list caps at sixty with the rest a button away, before the grouping, so
the last year on screen can be a part of itself; under a name order By year
is not offered, because the years would repeat out of sequence. The mock
gains a 140-record artist so both caps are walked rather than argued about.
---
ANDROID.md | 43 +++++-
src/lib/mockLibrary.js | 28 +++-
src/mobile/HalfSheet.svelte | 25 ++++
src/mobile/Mobile.svelte | 13 ++
src/mobile/disco.js | 58 ++++++++
src/mobile/screens/Artist.svelte | 16 ++-
src/mobile/screens/Discography.svelte | 196 ++++++++++++++++++++++++++
src/mobile/shell.css | 48 +++++++
tests/mobile/smoke.mjs | 76 ++++++++++
9 files changed, 491 insertions(+), 12 deletions(-)
create mode 100644 src/mobile/screens/Discography.svelte
diff --git a/ANDROID.md b/ANDROID.md
index ef29dd5..b226f4e 100644
--- a/ANDROID.md
+++ b/ANDROID.md
@@ -24,7 +24,7 @@ implements is `research/mobile-mockup/` (gitignored; see its README).
| `crates/tauri-plugin-media-session/` | a Tauri plugin with a Kotlin foreground service (`MediaService`) that owns the `MediaSession`, the MediaStyle notification, audio focus and the becoming-noisy pause. The audio never passes through it; it is what keeps the process alive in the background and what the lock screen shows |
| `crates/tauri-plugin-transfer-notice/` | a second Tauri plugin with a second Kotlin foreground service (`TransferService`, `dataSync`) — the downloads' own progress notification on its own low-importance channel, with Stop all and Open on it, and the reason a Save outlives the app going to the background with nothing playing |
| `src/mobile/Sheet.svelte`, `HalfSheet.svelte`, `SyncRoom.svelte`, `Lane.svelte`, `Cover.svelte`, `Release.svelte` | the player (art / lyrics / queue faces, container transform from the mini bar, drags), the bottom sheets (track, record, sort, the page overflow, the sleep timer), the tap-to-sync room, the waveform lane, the two card pieces (a long press on a card is its sheet) |
-| `src/mobile/screens/` | Home, Search (the library half: records and artists here, tracks through `library_track_search`), Library (albums / artists / tracks / playlists), Album, Artist (the library's shell at once, the catalog dissolving in: portrait, Popular, discography with what is here marked), CatalogAlbum (a catalog record, rows the library has play), Playlist, Community (sign in; rooms, chat, buddies, taste — the Soulseek side; the private layer waits), Room, Thread, You (Appearance switches, real) |
+| `src/mobile/screens/` | Home, Search (the library half: records and artists here, tracks through `library_track_search`), Library (albums / artists / tracks / playlists), Album, Artist (the library's shell at once, the catalog dissolving in: portrait, Popular, discography with what is here marked), CatalogAlbum (a catalog record, rows the library has play), Discography (every release, on one page), Playlist, Community (sign in; rooms, chat, buddies, taste — the Soulseek side; the private layer waits), Room, Thread, You (Appearance switches, real) |
| `src/mobile/prefs.svelte.js` | Appearance, kept in localStorage: ambience, accent from artwork, motion |
| `src/mobile/util.js` | what every screen shares and no screen should write twice: the formatters (`mmss` / `longDur` / `stamp` for a length in seconds, in words and in milliseconds; `ago` / `clock`; `fmtBytes` / `fmtCount`; `fmtOf` / `albumFmt`), the name and folder readers a peer's file needs, the `drag` action, and `devBuild` |
| `src-tauri/tauri.android.conf.json` | merged over `tauri.conf.json` on Android: one window at `mobile.html`, no desktop resources, `minSdkVersion` 26 |
@@ -163,6 +163,34 @@ shows on the next visit at once instead of behind a whole catalog page — and
both lanes are forgotten with the page when a hero is set or removed, or the
old banner would answer while the new one was still being fetched.
+The **whole discography** is a page of its own
+(`src/mobile/screens/Discography.svelte`), reached from the artist page's
+Discography head with **All**, and from under its grid with "Show all N
+Albums" — which is also where that grid now stops: it shows twelve of the
+active tab, because before this an artist with 140 releases rendered 140
+cards. The page is the desktop's `CatalogDiscography.svelte` reduced to the
+four things it has that the artist page's tabs do not: **All** in front of
+the categories (built by CONCATENATING the per-category families, never by
+re-folding the whole set — a single sharing an album's base title would fold
+into it as a phantom edition), a **sort** (newest, oldest, A–Z, Z–A), a
+**grouping** by year, and the **editions kept whole**, opened in place under
+their own record. Three of the desktop's are deliberately left behind: the
+per-record tracklists (140 records is 140 `catalog_album` calls over mobile
+data, for rows one tap away on the record page), the per-record accent
+sampled from each cover, and the scroll-led banner. The year is written into
+the list where the desktop has a sticky year rail — a phone list is read top
+to bottom. The order and the grouping live on the nav entry and are edited
+from the **Sort & group** sheet, Library's own idiom; under a name order By
+year is not offered, because the years would repeat out of sequence, and the
+sheet says so. The list is capped at sixty rows with a "+ N more releases"
+button (Browse's rule), and the cap is taken BEFORE the grouping, so the last
+year on screen can be a part of itself. Filing, families and labels all come
+from `src/mobile/disco.js` — the artist page's own classifier, called a
+second time over the same cached albums. The desktop cannot do that: its
+filing depends on a background description harvest and deferred MB type
+hints a second page would not read, which is why `CatalogDiscography.svelte`
+takes the artist page's computed tab list as the same array object instead.
+
The **catalog record page** is the desktop's album page: the kind, the
artist byline with a portrait, year · tracks · runtime, Last.fm's totals,
Play / Save / Shuffle, the record's other editions (the artist's page folded
@@ -1183,7 +1211,11 @@ waveform growing; backend events go over the mock's own bus, which
`src/lib/events.js` puts in front of Tauri's `listen`), drives Home → a long
press → Search → Library and its tabs → a record → its sheets → the player's
three faces → the back gesture → an artist page → Community → a room → a
-catalog search → a song through the resolver → the lane rising in the sheet →
+catalog search → that artist's whole discography on a page of its own (All
+in front of six categories, a year header per year, the editions opened in
+place, a name order dropping the grouping, and a 140-record artist whose
+grid caps at twelve and whose page shows sixty with the rest a button away)
+→ a song through the resolver → the lane rising in the sheet →
a catalog record, its Save and the Transfers burst (and the shade's notice:
posted with a count while rows are in flight, gone when they are not) →
Gaps → the playlist
@@ -1305,10 +1337,9 @@ symptom.
- **Off by design — external runtimes.** yt-dlp and deno, the vocal
separator, the lyric alignment models, and the listener door that needs
them.
-- **Still desktop-only, smaller.** Catalog track previews, the full
- discography page, and the fused-artist portrait verdict — that last one
- needs the identity merge the phone does not run, so porting it means
- porting the merge first.
+- **Still desktop-only, smaller.** Catalog track previews and the
+ fused-artist portrait verdict — that last one needs the identity merge the
+ phone does not run, so porting it means porting the merge first.
- **Per library folder.** Follows and the play history live in the library
folder, so the phone's own folder starts empty of both; they arrive with a
shared library.
diff --git a/src/lib/mockLibrary.js b/src/lib/mockLibrary.js
index 0030c29..1fc0ba7 100644
--- a/src/lib/mockLibrary.js
+++ b/src/lib/mockLibrary.js
@@ -635,13 +635,33 @@ const SLOW_ALBUMS = [
{ id: 4155, title: "Long Winter", release_date: "2023-01-20", record_type: "album", track_count: 10 },
{ id: 4156, title: "Open Water", release_date: "2024-08-30", record_type: "album", track_count: 11 },
].map((a) => ({ ...a, artist: CAT_SLOW.name, cover: svgArt((a.id * 17) % 360, a.title[0]), edition_group: null, is_soundtrack: false }));
+/* A prolific artist, so the discography page's cap is WALKED rather than
+ argued about: 140 records, four a year from 1985, each plainly titled for
+ the same reason SLOW_ALBUMS' are — the classifier must leave every one of
+ them on the Albums tab, or the counts the walk asserts stop meaning
+ anything. Deliberately NOT in RELATED: a fifth card in the rail would move
+ the artist page's own card census (cat-artist-tab counts 2 images and 2
+ placeholders there). The walk reaches it with a seed, so no search
+ answers for it either. */
+const CAT_BIG = { id: 416, name: "Ember Parade", picture: null, album_count: 140, fans: 91000 };
+const BIG_ALBUMS = Array.from({ length: 140 }, (_, i) => ({
+ id: 41600 + i,
+ title: "Field Notes " + (i + 1),
+ artist: CAT_BIG.name,
+ cover: svgArt((i * 37) % 360, "F"),
+ release_date: (1985 + Math.floor(i / 4)) + "-" + String((i % 4) * 3 + 2).padStart(2, "0") + "-15",
+ record_type: "album",
+ track_count: 8 + (i % 5),
+ edition_group: null,
+ is_soundtrack: false,
+}));
/* the heroes chosen this session — pick_image → set_artist_hero */
const heroes = {};
let follows = [];
function mockCatalog(cmd, args) {
if (cmd === "catalog_artist") {
const id = Number(args?.artistId);
- const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
+ const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id);
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));
const own = a === CAT_ARTIST;
/* the exhaustive page, kept four seconds on purpose: whatever is on
@@ -650,7 +670,7 @@ function mockCatalog(cmd, args) {
return new Promise((r) => setTimeout(() => r({
artist: a, banner: heroes[a.id] ?? null, banners: heroes[a.id] ? [heroes[a.id]] : [], banner_custom: !!heroes[a.id], logos: [],
popular_tracks: own ? CAT_TRACKS.slice(0, 7).map((t, i) => ({ ...t, listeners: 2_400_000 - i * 310_000 })) : [],
- albums: own ? CAT_DISCO : slow ? SLOW_ALBUMS : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
+ albums: own ? CAT_DISCO : slow ? SLOW_ALBUMS : a === CAT_BIG ? BIG_ALBUMS : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
top_genre: "Electronic", socials: [],
}), slow ? 4000 : 220));
}
@@ -746,9 +766,9 @@ function mockCatalog(cmd, args) {
so `artist.id` must equal the argument or the wrapper throws */
if (cmd === "catalog_artist_overview") {
const id = Number(args?.artistId);
- const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
+ const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id);
if (!a) return Promise.reject(new Error("catalog_artist_overview: unknown artist"));
- const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : [];
+ const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : id === CAT_BIG.id ? BIG_ALBUMS.slice(0, 4) : [];
return new Promise((r) => setTimeout(() => r({ artist: a, popular_tracks: [], albums }), 300));
}
if (cmd === "catalog_art") return Promise.resolve(svgArt(200, "D"));
diff --git a/src/mobile/HalfSheet.svelte b/src/mobile/HalfSheet.svelte
index 6611b33..a5f686e 100644
--- a/src/mobile/HalfSheet.svelte
+++ b/src/mobile/HalfSheet.svelte
@@ -62,6 +62,7 @@
if (!h) return null;
if (h.kind !== "more") return h.kind;
const t = top();
+ if (t.n === "disco") return "discosort";
if (t.n === "album") return "album-more";
if (t.n === "library") return "library-more";
if (t.n === "playlist") return "playlist-more";
@@ -722,6 +723,30 @@
{@render row("refresh", "Forget and look again", "", act(() => { void forgetLyrics(); }))}
{/if}
+ {:else if kind === "discosort"}
+ {@const c = top().c}
+ {@const ord = c.order || "new"}
+ {@const byName = ord === "az" || ord === "za"}
+ {@const grp = byName ? "none" : (c.group ?? "year")}
+
+
By year needs a date order — under A–Z the years would repeat out of sequence.
{/if}
+
{:else if kind === "librarysort"}
Sort & view
Order
diff --git a/src/mobile/Mobile.svelte b/src/mobile/Mobile.svelte
index feddbd7..9a0a5bf 100644
--- a/src/mobile/Mobile.svelte
+++ b/src/mobile/Mobile.svelte
@@ -22,6 +22,7 @@
import Library from "./screens/Library.svelte";
import Album from "./screens/Album.svelte";
import Artist from "./screens/Artist.svelte";
+ import Discography from "./screens/Discography.svelte";
import CatalogAlbum from "./screens/CatalogAlbum.svelte";
import Playlist from "./screens/Playlist.svelte";
import Import from "./screens/Import.svelte";
@@ -86,6 +87,16 @@
const a = albumById(e.c.id);
return { title: a?.title ?? "", kicker: a?.artist ?? "", amb: a?.art_path ?? null, flush: true };
}
+ /* the whole discography wears the artist's own ground, but no
+ hero of its own: it is a list, and the bar is over it from the
+ first pixel rather than after 210 of them */
+ case "disco": {
+ const as = albumsBy(e.c.name);
+ const cat = catalog.artists[artistKey(e.c.name)]?.page;
+ const p = cat?.banner || cat?.artist?.picture
+ || as.find((a) => a.artist_art_path)?.artist_art_path || as[0]?.art_path || null;
+ return { title: e.c.name, kicker: "Discography", amb: p };
+ }
case "artist": {
const as = albumsBy(e.c.name);
const cat = catalog.artists[artistKey(e.c.name)]?.page;
@@ -339,6 +350,8 @@
{:else if entry.n === "artist"}
+ {:else if entry.n === "disco"}
+
{:else if entry.n === "catalogAlbum"}
{:else if entry.n === "playlist"}
diff --git a/src/mobile/disco.js b/src/mobile/disco.js
index 0c39257..e43ec3f 100644
--- a/src/mobile/disco.js
+++ b/src/mobile/disco.js
@@ -92,3 +92,61 @@ export const SOCIAL_LABEL = {
youtube: "YouTube", instagram: "Instagram", twitter: "X", x: "X", facebook: "Facebook", tiktok: "TikTok",
spotify: "Spotify", apple: "Apple Music", discogs: "Discogs", lastfm: "Last.fm", musicbrainz: "MusicBrainz",
};
+
+/* ── the whole discography, on a page of its own ─────────────────────────
+ The artist page's tabs are one lane per category; the discography page is
+ the same filing seen whole. Both read ONE classifier, and it is this one.
+
+ The desktop cannot do that: CatalogDiscography.svelte takes the artist
+ page's already-computed tab list as the same array object, because the
+ desktop's filing depends on a background description harvest and on
+ deferred MusicBrainz type hints that a re-run would not read
+ (CatalogDiscography.svelte:28-46). `discoTabs` here is pure and reads only
+ what the catalog page carries, so the phone simply calls it twice over the
+ same cached albums and the two pages can never disagree. */
+
+/** "All" in front of the categories — built by CONCATENATING the per-category
+ * families, never by re-folding the whole set. A single that shares an
+ * album's base title (Pond released "Terrestrials" in 2026 as an album AND
+ * as a single) would fold into it as a phantom edition, and one of the two
+ * would vanish from its own tab. */
+export function allTabs(tabs) {
+ const ts = tabs || [];
+ if (!ts.length) return [];
+ return [{ key: "all", label: "All", items: ts.flatMap((t) => t.items) }, ...ts];
+}
+
+/** The four orders the page offers, and what each is called. The sort sheet
+ * in HalfSheet.svelte writes the same four out inline — it is edited by
+ * every other round and a new import there is a collision waiting to
+ * happen — so if a label changes it changes in both places. */
+export const ORDER_LABEL = { new: "Newest first", old: "Oldest first", az: "A–Z", za: "Z–A" };
+
+/** The desktop's sort (CatalogDiscography.svelte:71-78): dates on
+ * `release_date`, names on the title, either direction. */
+export function orderFamilies(items, order) {
+ const list = [...(items || [])];
+ const byName = order === "az" || order === "za";
+ list.sort((a, b) => (byName
+ ? (a.title ?? "").localeCompare(b.title ?? "")
+ : (a.release_date ?? "").localeCompare(b.release_date ?? "")));
+ return order === "new" || order === "za" ? list.reverse() : list;
+}
+
+/** [{ key: "2001", items }] — the year headers, cut out of the list in the
+ * order it is already in rather than re-sorted, which is why a name order
+ * must not be grouped: the years would repeat out of sequence. `on` false
+ * gives the whole list back under one null key, so the caller renders one
+ * loop either way. */
+export function byYear(items, on) {
+ const list = items || [];
+ if (!on) return list.length ? [{ key: null, items: list }] : [];
+ const out = [];
+ for (const r of list) {
+ const y = String(r.release_date || "").slice(0, 4) || "—";
+ const last = out[out.length - 1];
+ if (last && last.key === y) last.items.push(r);
+ else out.push({ key: y, items: [r] });
+ }
+ return out;
+}
diff --git a/src/mobile/screens/Artist.svelte b/src/mobile/screens/Artist.svelte
index 19d6ece..da140f2 100644
--- a/src/mobile/screens/Artist.svelte
+++ b/src/mobile/screens/Artist.svelte
@@ -128,7 +128,13 @@
else if (!ts.some((t) => t.key === tab)) tab = albumsFirst();
});
});
- const families = $derived(tabs.find((t) => t.key === tab)?.items ?? []);
+ /* the grid is a SLICE — the whole tab lives on the discography page,
+ which the head's "All" and the link under the grid both open. Before
+ this an artist with 140 releases rendered 140 cards here. */
+ const GRID_CAP = 12;
+ const activeTab = $derived(tabs.find((t) => t.key === tab) ?? null);
+ const families = $derived((activeTab?.items ?? []).slice(0, GRID_CAP));
+ const tabCount = $derived(activeTab?.items.length ?? 0);
const inLib = (a) => albums.find((x) => looseTitle(x.title) === looseTitle(a.title)) || null;
const familyLib = (f) => (f.editions || [f]).map(inLib).find(Boolean) || null;
const familyKeyOf = (f) => f.familyKey ?? f.key ?? f.id;
@@ -284,7 +290,10 @@
{/if}
{#if tabs.length}
-
Discography · {releasedCount} on the catalog
+
+
Discography · {releasedCount} on the catalog
+
+
{#if tabs.length > 1}
{#each tabs as t (t.key)}
@@ -306,6 +315,9 @@
/>
{/each}
Nothing to list.The catalog has no releases for {shownName}{cat.status === "error" ? " — it did not answer" : ""}.
+{/if}
+
+
diff --git a/src/mobile/shell.css b/src/mobile/shell.css
index bc37189..b7adf7e 100644
--- a/src/mobile/shell.css
+++ b/src/mobile/shell.css
@@ -1613,3 +1613,51 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="
}
/* why the rows will not move while the filter has something in it */
.pl-note { padding: 2px var(--pad) 8px; font-size: 12px; color: var(--text-muted); }
+
+/* ── the whole discography ───────────────────────────────────────────────
+ The artist page's "All". Nearly all of it is the app's own parts — .sec,
+ .segtabs, .row, .eds/.ed, .more-link, .btn — so what is here is the head,
+ the year written into the list where the desktop has a sticky rail, the
+ editions line under a record, and the editions themselves widened out of
+ the record page's 210px rail into the width this list already has. */
+.dg-head { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad) 6px; }
+.dg-head .b { flex: 1; min-width: 0; }
+.dg-head .t { font-size: 18px; font-weight: 800; letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.dg-head .m { font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }
+.dg-por {
+ width: 44px; height: 44px;
+ border-radius: 50%;
+ overflow: hidden;
+ flex: none;
+ background: var(--surface);
+ box-shadow: var(--ring);
+ display: grid;
+ place-items: center;
+ color: var(--text-muted);
+}
+.dg-por img { width: 100%; height: 100%; object-fit: cover; }
+.dg-sec { padding-top: 12px; }
+/* the year. A phone list is read top to bottom, so the span the desktop's
+ rail jumps to is simply written into it. */
+.dg-group {
+ padding: 16px var(--pad) 5px;
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: 0.4px;
+ color: var(--text-secondary);
+ font-variant-numeric: tabular-nums;
+}
+.dg-group em { font-style: normal; font-weight: 650; letter-spacing: 0; color: var(--text-muted); }
+/* both of these line up under the row's TITLE, not under its cover: the
+ pad, the 46px cover and the row's 12px gap */
+.dg-eline {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 var(--pad) 9px calc(var(--pad) + 58px);
+ font-size: 12px;
+ font-weight: 750;
+ color: var(--accent-hover);
+}
+.dg-eds { display: flex; flex-direction: column; gap: 6px; padding: 0 var(--pad) 12px calc(var(--pad) + 58px); }
+.dg-eds .ed { width: 100%; }
diff --git a/tests/mobile/smoke.mjs b/tests/mobile/smoke.mjs
index bd57675..3c5f9cf 100644
--- a/tests/mobile/smoke.mjs
+++ b/tests/mobile/smoke.mjs
@@ -355,6 +355,19 @@ const PROBE = `(function () {
syEst: (document.querySelector('.sy-est') || {}).textContent || '',
syHint: (document.querySelector('.sy-hint') || {}).textContent || '',
disco: document.querySelectorAll('.grid2 .release').length,
+ /* the whole-discography page: its rows, its tabs (All in front), the
+ year headers, an opened edition rail, the cap's own button, the
+ first row's title, the order the head is showing and the summary
+ line — plus, back on the artist page, the link down to it */
+ dgRows: document.querySelectorAll('.dg-row').length,
+ dgTabs: document.querySelectorAll('.dg-tabs button').length,
+ dgGroups: document.querySelectorAll('.dg-group').length,
+ dgEds: document.querySelectorAll('.dg-eds .ed').length,
+ dgMore: (document.querySelector('.dg-more') || {}).textContent || '',
+ dgFirst: (document.querySelector('.dg-row .t') || {}).textContent || '',
+ dgOrder: (document.querySelector('.dg-sec .all') || {}).textContent || '',
+ dgSum: (document.querySelector('.dg-head .m') || {}).textContent || '',
+ dgAll: (document.querySelector('.more-link.dg-all') || {}).textContent || '',
/* the artist page's three lanes at once — the page, the hero seed and
the overview — so a step can say what painted and what had not
landed yet: ":[+art]::" */
@@ -587,6 +600,27 @@ const MOCK_STEPS = [
{ do: `__click('.top button', 'Open')`, name: "cat-artist", wait: 2200 },
{ do: `__click('.stats .fb', 'Follow')`, name: "cat-artist-follow", wait: 900 },
{ do: `__click('.disco-tabs button', 'Singles')`, name: "cat-artist-tab", wait: 700 },
+ /* the whole discography, the page the artist page's "All" opens: the
+ eleven mock rows folded to ten families across six categories, the
+ year headers, a category tab, the one linked family opened in place,
+ and the sort sheet taking it to a name order — which is where the
+ grouping has to fall away */
+ { do: `__click('.sec .all', 'All')`, name: "disco-all", wait: 1400 },
+ { do: `__click('.dg-tabs button', 'Albums')`, name: "disco-albums", wait: 900 },
+ { do: `__click('.dg-tabs button', 'All')`, wait: 800 },
+ { do: `__click('.dg-eline', null)`, name: "disco-editions", wait: 800 },
+ { do: `__click('.dg-sec .all', null)`, name: "disco-sheet", wait: 900 },
+ { do: `__click('.halfsheet .chip', 'A–Z')`, name: "disco-az", wait: 900 },
+ { do: `document.querySelector('.scrim').click()`, wait: 700 },
+ { do: `history.back()`, wait: 800 },
+ /* and the cap, on an artist with 140 records: the grid on the artist
+ page is a slice with the way down under it, the page itself opens on
+ the tab that link was showing, and sixty rows extend to all of them */
+ { do: `__ss.push('artist', { name: 'Ember Parade', seed: { id: 416, name: 'Ember Parade', picture: null, album_count: 140, fans: 91000 } })`, name: "disco-many-artist", wait: 1800 },
+ { do: `__click('.more-link.dg-all', 'Show all')`, name: "disco-many", wait: 1500 },
+ { do: `__click('.dg-more', null)`, name: "disco-many-all", wait: 1400 },
+ { do: `history.back()`, wait: 800 },
+ { do: `history.back()`, wait: 900 },
/* the artist's photo: the page's ⋯ → a picture from this phone becomes
the hero, the page is asked again, then the catalog's own come back */
{ do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "artist-more", wait: 700 },
@@ -1171,6 +1205,26 @@ const MOCK_EXPECT = {
"cat-artist": (i) => i.screen.includes("has-hero") && i.title.startsWith("Daft Punk") && i.tabs >= 4 && i.pop >= 5 && i.socs === 2,
"cat-artist-follow": (i) => i.fb.includes("Following"),
"cat-artist-tab": (i) => i.title.startsWith("Daft Punk") && i.cardImgs === 2 && i.cardPhs === 2,
+ /* the whole discography: All in front of the six categories the eleven
+ mock rows file into, the ten families they fold to, a year header per
+ year, the desktop's own summary line and the order the head shows.
+ Two of those rows are dated relative to today (dayIso(-9), dayIso(21)),
+ so a run across a New Year splits their year in two — hence the range
+ rather than a number. */
+ "disco-all": (i) => i.dgTabs === 7 && i.dgRows === 10 && i.dgGroups >= 8 && i.dgGroups <= 9
+ && /^10 releases/.test(i.dgSum) && /Newest/.test(i.dgOrder)
+ && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
+ "disco-albums": (i) => i.dgRows === 4 && i.dgGroups === 4,
+ /* Discovery and its Deluxe — the one edition_group in the mock, and the
+ thing a flat list of releases loses */
+ "disco-editions": (i) => i.dgEds === 2 && i.dgRows === 10,
+ "disco-sheet": (i) => i.half.includes("up") && /By year/.test(i.halfText),
+ /* the rule this page has to get right: a name order cannot be grouped by
+ year, so the grouping falls away with it */
+ "disco-az": (i) => i.dgFirst === "Alive 1997" && i.dgGroups === 0 && /A–Z/.test(i.dgOrder),
+ "disco-many-artist": (i) => i.title.startsWith("Ember Parade") && i.disco === 12 && /Show all 140/.test(i.dgAll),
+ "disco-many": (i) => i.dgRows === 60 && /\+ 80 more/.test(i.dgMore),
+ "disco-many-all": (i) => i.dgRows === 140 && !i.dgMore,
"cat-artist-related": (i) => i.title.startsWith("Justice") && i.screen.includes("has-hero") && i.heroPix && i.pop >= 4 && i.popCounts >= 4,
"artist-more": (i) => i.half.includes("up") && /Choose hero photo/.test(i.halfText) && !/Remove custom hero/.test(i.halfText),
"artist-hero-set": (i) => !i.half.includes("up") && /3840×2160/.test(i.notice),
@@ -1536,6 +1590,20 @@ const PHONE_STEPS = [
same no-op as the button. Nothing is typed and nothing is picked. */
{ do: `__ss.openSetup('preview')`, name: "setup-preview", wait: 1300 },
{ do: `document.querySelector('.scrim').click()`, name: "setup-closed", wait: 900 },
+ /* the whole discography on the real core, on the same artist the walk
+ already opened above — so the catalog store is warm and the page has
+ its releases the moment it is pushed. It reads and it sorts; nothing
+ is downloaded, played, joined, sent, signed in to or written. */
+ { do: `document.querySelectorAll('.nav button')[2].click()`, wait: 600 },
+ { do: `document.querySelectorAll('.segtabs button')[1].click()`, wait: 600 },
+ { do: `__click('.screen button.row', null)`, wait: 500 },
+ { wait: 2500 },
+ { do: `__click('.sec .all', 'All')`, name: "dg-page", wait: 1600 },
+ { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "dg-sheet", wait: 1000 },
+ { do: `__click('.halfsheet .chip', 'A–Z')`, name: "dg-az", wait: 1000 },
+ { do: `document.querySelector('.scrim').click()`, wait: 700 },
+ { do: `history.back()`, wait: 800 },
+ { do: `history.back()`, wait: 900 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "end", wait: 800 },
];
const PHONE_EXPECT = {
@@ -1661,6 +1729,14 @@ const PHONE_EXPECT = {
"export-door": (i) => i.half.includes("up") && /M3U8/.test(i.halfText) && /Backup \(JSON\)/.test(i.halfText),
"export-picker": (i) => !i.half.includes("up") && !i.toast,
"export-dismissed": (i) => i.title.startsWith("Walk test") && !i.half.includes("up") && !i.toast && !i.notice,
+ /* the real core answers faster than the mock does, and it answers with
+ Kyle's own artists, so none of this asserts a count, a title or a
+ state with a window shorter than the still: only that the page is
+ there, listing something, in the order the sheet was just told to
+ use, and that nothing went wrong saying so */
+ "dg-page": (i) => i.dgTabs >= 1 && i.dgRows >= 1 && !i.toast && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
+ "dg-sheet": (i) => i.half.includes("up") && /By year/.test(i.halfText) && !i.toast,
+ "dg-az": (i) => i.dgRows >= 1 && /A–Z/.test(i.dgOrder) && !i.toast,
end: (i) => /^(Good |Still)/.test(i.title),
};