Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"typecheck": "tsc --noEmit",
"registry:build": "shadcn build registry.json --output ./public/r",
"postinstall": "fumadocs-mdx",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"check": "ultracite check",
"fix": "ultracite fix",
"prepare": "lefthook install"
Expand Down Expand Up @@ -60,13 +63,16 @@
"@types/node": "^25",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitest/coverage-v8": "^4.1.2",
"ink-testing": "^0.2.0",
"lefthook": "^2.1.4",
"oxfmt": "^0.43.0",
"oxlint": "^1.58.0",
"tailwindcss": "^4.2.2",
"tw-animate-css": "^1.4.0",
"typescript": "^6",
"ultracite": "7.4.3"
"ultracite": "7.4.3",
"vitest": "^4.1.2"
},
"engines": {
"node": ">=20.9.0",
Expand Down
355 changes: 353 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions registry/ui/__tests__/alert.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Text } from "ink";
import React from "react";
import { describe, it, expect, afterEach } from "vitest";

import { Alert } from "../alert";
import { renderTui } from "./render-tui";

describe("Alert", () => {
let unmount: () => void;
afterEach(() => unmount?.());

it("renders children", () => {
const tui = renderTui(
<Alert bordered={false}>
<Text>body text</Text>
</Alert>
);
({ unmount } = tui);
expect(tui.screen.contains("body text")).toBe(true);
});

it("renders title", () => {
const tui = renderTui(
<Alert title="Notice" bordered={false}>
<Text>msg</Text>
</Alert>
);
({ unmount } = tui);
expect(tui.screen.contains("Notice")).toBe(true);
});

it.each([
["success", "✓"],
["error", "✗"],
["warning", "⚠"],
["info", "ℹ"],
] as const)("renders variant icon for %s", (variant, icon) => {
const tui = renderTui(<Alert variant={variant} bordered={false} />);
({ unmount } = tui);
expect(tui.screen.contains(icon)).toBe(true);
});

it("renders without border", () => {
const tui = renderTui(
<Alert bordered={false} title="T">
<Text>inside</Text>
</Alert>
);
({ unmount } = tui);
expect(tui.screen.contains("inside")).toBe(true);
expect(tui.screen.contains("T")).toBe(true);
});
});
180 changes: 180 additions & 0 deletions registry/ui/__tests__/app-shell.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { Text } from "ink";
import React from "react";
import { describe, it, expect, vi, afterEach } from "vitest";

import { AppShell } from "../app-shell";
import type { TuiInstance } from "./render-tui";
import { renderTui } from "./render-tui";

const typeChars = async (tui: TuiInstance, text: string) => {
for (const char of text) {
tui.keys.press(char);
await tui.flush();
}
};

