Skip to content
Merged
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
23 changes: 23 additions & 0 deletions e2e/chat-thread.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect, test } from "@playwright/test";

test("an agent turn renders its reasoning and tool parts, each expanding on click", async ({
page,
}) => {
await page.goto("/chat-thread");
const turn = page.getByRole("article", { name: "Scout replied" });
await expect(turn.getByText("Three commits landed today.")).toBeVisible();

const reasoning = turn.locator("details");
await expect(reasoning).not.toHaveAttribute("open");
await reasoning.locator("summary").click();
await expect(reasoning).toHaveAttribute("open");
await expect(
reasoning.getByText("Check the commit log first."),
).toBeVisible();

const tool = turn.getByRole("button", { name: /Read today's commits/ });
await expect(tool).toHaveAttribute("aria-expanded", "false");
await tool.click();
await expect(tool).toHaveAttribute("aria-expanded", "true");
await expect(turn.getByText("3 commits")).toBeVisible();
});
50 changes: 50 additions & 0 deletions e2e/controls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "@playwright/test";

test("Select carries its value, aria wiring and change events to the native control", async ({
page,
}) => {
await page.goto("/select");
const select = page.getByRole("combobox", { name: "Letter" });
await expect(select).toHaveValue("a");
await expect(select).toHaveAttribute("aria-invalid", "true");
await expect(select).toHaveAttribute("aria-describedby", "letter-error");
await select.selectOption("b");
await expect(select).toHaveValue("b");
await expect(page.getByRole("status", { name: "Events" })).toHaveText("b");
await expect(page.getByRole("combobox", { name: "Locked" })).toBeDisabled();
});

test("FilterChip exposes its selected state through aria-pressed", async ({
page,
}) => {
await page.goto("/filter-chip");
const chip = page.getByRole("button", { name: "Live" });
await expect(chip).toHaveAttribute("aria-pressed", "false");
await chip.click();
await expect(chip).toHaveAttribute("aria-pressed", "true");
});

test("DictationStatusLine announces a denial with default copy", async ({
page,
}) => {
await page.goto("/dictation-denied");
await expect(page.getByRole("alert")).toHaveText(
/Microphone access was blocked\./,
);
});

test.describe("AuthLayout panel slot", () => {
test.use({ viewport: { width: 1280, height: 800 } });

test("the decorative panel is empty when panel is omitted", async ({
page,
}) => {
await page.goto("/auth-layout");
await expect(page.locator("[aria-hidden=true]").last()).toBeEmpty();
});

test("the panel renders the node it is given", async ({ page }) => {
await page.goto("/auth-layout-panel");
await expect(page.getByTestId("custom-panel")).toBeVisible();
});
});
81 changes: 81 additions & 0 deletions e2e/fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import type { ReactNode } from "react";

import {
ApprovalCard,
AuthLayout,
Button,
ChatThread,
DictationStatusLine,
FilterChip,
Select,
ThemeProvider,
ThemeToggle,
type ApprovalRequest,
type ChatMessage,
} from "@corbits/react-ui";
import {
CommandPalette,
Expand Down Expand Up @@ -79,6 +85,67 @@ function Palette() {
);
}

const CHAT: ChatMessage[] = [
{
id: "m1",
role: "user",
createdAt: "2026-01-01T09:00:00Z",
parts: [{ type: "text", text: "What changed today?" }],
},
{
id: "m2",
role: "agent",
createdAt: "2026-01-01T09:00:04Z",
parts: [
{ type: "reasoning", text: "Check the commit log first." },
{
type: "tool",
toolCallId: "t1",
toolName: "git__log",
label: "Read today's commits",
state: "done",
output: "3 commits",
},
{ type: "text", text: "Three commits landed today." },
],
},
];

function SelectField() {
const [value, setValue] = useState("a");
const { log, record } = useEventLog();
return (
<>
<Select
aria-label="Letter"
aria-invalid
aria-describedby="letter-error"
value={value}
onChange={(event) => {
setValue(event.target.value);
record(event.target.value);
}}
>
<option value="a">A</option>
<option value="b">B</option>
</Select>
<Select aria-label="Locked" value="a" disabled onChange={() => {}}>
<option value="a">A</option>
</Select>
{log}
</>
);
}

function Chip() {
const [selected, setSelected] = useState(false);
return (
<FilterChip selected={selected} onClick={() => setSelected(!selected)}>
Live
</FilterChip>
);
}

const SWATCH_TOKENS = [
"background",
"foreground",
Expand Down Expand Up @@ -108,6 +175,20 @@ export const scenarios: Record<string, () => ReactNode> = {
"approval-card": () => <Approval state="idle" />,
"approval-card-busy": () => <Approval state="approving" />,
"command-palette": () => <Palette />,
"chat-thread": () => (
<div style={{ display: "flex", flexDirection: "column", height: 480 }}>
<ChatThread messages={CHAT} identity={{ name: "Scout" }} />
</div>
),
select: () => <SelectField />,
"filter-chip": () => <Chip />,
"dictation-denied": () => <DictationStatusLine state="denied" />,
"auth-layout": () => <AuthLayout>Form</AuthLayout>,
"auth-layout-panel": () => (
<AuthLayout panel={<div data-testid="custom-panel">custom</div>}>
Form
</AuthLayout>
),
theme: () => <Swatches />,
"theme-toggle": () => (
<ThemeProvider storageKey="e2e-theme" defaultMode="light">
Expand Down
Loading