tests/chat.spec.js
import { test, expect, sse } from "./fixtures.js";
test("a reply streams in and the Stop button shows while it does", async ({ chat }) => {
await chat.ask("hello");
await expect(chat.stop).toBeVisible(); // still streaming
await chat.waitForReply();
await expect(chat.stop).toBeHidden();
await expect(chat.lastReply()).toContainText("I can look up an order");
});
test("looking up an order renders an order card @mobile", async ({ chat }) => {
await chat.ask("where is A-1002?");
await chat.waitForReply();
// An ARIA snapshot asserts structure and accessible names in one go (§77).
await expect(chat.lastReply().getByRole("article")).toMatchAriaSnapshot(`
- article "Order A-1002":
- strong: A-1002
- text: /Mechanical keyboard/
`);
});
test("refund needs a human approval, then updates the order list", async ({ chat, seedOrder, page }) => {
// The server is shared by every test (and by retries), so this test makes
// its own order instead of mutating A-1001/A-1002 that others rely on.
const order = await seedOrder({ item: "Desk lamp", total: 35 });
await page.reload(); // pick up the new order in the sidebar
await chat.ask(`please refund ${order.id}`);
await chat.waitForReply();
const card = chat.lastReply().getByRole("article", { name: `Approve refund for ${order.id}` });
await expect(card).toBeVisible();
await card.getByRole("button", { name: "Approve refund of $35.00" }).click();
await expect(card.getByRole("status")).toHaveText(`Refund requested for ${order.id}.`);
await expect(chat.orders.locator(`[data-order="${order.id}"]`)).toContainText("refund requested");
});
test("declining a refund changes nothing", async ({ chat }) => {
await chat.ask("refund A-1001");
await chat.waitForReply();
const card = chat.lastReply().getByRole("article", { name: "Approve refund for A-1001" });
await card.getByRole("button", { name: "Decline" }).click();
await expect(card.getByRole("status")).toHaveText("Refund declined. Nothing was changed.");
await expect(chat.orders.locator('[data-order="A-1001"]')).toContainText("delivered");
});
test("you cannot look up someone else's order", async ({ chat }) => {
await chat.ask("where is B-2001?");
await chat.waitForReply();
await expect(chat.lastReply()).toHaveText("I couldn't find order B-2001 on your account.");
});
test("Stop aborts the stream mid-reply", async ({ chat, page }) => {
// Hold the response for a second, so there is time to press Stop mid-request.
await page.route("**/api/chat", async (route) => {
const body = sse(
{ type: "run-started" },
...Array.from({ length: 50 }, (_, i) => ({ type: "text-delta", delta: `word${i} ` })),
{ type: "run-finished" },
);
await new Promise((r) => setTimeout(r, 1000)); // hold the response
await route.fulfill({ contentType: "text/event-stream", body }).catch(() => {}); // the page may have aborted
});
await chat.ask("tell me a long story");
await chat.stop.click();
await expect(chat.lastReply()).toContainText("(stopped)");
await expect(chat.send).toBeEnabled();
});
test.describe("with a mocked agent", () => {
test("a server error shows Retry, and Retry recovers", async ({ chat, page }) => {
let calls = 0;
await page.route("**/api/chat", (route) =>
++calls === 1
? route.fulfill({ status: 500, json: { error: "boom" } })
: route.fulfill({ contentType: "text/event-stream", body: sse({ type: "text-delta", delta: "Back online." }) }),
);
await chat.ask("hello?");
await expect(page.getByRole("alert")).toContainText("The assistant is unavailable.");
await page.getByRole("button", { name: "Retry" }).click();
await expect(chat.lastReply()).toHaveText("Back online.");
await expect(page.getByRole("alert")).toBeHidden();
});
test("model output is rendered as text, never as HTML", async ({ chat, page }) => {
const dialogs = [];
page.on("dialog", (d) => (dialogs.push(d.message()), d.dismiss()));
const payload = `<img src=x onerror="alert('xss')">`;
await page.route("**/api/chat", (route) =>
route.fulfill({ contentType: "text/event-stream", body: sse({ type: "text-delta", delta: payload }) }),
);
await chat.ask("hi");
await expect(chat.lastReply()).toHaveText(payload);
await expect(chat.lastReply().locator("img")).toHaveCount(0);
expect(dialogs).toEqual([]);
});
test("the UI refuses components it does not know or props that fail validation", async ({ chat, page }) => {
await page.route("**/api/chat", (route) =>
route.fulfill({
contentType: "text/event-stream",
body: sse(
{ type: "ui", component: "iframe", props: { src: "https://evil.example" } },
{ type: "ui", component: "order-card", props: { id: "X-1", item: "Thing", total: "free", status: "?" } },
{ type: "ui", component: "order-card", props: { id: "X-2", item: "Real thing", total: 5, status: "ok" } },
),
}),
);
await chat.ask("show me");
await chat.waitForReply();
await expect(chat.lastReply().getByRole("article")).toHaveCount(1);
await expect(chat.lastReply().getByRole("article")).toHaveAccessibleName("Order X-2");
});
});