返回 CodeWhale
catalogue.test.ts
根目录 / web / lib / ratatui / catalogue.test.ts
1 import { readFileSync } from "node:fs";
2 import { join } from "node:path";
3 import { describe, expect, it } from "vitest";
4 import { motionId, previewAssetPath, readCatalogue, searchEntries } from "./catalogue";
5 import { discoveryText } from "./learning";
6
7 const catalogue = readCatalogue();
8 const publicRoot = join(process.cwd(), "public", "ratatui");
9
10 describe("Ratatui explorer", () => {
11 it("finds the base composer by its public API alongside its responsive variants", () => {
12 const found = searchEntries(catalogue.entries, "NativeComposer");
13 expect(found.map((entry) => entry.name)).toContain("native-composer");
14 expect(found.map((entry) => entry.name)).toContain("native-composer-narrow");
15 expect(searchEntries(catalogue.entries, "native composer narrow").map((entry) => entry.name))
16 .toContain("native-composer-narrow");
17 });
18
19 it("combines search and collection selection and leaves an empty search recoverable", () => {
20 expect(searchEntries(catalogue.entries, "fish", "habitat").map((entry) => entry.name)).toContain("fish-school");
21 expect(searchEntries(catalogue.entries, "fish", "input")).toHaveLength(0);
22 expect(searchEntries(catalogue.entries, "" )).toHaveLength(catalogue.entries.length);
23 });
24
25 it("finds studio scenes by their displayed composed APIs instead of private fixture inputs", () => {
26 const studio = catalogue.entries.filter((entry) => entry.family === "studio");
27 const shells = searchEntries(studio, "TerminalShell").map((entry) => entry.name);
28 expect(shells).toHaveLength(3);
29 expect(shells).toEqual(expect.arrayContaining(["showcase-work", "showcase-decision", "showcase-narrow"]));
30 expect(searchEntries(studio, "NativeComposer").map((entry) => entry.name))
31 .toEqual(expect.arrayContaining(["showcase-work", "showcase-narrow"]));
32 expect(searchEntries(studio, "ApprovalCard").map((entry) => entry.name)).toEqual(["showcase-decision"]);
33 expect(searchEntries(studio, "Inputs")).toHaveLength(0);
34 for (const name of ["showcase-work", "showcase-decision", "showcase-narrow"]) {
35 const text = discoveryText(studio.find((entry) => entry.name === name)!);
36 expect(text).toContain("terminalshell");
37 expect(text).not.toContain("inputs");
38 }
39 });
40
41 it("keeps the hub hero entry available with its wide and narrow renders", () => {
42 const hero = catalogue.entries.find((entry) => entry.name === "showcase-work");
43 expect(hero).toBeDefined();
44 const data = JSON.parse(readFileSync(join(publicRoot, hero!.previewPath), "utf8"));
45 expect(data.previews["dark-truecolor"]["native"]).toContain("viewBox=");
46 expect(data.previews["dark-truecolor"]["40"]).toContain('viewBox="0 0 400 ');
47 });
48
49 it("rejects an asset path outside the generated component directory", () => {
50 const entry = catalogue.entries[0];
51 expect(previewAssetPath(entry)).toBe(`/ratatui/entries/${entry.name}.json`);
52 expect(() => previewAssetPath({ ...entry, previewPath: "entries/../../secret.json" })).toThrow();
53 });
54
55 it("provides every advertised profile and actual column width for every component", () => {
56 const names = new Set(catalogue.entries.map((entry) => entry.name));
57 expect(names.size).toBe(catalogue.entries.length);
58 expect(catalogue.profiles).toHaveLength(9);
59 expect(catalogue.sizes.map((size) => size.id)).toEqual(["native", "40", "80", "120"]);
60 let buffers = 0;
61 for (const entry of catalogue.entries) {
62 expect(entry.api.length, entry.name).toBeGreaterThan(0);
63 expect(entry.source.url).toContain(`/blob/${catalogue.source.revision}/src/gallery/`);
64 const data = JSON.parse(readFileSync(join(publicRoot, entry.previewPath), "utf8"));
65 expect(data.name).toBe(entry.name);
66 expect(data.fixture.standalone).toBe(false);
67 expect(data.fixture.code.length).toBeGreaterThan(0);
68 for (const profile of catalogue.profiles) for (const size of catalogue.sizes) {
69 const svg = data.previews[profile.id][size.id];
70 const width = size.id === "native" ? entry.width : Number(size.id);
71 expect(svg).toContain(`viewBox="0 0 ${width * 10} ${entry.height * 20}"`);
72 expect(svg).not.toMatch(/<script|foreignObject|https?:\/\/[^\s"']+\.(?:png|js|woff)/i);
73 buffers += 1;
74 }
75 }
76 expect(buffers).toBe(catalogue.entries.length * 9 * 4);
77 expect(catalogue.families.reduce((sum, family) => sum + family.count, 0)).toBe(catalogue.entries.length);
78 });
79
80 it("loads native motion with its real cadence and preserves the static accessibility profiles", () => {
81 for (const entry of catalogue.entries) {
82 const id = motionId(entry, "dark-truecolor");
83 if (!id) continue;
84 const data = JSON.parse(readFileSync(join(publicRoot, "motion", `${id}.json`), "utf8"));
85 expect(data.frames.length).toBeGreaterThan(1);
86 expect(data.frameMs).toBeGreaterThanOrEqual(20);
87 expect(data.source).toMatch(/^https:\/\/github\.com\/codewhale-hq\/codewhale-ratatui\/blob\/[a-f0-9]{40}\//);
88 }
89 expect(motionId(catalogue.entries.find((entry) => entry.name === "showcase-work")!, "light-truecolor")).toBe("studio-light");
90 expect(catalogue.profiles.map((profile) => profile.id)).toEqual(expect.arrayContaining(["ascii", "no-color", "ansi-16"]));
91 });
92 });
93
93 lines TYPESCRIPT