| 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 |