| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093 |
- /**
- * End-to-end test: JSON spec → Ink terminal output.
- *
- * Exercises the full user-facing API:
- * 1. defineCatalog() with standard Ink component/action definitions
- * 2. Hand-authored Spec (same shape an LLM would generate)
- * 3. JSONUIProvider + Renderer → renderToString() → actual terminal text
- *
- * Every assertion checks real rendered output, not internal structures.
- */
- import React from "react";
- import { describe, it, expect } from "vitest";
- import { renderToString } from "ink";
- import {
- defineCatalog,
- buildUserPrompt,
- createStateStore,
- type Spec,
- } from "@json-render/core";
- import { schema } from "@json-render/ink/schema";
- import {
- standardComponentDefinitions,
- standardActionDefinitions,
- } from "@json-render/ink/catalog";
- import { JSONUIProvider, Renderer } from "@json-render/ink";
- function renderSpec(
- spec: Spec,
- opts: { columns?: number; initialState?: Record<string, unknown> } = {},
- ): string {
- return renderToString(
- <JSONUIProvider initialState={opts.initialState ?? spec.state ?? {}}>
- <Renderer spec={spec} />
- </JSONUIProvider>,
- { columns: opts.columns ?? 100 },
- );
- }
- const catalog = defineCatalog(schema, {
- components: standardComponentDefinitions,
- actions: standardActionDefinitions,
- });
- describe("ink e2e: catalog → prompt", () => {
- it("prompt includes all standard components", () => {
- const prompt = catalog.prompt();
- for (const name of Object.keys(standardComponentDefinitions)) {
- expect(prompt, `Missing component "${name}" in prompt`).toContain(name);
- }
- });
- it("prompt includes all 5 standard + built-in actions", () => {
- const prompt = catalog.prompt();
- for (const name of [
- "setState",
- "pushState",
- "removeState",
- "exit",
- "log",
- ]) {
- expect(prompt, `Missing action "${name}" in prompt`).toContain(name);
- }
- });
- it("buildUserPrompt embeds the user message", () => {
- const msg = buildUserPrompt({
- prompt: "Show me a deployment dashboard",
- });
- expect(msg).toContain("deployment dashboard");
- });
- });
- describe("ink e2e: simple spec → terminal output", () => {
- it("renders a Heading", () => {
- const spec: Spec = {
- root: "h",
- elements: {
- h: {
- type: "Heading",
- props: { text: "Hello Terminal", level: "h1" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Hello Terminal");
- });
- it("renders Text with content", () => {
- const spec: Spec = {
- root: "t",
- elements: {
- t: {
- type: "Text",
- props: { text: "plain text output" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("plain text output");
- });
- it("renders a Badge", () => {
- const spec: Spec = {
- root: "b",
- elements: {
- b: {
- type: "Badge",
- props: { label: "ACTIVE", variant: "success" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("ACTIVE");
- });
- it("renders a KeyValue pair", () => {
- const spec: Spec = {
- root: "kv",
- elements: {
- kv: {
- type: "KeyValue",
- props: { label: "Status", value: "Running" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Status");
- expect(output).toContain("Running");
- });
- it("renders a StatusLine with icon", () => {
- const spec: Spec = {
- root: "sl",
- elements: {
- sl: {
- type: "StatusLine",
- props: { text: "All checks passed", status: "success" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("All checks passed");
- expect(output).toContain("✔");
- });
- it("renders a Divider with title", () => {
- const spec: Spec = {
- root: "d",
- elements: {
- d: {
- type: "Divider",
- props: { title: "Section", color: "gray" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Section");
- expect(output).toContain("─");
- });
- it("renders a List", () => {
- const spec: Spec = {
- root: "l",
- elements: {
- l: {
- type: "List",
- props: { items: ["Install", "Build", "Deploy"], ordered: true },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("1.");
- expect(output).toContain("Install");
- expect(output).toContain("2.");
- expect(output).toContain("Build");
- expect(output).toContain("3.");
- expect(output).toContain("Deploy");
- });
- it("renders a ProgressBar", () => {
- const spec: Spec = {
- root: "pb",
- elements: {
- pb: {
- type: "ProgressBar",
- props: { progress: 0.5, width: 20, label: "Upload" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Upload");
- expect(output).toContain("50%");
- expect(output).toContain("█");
- });
- });
- describe("ink e2e: nested layout → terminal output", () => {
- it("renders Box with children", () => {
- const spec: Spec = {
- root: "box",
- elements: {
- box: {
- type: "Box",
- props: { flexDirection: "column", gap: 1 },
- children: ["line1", "line2"],
- },
- line1: { type: "Text", props: { text: "First line" }, children: [] },
- line2: { type: "Text", props: { text: "Second line" }, children: [] },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("First line");
- expect(output).toContain("Second line");
- });
- it("renders Card with title and nested content", () => {
- const spec: Spec = {
- root: "card",
- elements: {
- card: {
- type: "Card",
- props: { title: "Details", borderStyle: "round" },
- children: ["inner"],
- },
- inner: { type: "Text", props: { text: "Card body" }, children: [] },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Details");
- expect(output).toContain("Card body");
- // Round border chars
- expect(output).toContain("╭");
- expect(output).toContain("╰");
- });
- it("renders Table with headers and data", () => {
- const spec: Spec = {
- root: "tbl",
- elements: {
- tbl: {
- type: "Table",
- props: {
- columns: [
- { header: "Name", key: "name", width: 12 },
- { header: "Role", key: "role", width: 10 },
- ],
- rows: [
- { name: "Alice", role: "Admin" },
- { name: "Bob", role: "User" },
- ],
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Name");
- expect(output).toContain("Role");
- expect(output).toContain("Alice");
- expect(output).toContain("Admin");
- expect(output).toContain("Bob");
- expect(output).toContain("User");
- });
- });
- describe("ink e2e: state binding → terminal output", () => {
- it("$state expression resolves to state value", () => {
- const spec: Spec = {
- state: { greeting: "Hello from state!" },
- root: "t",
- elements: {
- t: {
- type: "Text",
- props: { text: { $state: "/greeting" } },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Hello from state!");
- });
- it("visibility hides elements when condition is false", () => {
- const spec: Spec = {
- state: { show: false },
- root: "box",
- elements: {
- box: {
- type: "Box",
- props: { flexDirection: "column" },
- children: ["always", "maybe"],
- },
- always: {
- type: "Text",
- props: { text: "Always visible" },
- children: [],
- },
- maybe: {
- type: "Text",
- props: { text: "Conditional text" },
- visible: { $state: "/show" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Always visible");
- expect(output).not.toContain("Conditional text");
- });
- it("visibility shows elements when condition is true", () => {
- const spec: Spec = {
- state: { show: true },
- root: "box",
- elements: {
- box: {
- type: "Box",
- props: { flexDirection: "column" },
- children: ["maybe"],
- },
- maybe: {
- type: "Text",
- props: { text: "Now you see me" },
- visible: { $state: "/show" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Now you see me");
- });
- it("$state equality condition works", () => {
- const spec: Spec = {
- state: { tab: "deploy" },
- root: "box",
- elements: {
- box: {
- type: "Box",
- props: { flexDirection: "column" },
- children: ["deploy-tab", "settings-tab"],
- },
- "deploy-tab": {
- type: "Text",
- props: { text: "Deploy panel" },
- visible: { $state: "/tab", eq: "deploy" },
- children: [],
- },
- "settings-tab": {
- type: "Text",
- props: { text: "Settings panel" },
- visible: { $state: "/tab", eq: "settings" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Deploy panel");
- expect(output).not.toContain("Settings panel");
- });
- });
- describe("ink e2e: repeat → terminal output", () => {
- it("repeat renders one row per array item with $item", () => {
- const spec: Spec = {
- state: {
- tasks: [
- { name: "Lint", done: true },
- { name: "Test", done: false },
- { name: "Build", done: true },
- ],
- },
- root: "list",
- elements: {
- list: {
- type: "Box",
- props: { flexDirection: "column" },
- repeat: { statePath: "/tasks" },
- children: ["row"],
- },
- row: {
- type: "ListItem",
- props: {
- title: { $item: "name" },
- trailing: { $item: "done" },
- leading: "•",
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Lint");
- expect(output).toContain("Test");
- expect(output).toContain("Build");
- });
- });
- describe("ink e2e: full dashboard spec → terminal output", () => {
- const dashboardSpec: Spec = {
- state: {
- deployments: [
- { id: "dpl_abc", service: "api-server", status: "running" },
- { id: "dpl_def", service: "web-app", status: "building" },
- { id: "dpl_ghi", service: "worker", status: "error" },
- ],
- },
- root: "root",
- elements: {
- root: {
- type: "Box",
- props: { flexDirection: "column", padding: 1, gap: 1 },
- children: [
- "heading",
- "divider",
- "stats",
- "table",
- "progress",
- "repeats",
- ],
- },
- heading: {
- type: "Heading",
- props: { text: "Deployment Dashboard", level: "h1", color: "cyan" },
- children: [],
- },
- divider: {
- type: "Divider",
- props: { title: "Overview", color: "gray" },
- children: [],
- },
- stats: {
- type: "Box",
- props: { gap: 4 },
- children: ["kv-total", "kv-err", "badge"],
- },
- "kv-total": {
- type: "KeyValue",
- props: { label: "Total", value: "3", labelColor: "cyan" },
- children: [],
- },
- "kv-err": {
- type: "KeyValue",
- props: { label: "Errors", value: "1", labelColor: "red" },
- children: [],
- },
- badge: {
- type: "Badge",
- props: { label: "LIVE", variant: "success" },
- children: [],
- },
- table: {
- type: "Table",
- props: {
- columns: [
- { header: "Service", key: "service", width: 15 },
- { header: "Status", key: "status", width: 12 },
- ],
- rows: [
- { service: "api-server", status: "running" },
- { service: "web-app", status: "building" },
- { service: "worker", status: "error" },
- ],
- headerColor: "cyan",
- },
- children: [],
- },
- progress: {
- type: "ProgressBar",
- props: { progress: 0.85, width: 30, color: "green", label: "Deploy" },
- children: [],
- },
- repeats: {
- type: "Box",
- props: { flexDirection: "column" },
- repeat: { statePath: "/deployments", key: "id" },
- children: ["item"],
- },
- item: {
- type: "ListItem",
- props: {
- title: { $item: "service" },
- subtitle: { $item: "id" },
- leading: "▸",
- trailing: { $item: "status" },
- },
- children: [],
- },
- },
- };
- it("renders without throwing", () => {
- expect(() => renderSpec(dashboardSpec)).not.toThrow();
- });
- it("output contains heading text", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("Deployment Dashboard");
- });
- it("output contains table data", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("Service");
- expect(output).toContain("api-server");
- expect(output).toContain("web-app");
- expect(output).toContain("worker");
- });
- it("output contains key-value stats", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("Total");
- expect(output).toContain("Errors");
- });
- it("output contains progress bar", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("Deploy");
- expect(output).toContain("85%");
- expect(output).toContain("█");
- });
- it("output contains repeated deployment items", () => {
- const output = renderSpec(dashboardSpec);
- // $item expressions should resolve to actual data
- expect(output).toContain("api-server");
- expect(output).toContain("dpl_abc");
- expect(output).toContain("running");
- expect(output).toContain("web-app");
- expect(output).toContain("worker");
- });
- it("output contains badge", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("LIVE");
- });
- it("output contains divider with title", () => {
- const output = renderSpec(dashboardSpec);
- expect(output).toContain("Overview");
- expect(output).toContain("─");
- });
- });
- describe("ink e2e: Markdown component", () => {
- it("renders bold text", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "This is **bold** text" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("bold");
- expect(output).not.toContain("**");
- });
- it("renders italic text", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "This is *italic* text" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("italic");
- expect(output).not.toContain("*italic*");
- });
- it("renders inline code", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "Run `npm install` to start" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("`npm install`");
- });
- it("renders headings", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "# Main Title\n\n## Section" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Main Title");
- expect(output).toContain("Section");
- expect(output).not.toContain("#");
- });
- it("renders unordered lists", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "- First\n- Second\n- Third" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("First");
- expect(output).toContain("Second");
- expect(output).toContain("Third");
- expect(output).toContain("•");
- });
- it("renders ordered lists", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "1. Alpha\n2. Beta\n3. Gamma" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Alpha");
- expect(output).toContain("Beta");
- expect(output).toContain("Gamma");
- });
- it("renders horizontal rules", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "Above\n\n---\n\nBelow" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Above");
- expect(output).toContain("Below");
- expect(output).toContain("─");
- });
- it("renders code blocks", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: { text: "```\nconst x = 42;\n```" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("const x = 42;");
- expect(output).not.toContain("```");
- });
- it("handles mixed markdown from LLM-style text", () => {
- const spec: Spec = {
- root: "md",
- elements: {
- md: {
- type: "Markdown",
- props: {
- text:
- "Here's a summary with **key metrics** on performance:\n\n" +
- "- **Latency**: 45ms average\n" +
- "- **Uptime**: 99.9%\n" +
- "- **Requests**: 1.2M/day",
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("key metrics");
- expect(output).toContain("Latency");
- expect(output).toContain("45ms average");
- expect(output).not.toContain("**");
- });
- });
- describe("ink e2e: MultiSelect", () => {
- it("renders options with check indicators", () => {
- const spec: Spec = {
- root: "ms",
- elements: {
- ms: {
- type: "MultiSelect",
- props: {
- options: [
- { label: "TypeScript", value: "ts" },
- { label: "Python", value: "py" },
- { label: "Rust", value: "rs" },
- ],
- value: { $state: "/langs" },
- $bindState: { value: "/langs" },
- label: "Languages",
- },
- children: [],
- },
- },
- state: { langs: ["ts"] },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Languages");
- expect(output).toContain("TypeScript");
- expect(output).toContain("Python");
- expect(output).toContain("Rust");
- // ts is selected
- expect(output).toContain("◉");
- // py and rs are not
- expect(output).toContain("◯");
- expect(output).toContain("Selected: 1");
- });
- it("renders with no selections", () => {
- const spec: Spec = {
- root: "ms",
- elements: {
- ms: {
- type: "MultiSelect",
- props: {
- options: [
- { label: "A", value: "a" },
- { label: "B", value: "b" },
- ],
- value: { $state: "/sel" },
- $bindState: { value: "/sel" },
- },
- children: [],
- },
- },
- state: { sel: [] },
- };
- const output = renderSpec(spec);
- expect(output).toContain("A");
- expect(output).toContain("B");
- expect(output).not.toContain("◉");
- expect(output).not.toContain("Selected:");
- });
- });
- describe("ink e2e: ConfirmInput", () => {
- it("renders confirmation prompt with message", () => {
- const spec: Spec = {
- root: "ci",
- elements: {
- ci: {
- type: "ConfirmInput",
- props: {
- message: "Delete all files?",
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Delete all files?");
- expect(output).toContain("Yes");
- expect(output).toContain("No");
- expect(output).toContain("(y/n)");
- });
- it("renders with custom labels", () => {
- const spec: Spec = {
- root: "ci",
- elements: {
- ci: {
- type: "ConfirmInput",
- props: {
- message: "Continue?",
- yesLabel: "Proceed",
- noLabel: "Cancel",
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Continue?");
- expect(output).toContain("Proceed");
- expect(output).toContain("Cancel");
- });
- });
- describe("ink e2e: Tabs", () => {
- it("renders tab bar with active indicator", () => {
- const spec: Spec = {
- root: "tabs",
- elements: {
- tabs: {
- type: "Tabs",
- props: {
- tabs: [
- { label: "Overview", value: "overview" },
- { label: "Logs", value: "logs" },
- { label: "Settings", value: "settings" },
- ],
- value: { $state: "/tab" },
- $bindState: { value: "/tab" },
- },
- children: ["content-overview"],
- },
- "content-overview": {
- type: "Text",
- props: { text: "Overview content here" },
- children: [],
- visible: { $state: "/tab", eq: "overview" },
- },
- },
- state: { tab: "overview" },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Overview");
- expect(output).toContain("Logs");
- expect(output).toContain("Settings");
- expect(output).toContain("Overview content here");
- // Active tab indicator
- expect(output).toContain("━");
- });
- it("renders with icons", () => {
- const spec: Spec = {
- root: "tabs",
- elements: {
- tabs: {
- type: "Tabs",
- props: {
- tabs: [
- { label: "Home", value: "home", icon: "🏠" },
- { label: "Settings", value: "settings", icon: "⚙" },
- ],
- value: { $state: "/tab" },
- $bindState: { value: "/tab" },
- },
- children: [],
- },
- },
- state: { tab: "home" },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Home");
- expect(output).toContain("Settings");
- });
- it("hides content for inactive tab via visible condition", () => {
- const spec: Spec = {
- root: "tabs",
- elements: {
- tabs: {
- type: "Tabs",
- props: {
- tabs: [
- { label: "A", value: "a" },
- { label: "B", value: "b" },
- ],
- value: { $state: "/tab" },
- $bindState: { value: "/tab" },
- },
- children: ["panel-a", "panel-b"],
- },
- "panel-a": {
- type: "Text",
- props: { text: "Panel A content" },
- children: [],
- visible: { $state: "/tab", eq: "a" },
- },
- "panel-b": {
- type: "Text",
- props: { text: "Panel B content" },
- children: [],
- visible: { $state: "/tab", eq: "b" },
- },
- },
- state: { tab: "a" },
- };
- const output = renderSpec(spec);
- expect(output).toContain("Panel A content");
- expect(output).not.toContain("Panel B content");
- });
- });
- describe("ink e2e: Sparkline", () => {
- it("renders sparkline blocks from data", () => {
- const spec: Spec = {
- root: "spark",
- elements: {
- spark: {
- type: "Sparkline",
- props: {
- data: [1, 5, 2, 8, 3, 7, 4],
- label: "CPU",
- color: "cyan",
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("CPU");
- // Should contain some block characters
- expect(output).toMatch(/[▁▂▃▄▅▆▇█]/);
- });
- it("handles single value", () => {
- const spec: Spec = {
- root: "spark",
- elements: {
- spark: {
- type: "Sparkline",
- props: { data: [5] },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toMatch(/[▁▂▃▄▅▆▇█]/);
- });
- it("handles empty data gracefully", () => {
- const spec: Spec = {
- root: "spark",
- elements: {
- spark: {
- type: "Sparkline",
- props: { data: [], label: "Empty" },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("no data");
- });
- it("renders min/max sparkline with all equal values at top", () => {
- const spec: Spec = {
- root: "spark",
- elements: {
- spark: {
- type: "Sparkline",
- props: { data: [5, 5, 5] },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- // All same value — should produce consistent blocks
- expect(output).toMatch(/[▁▂▃▄▅▆▇█]/);
- });
- });
- describe("ink e2e: BarChart", () => {
- it("renders horizontal bars with labels", () => {
- const spec: Spec = {
- root: "chart",
- elements: {
- chart: {
- type: "BarChart",
- props: {
- data: [
- { label: "TypeScript", value: 65, color: "blue" },
- { label: "Python", value: 20, color: "yellow" },
- { label: "Rust", value: 15, color: "red" },
- ],
- showPercentage: true,
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("TypeScript");
- expect(output).toContain("Python");
- expect(output).toContain("Rust");
- expect(output).toContain("█");
- expect(output).toContain("65%");
- expect(output).toContain("20%");
- expect(output).toContain("15%");
- });
- it("renders with showValues", () => {
- const spec: Spec = {
- root: "chart",
- elements: {
- chart: {
- type: "BarChart",
- props: {
- data: [
- { label: "A", value: 100 },
- { label: "B", value: 50 },
- ],
- showValues: true,
- width: 20,
- },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- expect(output).toContain("100");
- expect(output).toContain("50");
- });
- it("handles empty data", () => {
- const spec: Spec = {
- root: "chart",
- elements: {
- chart: {
- type: "BarChart",
- props: { data: [] },
- children: [],
- },
- },
- };
- const output = renderSpec(spec);
- // Should render nothing (no crash)
- expect(output).toBe("");
- });
- });
- describe("ink e2e: state store round-trip", () => {
- it("createStateStore reads/writes state used by specs", () => {
- const store = createStateStore({ name: "Alice", count: 0 });
- expect(store.get("/name")).toBe("Alice");
- expect(store.get("/count")).toBe(0);
- store.set("/count", 42);
- expect(store.get("/count")).toBe(42);
- store.set("/name", "Bob");
- expect(store.get("/name")).toBe("Bob");
- });
- it("array manipulation (push/remove pattern)", () => {
- const store = createStateStore({ items: ["a", "b", "c"] });
- // Push
- const arr = store.get("/items") as string[];
- store.set("/items", [...arr, "d"]);
- expect((store.get("/items") as string[]).length).toBe(4);
- // Remove index 1
- const arr2 = store.get("/items") as string[];
- store.set(
- "/items",
- arr2.filter((_, i) => i !== 1),
- );
- expect(store.get("/items")).toEqual(["a", "c", "d"]);
- });
- });
|