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
21 changes: 21 additions & 0 deletions packages/webui/test/browser/harness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ export const PIXEL_BUDGET = { maxDiffPixels: 8 };
/** Screenshot options shared by every baseline comparison. */
export const screenshotOptions = { animations: "disabled", ...PIXEL_BUDGET };

/**
* Waits for the shell's own asset images to settle before a baseline is
* taken.
*
* A screenshot records whatever is painted at that instant, and the shell
* draws its composer mascot from a `<picture>`: the browser starts on the
* poster PNG and upgrades to the WebP, and the upgrade aborts the first
* request. A frame taken inside that window shows the browser's broken-image
* placeholder instead of the picture, which is a few thousand pixels of
* difference that says nothing about the layout under review.
*
* The wait is on the settled state rather than on a deadline: an image that
* is still being fetched is not settled, and one that failed to load fails
* the poll with a legible message instead of a cryptic pixel diff.
*/
export async function waitForLocalImages(page) {
await expect.poll(() => page.locator('img[src^="/assets/"]').evaluateAll((images) =>
images.every((image) => image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0),
)).toBe(true);
}

export async function configureFixture(page, setup) {
await page.addInitScript({ content: `window.__WEBUI_FIXTURE_SETUP__ ??= []; window.__WEBUI_FIXTURE_SETUP__.push((${setup.toString()}));` });
}
Expand Down
3 changes: 2 additions & 1 deletion packages/webui/test/browser/plugin-catalogue-layout.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { expect } from "@playwright/test";

import { openApp, screenshotOptions, test } from "./harness.mjs";
import { openApp, screenshotOptions, test, waitForLocalImages } from "./harness.mjs";

test("marketplace and management keep vertical categories pinned on the left", async ({ page }) => {
await page.addInitScript(() => {
Expand Down Expand Up @@ -430,6 +430,7 @@ async function expectCenteredDialog(dialog, page, fieldsSelector, expectedFields
insideViewport: true,
...(fieldsSelector ? { fieldsDisplay: expectedFieldsDisplay } : {}),
});
await waitForLocalImages(page);
await expect(page.locator('[data-webui-dialog-scrim="true"]')).toHaveScreenshot(screenshotOptions);
}

Expand Down
10 changes: 9 additions & 1 deletion packages/webui/test/browser/responsive-baselines.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@

import { expect } from "@playwright/test";

import { configureFixture, openApp, screenshotOptions, test } from "./harness.mjs";
import {
configureFixture,
openApp,
screenshotOptions,
test,
waitForLocalImages,
} from "./harness.mjs";

/** The widths the contract's matrix covers. */
const WIDTHS = [1440, 1280, 1024, 900, 768, 640, 480];
Expand All @@ -35,6 +41,7 @@ for (const width of WIDTHS) {
await page.setViewportSize({ width, height: 900 });
await configureFixture(page, () => undefined);
await openApp(page, "#session=A");
await waitForLocalImages(page);
await expect(page.locator("#webui-root")).toHaveScreenshot(`shell-at-${width}.png`, screenshotOptions);
});
}
Expand All @@ -46,6 +53,7 @@ for (const width of PANEL_WIDTHS) {
await openApp(page, "#session=A");
await page.getByTestId("workspace-panel-controls").getByRole("button", { name: "浏览器" }).click();
await expect(page.getByTestId("workspace-panel")).toBeVisible();
await waitForLocalImages(page);
await expect(page.locator("#webui-root")).toHaveScreenshot(`workspace-panel-at-${width}.png`, screenshotOptions);
});
}
11 changes: 10 additions & 1 deletion packages/webui/test/browser/scheduling-panel.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
// is in when they click the rail.
import { expect } from "@playwright/test";

import { configureFixture, openApp, screenshotOptions, test } from "./harness.mjs";
import {
configureFixture,
openApp,
screenshotOptions,
test,
waitForLocalImages,
} from "./harness.mjs";

