diff --git a/e2e/chat-thread.spec.ts b/e2e/chat-thread.spec.ts
new file mode 100644
index 00000000..508a5c20
--- /dev/null
+++ b/e2e/chat-thread.spec.ts
@@ -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();
+});
diff --git a/e2e/controls.spec.ts b/e2e/controls.spec.ts
new file mode 100644
index 00000000..1025689e
--- /dev/null
+++ b/e2e/controls.spec.ts
@@ -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();
+ });
+});
diff --git a/e2e/fixtures.tsx b/e2e/fixtures.tsx
index b20b7e01..6f8ae994 100644
--- a/e2e/fixtures.tsx
+++ b/e2e/fixtures.tsx
@@ -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,
@@ -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 (
+ <>
+
+
+ {log}
+ >
+ );
+}
+
+function Chip() {
+ const [selected, setSelected] = useState(false);
+ return (
+ setSelected(!selected)}>
+ Live
+
+ );
+}
+
const SWATCH_TOKENS = [
"background",
"foreground",
@@ -108,6 +175,20 @@ export const scenarios: Record ReactNode> = {
"approval-card": () => ,
"approval-card-busy": () => ,
"command-palette": () => ,
+ "chat-thread": () => (
+
+
+
+ ),
+ select: () => ,
+ "filter-chip": () => ,
+ "dictation-denied": () => ,
+ "auth-layout": () => Form,
+ "auth-layout-panel": () => (
+ custom}>
+ Form
+
+ ),
theme: () => ,
"theme-toggle": () => (