Frontend Labs
⋯

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");
  });
});

Generated from SOURCE.md · 8 Oct 2026 16:36