test.describe("the scheduled-task panel", () => {
test("opens from the rail and lists what is scheduled", async ({ page }) => {
Expand Down Expand Up @@ -47,6 +53,7 @@ test.describe("the scheduled-task panel", () => {
// The panel is a surface, not an overlay: the transcript is gone while it is open.
await expect(page.locator('[data-webui-component="composer"]')).toHaveCount(0);

await waitForLocalImages(page);
await expect(panel).toHaveScreenshot("scheduling-panel.png", screenshotOptions);

// Closing returns to the conversation, and the panel's own control is what does it — the rail
Expand Down Expand Up @@ -92,6 +99,7 @@ test.describe("the scheduled-task panel", () => {
await page.locator('[data-webui-model-list-row="fixture/stub-model/"]').click();
await expect(modelField).toContainText("Stub Model");
await editor.locator('[data-webui-scheduling-field="prompt"]').fill("Summarize the day.");
await waitForLocalImages(page);
await expect(editor).toHaveScreenshot("scheduling-editor.png", screenshotOptions);
// The editor's submit control is the dialog footer's, so it is not inside the
// editor's own element.
Expand Down Expand Up @@ -160,6 +168,7 @@ test.describe("the scheduled-task panel", () => {
runs.locator('[data-webui-scheduling-run-session="A"]').first(),
).toContainText("打开会话");

await waitForLocalImages(page);
await expect(panel).toHaveScreenshot("scheduling-panel-runs.png", screenshotOptions);

// Opening the session a run produced leaves the panel and lands on that session.
Expand Down
20 changes: 14 additions & 6 deletions packages/webui/test/browser/visual-baselines.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import { expect } from "@playwright/test";

import {
openApp,
screenshotOptions,
test,
waitForLocalImages,
assistantBody,
emitAgentMessage,
emitStream,
openApp,
screenshotOptions,
startTurn,
test,
} from "./harness.mjs";

/**
Expand Down Expand Up @@ -54,9 +55,7 @@ test("shell and session rail layout baseline", async ({ page }) => {
await expect(rail).toBeVisible();
await expect(rail.getByText("Synthetic session A")).toBeVisible();
await expect(rail.getByText("Synthetic session B")).toBeVisible();
await expect.poll(() => page.locator('img[src^="/assets/"]').evaluateAll((images) =>
images.every((image) => image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0),
)).toBe(true);
await waitForLocalImages(page);
await expect(shell).toHaveScreenshot("shell-and-rail.png", screenshotOptions);
});

Expand Down Expand Up @@ -104,6 +103,7 @@ test("transcript and composer layout baseline", async ({ page }) => {
const transcript = page.locator('[data-webui-transcript="A"]');
await assertInViewport(transcript, page);
await expect(page.getByPlaceholder("输入消息…(输入 / 唤起命令)")).toBeVisible();
await waitForLocalImages(page);
await expect(page.locator("#webui-root")).toHaveScreenshot("transcript-and-composer.png", screenshotOptions);
});

Expand All @@ -120,6 +120,7 @@ test("settings dialog layout baseline", async ({ page }) => {
// same wait the browser panel case uses, over the whole subtree, so this
// records the settled frame rather than a lucky one.
await settleAnimations(dialog);
await waitForLocalImages(page);
await expect(dialog).toHaveScreenshot("settings-dialog.png", screenshotOptions);
});

Expand All @@ -145,6 +146,7 @@ test("progress panel layout baseline", async ({ page }) => {
const panel = page.getByTestId("progress-overview-panel");
await expect(panel).toContainText("Synthetic visual todo");
await assertInViewport(panel, page);
await waitForLocalImages(page);
await expect(panel).toHaveScreenshot("progress-panel.png", screenshotOptions);
});

Expand All @@ -159,6 +161,7 @@ test("embedded browser panel layout baseline", async ({ page }) => {
});
await assertInViewport(panel, page);
await expect(page.getByTestId("workspace-launcher")).toBeVisible();
await waitForLocalImages(page);
await expect(panel).toHaveScreenshot("embedded-browser-panel.png", screenshotOptions);
});

Expand All @@ -171,6 +174,7 @@ test("composer command overlay layout baseline", async ({ page }) => {
const overlay = page.locator('[data-webui-command-menu="true"]');
await expect(overlay).toBeVisible();
await assertInViewport(overlay, page);
await waitForLocalImages(page);
await expect(overlay).toHaveScreenshot("composer-command-overlay.png", screenshotOptions);
});

Expand Down Expand Up @@ -205,11 +209,13 @@ test("plugin marketplace and skills catalogue layout baselines", async ({ page }
const grid = await pluginGrid.first().evaluate((node) => getComputedStyle(node).gridTemplateColumns.split(" ").length);
expect(grid).toBe(2);
await assertInViewport(pluginGrid.first(), page);
await waitForLocalImages(page);
await expect(surface).toHaveScreenshot("plugin-marketplace.png", screenshotOptions);

await surface.getByRole("button", { name: "技能", exact: true }).click();
await expect(surface.locator(".webui-plugin-grid .webui-plugin-row")).toHaveCount(2);
await expect(surface.getByText("Release notes")).toBeVisible();
await waitForLocalImages(page);
await expect(surface).toHaveScreenshot("skill-marketplace.png", screenshotOptions);
});

Expand Down Expand Up @@ -240,12 +246,14 @@ test("plugin management skill and MCP rows layout baselines", async ({ page }) =
await expect(surface.getByText("Local checklist")).toBeVisible();
await expect(surface.getByLabel("启用 disabled-review")).toBeVisible();
await assertInViewport(surface.locator(".webui-plugin-list"), page);
await waitForLocalImages(page);
await expect(surface).toHaveScreenshot("skill-management.png", screenshotOptions);

await surface.locator('.webui-plugin-category-filter button', { hasText: "MCP" }).click();
// The rows name their servers; the status chip that used to sit beside them was
// removed with the row redesign, so the baseline shows the rows and their actions.
await expect(surface.getByText("local-http")).toBeVisible();
await expect(surface.getByText("missing-binary")).toBeVisible();
await waitForLocalImages(page);
await expect(surface).toHaveScreenshot("mcp-management.png", screenshotOptions);
});
20 changes: 19 additions & 1 deletion packages/webui/test/integration/agent-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,25 @@ it.each(["transcript", "cross-client", "external-active-mutation", "external-act
await page.getByTestId("workspace-launcher-url").fill(pageUrl);
await page.getByTestId("workspace-launcher-url-submit").click();
const browserFrame = page.getByTestId("browser-panel-frame");
await browserExpect(browserFrame).toBeVisible({ timeout: 30_000 });
// The panel can only render a frame after the isolated Chromium has
// booted, opened the page and streamed its first picture back over the
// event channel. That whole pipeline -- not the product's own
// responsiveness -- is what this wait covers, and on a loaded CI runner
// it can outlast the 30s window this test used to give it. Give the wait
// its own budget, and keep the failure readable: if the panel still
// shows its empty placeholder the frame never arrived, and if a
// capability notice is on screen the host could not serve a browser at
// all. Those are different faults and deserve different fixes.
try {
await browserExpect(browserFrame).toBeVisible({ timeout: 60_000 });
} catch (error) {
throw new Error(`${error instanceof Error ? error.message : String(error)}\n${JSON.stringify({
emptyFrameStillShowing: await page.getByTestId("browser-panel-frame-empty").count(),
capabilityNotices: await page.locator(
'[data-testid="browser-panel-unknown"], [data-testid="browser-panel-unauthorized"]',
).allTextContents(),
})}`);
}
const browserGeometry = await browserFrame.evaluate((node) => {
const frame = node.getBoundingClientRect();
const panel = node.closest(".webui-browser-panel")?.getBoundingClientRect();
Expand Down
Loading