describe("AppShell", () => {
let unmount: () => void;
afterEach(() => unmount?.());

it("renders children", () => {
const tui = renderTui(
<AppShell>
<Text>App Content</Text>
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("App Content")).toBe(true);
});

describe("AppShell.Header", () => {
it("renders header children", () => {
const tui = renderTui(
<AppShell>
<AppShell.Header>
<Text>My App Header</Text>
</AppShell.Header>
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("My App Header")).toBe(true);
});
});

describe("AppShell.Content", () => {
it("renders content children", () => {
const tui = renderTui(
<AppShell>
<AppShell.Content>
<Text>Main content area</Text>
</AppShell.Content>
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("Main content area")).toBe(true);
});
});

describe("AppShell.Input", () => {
it("renders placeholder", () => {
const tui = renderTui(
<AppShell>
<AppShell.Input placeholder="Enter command..." />
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("Enter command...")).toBe(true);
});

it("renders prefix", () => {
const tui = renderTui(
<AppShell>
<AppShell.Input prefix="$" />
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("$")).toBe(true);
});

it("accepts typed input", async () => {
const tui = renderTui(
<AppShell>
<AppShell.Input />
</AppShell>
);
({ unmount } = tui);
await typeChars(tui, "hello");
expect(tui.screen.contains("hello")).toBe(true);
});

it("calls onChange", async () => {
const onChange = vi.fn();
const tui = renderTui(
<AppShell>
<AppShell.Input value="" onChange={onChange} />
</AppShell>
);
({ unmount } = tui);
tui.keys.press("x");
await tui.flush();
expect(onChange).toHaveBeenCalledWith("x");
});

it("calls onSubmit on enter", async () => {
const onSubmit = vi.fn();
const tui = renderTui(
<AppShell>
<AppShell.Input onSubmit={onSubmit} />
</AppShell>
);
({ unmount } = tui);
await typeChars(tui, "cmd");
tui.keys.enter();
await tui.flush();
expect(onSubmit).toHaveBeenCalledWith("cmd");
});

it("clears input after uncontrolled submit", async () => {
const onSubmit = vi.fn();
const tui = renderTui(
<AppShell>
<AppShell.Input onSubmit={onSubmit} />
</AppShell>
);
({ unmount } = tui);
await typeChars(tui, "test");
tui.keys.enter();
await tui.flush();
expect(tui.screen.contains("Type something...")).toBe(true);
});

it("handles backspace", async () => {
const tui = renderTui(
<AppShell>
<AppShell.Input />
</AppShell>
);
({ unmount } = tui);
await typeChars(tui, "ab");
tui.keys.backspace();
await tui.flush();
expect(tui.screen.contains("a")).toBe(true);
});
});

describe("AppShell.Hints", () => {
it("renders hint items", () => {
const tui = renderTui(
<AppShell>
<AppShell.Hints items={["Ctrl+C: quit", "?: help"]} />
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("Ctrl+C: quit")).toBe(true);
expect(tui.screen.contains("?: help")).toBe(true);
});

it("renders children as content", () => {
const tui = renderTui(
<AppShell>
<AppShell.Hints>Press q to quit</AppShell.Hints>
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("Press q to quit")).toBe(true);
});
});

describe("AppShell.Tip", () => {
it("renders tip text", () => {
const tui = renderTui(
<AppShell>
<AppShell.Tip>Use tab to navigate</AppShell.Tip>
</AppShell>
);
({ unmount } = tui);
expect(tui.screen.contains("Tip:")).toBe(true);
expect(tui.screen.contains("Use tab to navigate")).toBe(true);
});
});
});
31 changes: 31 additions & 0 deletions registry/ui/__tests__/aspect-ratio.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Text } from "ink";
import React from "react";
import { describe, it, expect, afterEach } from "vitest";

import { AspectRatio } from "../aspect-ratio";
import { renderTui } from "./render-tui";

describe("AspectRatio", () => {
let unmount: () => void;
afterEach(() => unmount?.());

it("renders children", () => {
const tui = renderTui(
<AspectRatio>
<Text>child</Text>
</AspectRatio>
);
({ unmount } = tui);
expect(tui.screen.contains("child")).toBe(true);
});

it("renders without crashing", () => {
const tui = renderTui(
<AspectRatio ratio={4 / 3} width={40}>
<Text>ok</Text>
</AspectRatio>
);
({ unmount } = tui);
expect(tui.screen.text().length).toBeGreaterThan(0);
});
});
37 changes: 37 additions & 0 deletions registry/ui/__tests__/badge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { describe, it, expect, afterEach } from "vitest";

import { Badge } from "../badge";
import { renderTui } from "./render-tui";

describe("Badge", () => {
let unmount: () => void;
afterEach(() => unmount?.());

it("renders children text", () => {
const tui = renderTui(<Badge>hello</Badge>);
({ unmount } = tui);
expect(tui.screen.contains("hello")).toBe(true);
});

it("renders without border when bordered=false", () => {
const tui = renderTui(<Badge bordered={false}>plain</Badge>);
({ unmount } = tui);
expect(tui.screen.contains("plain")).toBe(true);
});

it("renders all variants", () => {
for (const variant of [
"default",
"success",
"warning",
"error",
"info",
"secondary",
] as const) {
const tui = renderTui(<Badge variant={variant}>v</Badge>);
expect(tui.screen.text().length).toBeGreaterThan(0);
tui.unmount();
}
});
});
Loading
Loading