fix(ui): review round 2 — reveal floor, failed next page, count states, row Play

- DataTable: the reveal floor holds only in the view it was made for (Channels
  never clears its follow, so every later sort mounted rows up to it)
- Feed + Plex browse: a failed next page stops the read-ahead and shows a row
  that retries, instead of re-requesting it once a round-trip
- the YouTube Load more click uses nextPageOnClick; a contract pins NEXT_PAGE
  to the read-aheads and the click sites to nextPageOnClick
- ItemCount: a failed count says so; a stale one says updating to a screen reader
- the narrow-picture Play lift applies only where a hover row is there to clear
- PlayerModal: one description ladder for the popover and the panel, both tested
- plexImage contract reads every img src, any attribute order
- Tabs comment names who persists the sub-tab
This commit is contained in:
2026-10-03 02:25:15 +02:00
parent c4c442bc5b
commit b40440b825
17 changed files with 360 additions and 63 deletions
@@ -29,4 +29,34 @@ describe("an infinite list's read-ahead", () => {
it("the feed hands the grid the query's isFetching", () => {
expect(source("Feed.tsx")).toMatch(/isFetching=\{query\.isFetching\}/);
});
// Retry is off, so a failed page settles at once; a read-ahead that re-runs on the settle would
// request it again once a round-trip while the end is in view (E31 review 2).
it("stands down at a failed next page", () => {
expect(source("Feed.tsx")).toMatch(
/hasNextPage=\{!!hasNextPage && !query\.isFetchNextPageError\}/,
);
expect(source("PlexBrowse.tsx")).toMatch(
/const hasNextPage = browseQ\.hasNextPage && !isFetchNextPageError;/,
);
});
});
// A click joins an in-flight next page but cancels a background refetch (nextPageOnClick): through
// NEXT_PAGE it would join the refetch and load nothing. NEXT_PAGE is only for the read-aheads, so a
// click site that goes back to it fails here (E31 review 2: the YouTube "Load more" was missed).
describe("a click that loads the next page", () => {
const READ_AHEADS = new Set(["ui/VirtualGrid.tsx", "PlexBrowse.tsx"]);
it("uses nextPageOnClick; NEXT_PAGE only in the read-aheads", () => {
const users = Object.entries(files)
.filter(([, src]) => /fetchNextPage\(NEXT_PAGE\)/.test(src as string))
.map(([path]) => path.replace("../components/", ""));
expect(users.sort()).toEqual([...READ_AHEADS].sort());
for (const [file, sites] of [
["AdminLogs.tsx", 2],
["Feed.tsx", 2],
["PlexBrowse.tsx", 1],
] as const)
expect(source(file).match(/fetchNextPage\(nextPageOnClick\(/g)?.length, file).toBe(sites);
});
});
+8 -3
View File
@@ -47,8 +47,10 @@ it("names each backdrop cut by the width the proxy cuts it at", () => {
// Every small Plex picture asks for its card-size cut through cardImage (E31 S4e, swept in S5: the
// playlist rows, the search box and the collection picker still fetched the 400px poster for a 32px
// picture). A bare `src={…thumb}` is allowed only where the full cut is the point or the URL is not
// the proxy's; the SET is pinned, so a new bare one fails here.
// picture). A plain `src={…}` is allowed only where the full cut is the point or the URL is not the
// proxy's; the SET is pinned, so a new one fails here. Every <img>'s src counts, wherever it sits
// among the attributes and whatever its variable is called: the first cut of this test read only a
// leading `src={…thumb…}` and missed two of the three sites it was written for (E31 review 2).
const plexFiles = import.meta.glob(
[
"../components/{Plex,plex}*.tsx",
@@ -58,7 +60,10 @@ const plexFiles = import.meta.glob(
{ query: "?raw", import: "default", eager: true },
);
const ALLOWED = new Set([
"plexCards.tsx pic.src", // cardImage's own src (srcSet/sizes beside it): the card cut
"plexCards.tsx c.thumb", // cast photos: the person-image route, not the image proxy
"PlexBrowse.tsx p.photo", // the person filter chip's headshot: the person-image route
"plexViews.tsx d.photo", // the person page's headshot: the same
"PlexInfo.tsx c.thumb", // the same cast photos on the detail page
"PlexInfo.tsx detail.thumb", // the detail page's own poster: the full cut is the point
"plexDetailUi.tsx thumb", // the show/season header poster: the same
@@ -69,7 +74,7 @@ it("leaves no small Plex picture on the full cut", () => {
expect(Object.keys(plexFiles).length).toBeGreaterThan(5);
const bare = new Set<string>();
for (const [path, src] of Object.entries(plexFiles))
for (const m of (src as string).matchAll(/<img\s+src=\{([^}]*thumb[^}]*)\}/g))
for (const m of (src as string).matchAll(/<img\b[^>]*?\bsrc=\{([^}]*)\}/g))
bare.add(`${path.split("/").pop()} ${m[1]!.trim()}`);
expect([...bare].filter((b) => !ALLOWED.has(b))).toEqual([]);
});