The Channel manager lists the channels the Takeout history shows watched often but not followed, the most watched first: twenty at a time, the channel's About on hover, its name to its page, Follow, and Not interested with an Undo. A row's count opens the videos it counts, ready to play one or all. Before a history is uploaded it points to the upload; while the import still asks YouTube it says more channels may join.
119 lines
5.5 KiB
TypeScript
119 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readSources } from "../../lib/rawSources.testkit";
|
|
|
|
// docs/DESIGN.md "Text stays selectable": a click surface whose text is a title or a name (a card, a
|
|
// row, a person, a channel, a playlist, a log line) is `<Pressable selectable`, so its words select and
|
|
// the click that ends selecting them does nothing. A Pressable that is NOT selectable is a control whose
|
|
// text is its own label or that has no text at all (a menu row, an option, a tab, a nav item, a
|
|
// picture, a switch). Which one a new surface is cannot be read from the code, so every plain one is
|
|
// pinned here with the reason: a new Pressable, or one that lost its flag, fails this test until
|
|
// someone decides.
|
|
|
|
const files = readSources(__dirname, [
|
|
"../../**/*.tsx",
|
|
"!../../**/*.test.*",
|
|
"!../../design-lab/**",
|
|
]) as Record<string, string>;
|
|
|
|
/** file → [how many plain Pressables, why they are not titles]. */
|
|
const PLAIN: Record<string, [number, string]> = {
|
|
"modules/settings/ApiTokens.tsx": [
|
|
1,
|
|
"a token kind's tile in the new-token dialog (an option: its text is its label)",
|
|
],
|
|
"features/downloads/DownloadTray.tsx": [
|
|
2,
|
|
"the folded pill (its summary is its label); a failure's reason, which opens its details",
|
|
],
|
|
"modules/files/FileCard.tsx": [
|
|
1,
|
|
"a card's picture (named by aria-label; its title is the caption)",
|
|
],
|
|
"modules/files/FilesRail.tsx": [2, "the load-error retry; the New-folder command"],
|
|
"modules/home/Home.tsx": [
|
|
2,
|
|
"the hero's picture and a shelf tile's picture (each named by aria-label; its title sits outside)",
|
|
],
|
|
"components/ui/LanguageSwitcher.tsx": [1, "an option row"],
|
|
"shell/NavSidebar.tsx": [3, "the account trigger and its menu rows"],
|
|
"features/header/PaletteLauncher.tsx": [2, "launcher controls"],
|
|
"modules/playlists/PlaylistsRail.tsx": [1, "the load-error retry"],
|
|
"modules/plex/PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
|
"modules/plex/PlexInfo.tsx": [1, "the rating filter (a number)"],
|
|
"modules/plex/PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
|
"features/plexCards/PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
|
"modules/plex/PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
|
"modules/plex/PlexSearchBox.tsx": [1, "a listbox option"],
|
|
"features/plexCards/plexCards.tsx": [
|
|
1,
|
|
"a card's picture (named by aria-label; its title is the caption)",
|
|
],
|
|
"modules/scheduler/Scheduler.tsx": [1, "the interval editor trigger"],
|
|
"modules/settings/SettingsPanel.tsx": [1, "a colour swatch"],
|
|
"modules/channels/SyncStatus.tsx": [1, "the status pill trigger"],
|
|
"features/channel/TagManager.tsx": [1, "a suggestion in a picker"],
|
|
"components/ui/DropZone.tsx": [1, "the drop zone (its text is its own prompt)"],
|
|
"modules/files/VideoEditor.tsx": [2, "timeline glyphs"],
|
|
"shell/Welcome.tsx": [1, "a screenshot (zoom)"],
|
|
"modules/playlists/playlistColumns.tsx": [1, "the thumbnail"],
|
|
"modules/channels/WatchedVideosDialog.tsx": [1, "the thumbnail (its title sits beside it)"],
|
|
"modules/plex/plexDetailUi.tsx": [2, "a switch row and the poster"],
|
|
"modules/plex/plexViews.tsx": [1, "the rating filter (a number)"],
|
|
"components/ui/ActionMenu.tsx": [1, "a menu row"],
|
|
"components/ui/BackToTop.tsx": [1, "a control"],
|
|
"components/ui/CommandPalette.tsx": [1, "a command row"],
|
|
"components/ui/NavRail.tsx": [
|
|
2,
|
|
"the action rows (About, Sign out) and the group row that opens its panel; a page row is a link",
|
|
],
|
|
"components/ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"],
|
|
"components/ui/SidePanel.tsx": [2, "panel controls"],
|
|
"components/ui/tableHeader.tsx": [1, "a column's sort header"],
|
|
};
|
|
|
|
function scan(): { plain: Record<string, number>; selectable: number } {
|
|
const plain: Record<string, number> = {};
|
|
let selectable = 0;
|
|
for (const [path, code] of Object.entries(files)) {
|
|
// Glob keys are relative to this folder (`./x.tsx` is ui/); the table keys them from src/.
|
|
const rel = path.startsWith("./")
|
|
? `components/ui/${path.slice(2)}`
|
|
: path.replace(/^(\.\.\/)+/, "");
|
|
for (const m of code.matchAll(/<Pressable\b/g)) {
|
|
if (/\sselectable\b(?!=)/.test(openingTag(code, m.index))) selectable++;
|
|
else plain[rel] = (plain[rel] ?? 0) + 1;
|
|
}
|
|
}
|
|
return { plain, selectable };
|
|
}
|
|
|
|
/** The opening tag starting at `from`: up to its `>` outside any `{…}` expression, quoted string or
|
|
* line comment (an arrow's `=>` sits inside braces, a class like `[&>svg]` inside quotes). */
|
|
function openingTag(code: string, from: number): string {
|
|
let depth = 0;
|
|
let quote: string | null = null;
|
|
for (let i = from + 1; i < code.length; i++) {
|
|
const c = code[i];
|
|
if (quote) {
|
|
if (c === quote) quote = null;
|
|
} else if (c === "/" && code[i + 1] === "/") {
|
|
i = code.indexOf("\n", i); // a comment between the props: its apostrophes are not quotes
|
|
if (i < 0) break;
|
|
} else if (c === '"' || c === "'" || c === "`") quote = c;
|
|
else if (c === "{") depth++;
|
|
else if (c === "}") depth--;
|
|
else if (c === ">" && depth === 0) return code.slice(from, i + 1);
|
|
}
|
|
return code.slice(from);
|
|
}
|
|
|
|
describe("every Pressable decides whether its text is a title", () => {
|
|
it("finds the selectable ones, and no plain one outside the pinned list", () => {
|
|
const { plain, selectable } = scan();
|
|
// A scan that stopped matching would be green over nothing.
|
|
expect(selectable).toBeGreaterThan(15);
|
|
const pinned = Object.fromEntries(Object.entries(PLAIN).map(([f, [n]]) => [f, n]));
|
|
expect(plain).toEqual(pinned);
|
|
});
|
|
});
|