返回 CodeWhale
learning.test.ts
根目录 / web / lib / ratatui / learning.test.ts
1 import { existsSync } from "node:fs";
2 import { join } from "node:path";
3 import { describe, expect, it } from "vitest";
4 import { readCatalogue } from "./catalogue";
5 import { FAMILY_GUIDANCE, TASKS, discoveryText, displayApi, filterByTask, getGuidance, normalizeSearch } from "./learning";
6
7 const catalogue = readCatalogue();
8 const names = new Map(catalogue.entries.map((entry) => [entry.name, entry]));
9 const matching = (query: string) => {
10 const terms = normalizeSearch(query).trim().split(/\s+/).filter(Boolean);
11 return catalogue.entries.filter((entry) => terms.every((term) => discoveryText(entry).includes(term)))
12 .map((entry) => entry.name);
13 };
14
15 describe("Ratatui learning routes", () => {
16 it("gives every generated collection exactly one intent and an existing example", () => {
17 const families = TASKS.flatMap((task) => task.families);
18 expect(new Set(families).size).toBe(families.length);
19 expect([...families].sort()).toEqual(catalogue.families.map((family) => family.id).sort());
20 expect(Object.keys(FAMILY_GUIDANCE).sort()).toEqual([...families].sort());
21 for (const guidance of Object.values(FAMILY_GUIDANCE)) {
22 expect(guidance.purpose.en.length).toBeGreaterThan(0);
23 expect(guidance.purpose.zh.length).toBeGreaterThan(0);
24 expect(guidance.responsibility.en.length).toBeGreaterThan(0);
25 expect(guidance.responsibility.zh.length).toBeGreaterThan(0);
26 expect(existsSync(join(process.cwd(), "..", "vendor", "codewhale-ratatui", guidance.example)), guidance.example).toBe(true);
27 }
28 });
29
30 it("recommends available entries belonging to each task and keeps gallery order", () => {
31 for (const task of TASKS) {
32 const filtered = filterByTask(catalogue.entries, task.id);
33 expect(filtered).toEqual(catalogue.entries.filter((entry) => task.families.includes(entry.family)));
34 for (const name of task.recommended) {
35 expect(names.has(name), name).toBe(true);
36 expect(task.families).toContain(names.get(name)!.family);
37 }
38 }
39 expect(filterByTask(catalogue.entries, "all")).toEqual(catalogue.entries);
40 expect(filterByTask(catalogue.entries, "unknown")).toEqual(catalogue.entries);
41 });
42
43 it("finds themes, ombré and loading by common words without matching unrelated widgets", () => {
44 expect(matching("themes")).toContain("tui-theme-underwater");
45 expect(matching("ombré")).toEqual(matching("ombre"));
46 expect(matching("ombré")).toContain("ocean-column");
47 expect(matching("loading")).toContain("spinner");
48 expect(matching("loading")).not.toContain("toasts");
49 expect(matching("progress")).toContain("workflow-progress-live");
50 expect(matching("progress")).not.toContain("receipt-table");
51 expect(matching("progress")).not.toContain("artifact");
52 });
53
54 it("distinguishes fish, jellyfish, fields and search while retaining public API lookup", () => {
55 expect(matching("jelly")).toContain("jellyfish");
56 expect(matching("jelly")).not.toContain("fish-school");
57 expect(matching("fish")).toContain("fish-school");
58 expect(matching("forms")).toContain("form");
59 expect(matching("forms")).not.toContain("text-input-typed");
60 expect(matching("search")).toContain("picker-query");
61 expect(matching("search")).not.toContain("whale-rest");
62 expect(matching("NativeComposer")).toContain("native-composer");
63 expect(matching("水母")).toContain("jellyfish");
64 });
65
66 it("offers public recipes only for matching component APIs and uses English locale fallback", () => {
67 const expected = {
68 "native-composer": "composer", "workbar-tasks": "workbar", "ocean-column": "ocean",
69 "whale-busy": "whale", "whale-actions": "animated-whale",
70 };
71 for (const [name, recipe] of Object.entries(expected)) {
72 expect(getGuidance(names.get(name)!, "en").recipeId).toBe(recipe);
73 }
74 for (const name of ["native-composer-rich-selection", "showcase-work", "atmosphere-ocean", "fish-school"]) {
75 expect(getGuidance(names.get(name)!, "en").recipeId).toBeUndefined();
76 }
77 const whale = names.get("whale-busy")!;
78 expect(getGuidance(whale, "fr")).toEqual(getGuidance(whale, "en"));
79 expect(getGuidance(whale, "zh").hostNote).toContain("WhaleState");
80 expect(getGuidance(whale, "zh").description).not.toBe(getGuidance(whale, "en").description);
81 });
82
83 it("shows each studio scene's composed components instead of the shared frame's imports", () => {
84 expect(displayApi(names.get("showcase-work")!)).toContain("TerminalShell");
85 expect(displayApi(names.get("showcase-work")!)).toContain("NativeComposer");
86 expect(displayApi(names.get("showcase-decision")!)).toContain("ApprovalCard");
87 expect(displayApi(names.get("showcase-color")!)).toContain("Ombre");
88 expect(displayApi(names.get("showcase-life")!)).toContain("Whale");
89 expect(displayApi(names.get("composer")!)).toEqual(names.get("composer")!.api);
90 });
91 });
92
92 lines TYPESCRIPT