Files
buzz/desktop/tests/e2e/needs-restart-screenshots.spec.ts
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

563 lines
19 KiB
TypeScript

/**
* Screenshot spec for the needsRestart badge and config diff (PR #1853 + diff
* overlay work).
*
* Exercises:
* - Agent grid restart actions without a duplicate status badge.
* - Profile badge tooltip with itemised before→after diff.
* - Runtime-tab banner with full uncapped diff list.
* - Side-panel badge visible on the default (Info) tab — not only Runtime.
* - DOM validity: tooltip trigger has no <button> ancestor.
* - Generic rendering: unknown field ids, number/array values, masked values.
*/
import { expect, test } from "@playwright/test";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
const SHOTS = "test-results/restart-diff-screenshots";
// ── Sample diff entries ───────────────────────────────────────────────────────
const DIFF_ENTRIES = [
{
field: "model",
change: {
kind: "value" as const,
before: "gpt-4",
after: "claude-3-5-sonnet",
},
},
{
field: "system_prompt",
change: { kind: "text" as const, before_chars: 512, after_chars: 1024 },
},
{
field: "env.OPENAI_API_KEY",
change: { kind: "masked" as const, before: "••••abc1", after: "••••xyz9" },
},
{ field: "env.BUZZ_LOG", change: { kind: "added" as const } },
{
field: "relay_url",
change: { kind: "masked" as const, before: "••••", after: "••••" },
},
// Array value — args are atomic; rendered via JSON.stringify (position 6: last visible in tooltip)
{
field: "agent_args",
change: {
kind: "value" as const,
before: ["acp"],
after: ["acp", "--verbose"],
},
},
// 7th entry — truncated in tooltip (cap is 6); visible in uncapped banner
{
field: "parallelism",
change: { kind: "value" as const, before: 1, after: 4 },
},
// 8th entry — truncated in tooltip; visible in uncapped banner
{
field: "args",
change: { kind: "masked" as const, before: "••••", after: "••••" },
},
];
const STANDALONE_AGENT = {
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "Local Agent",
status: "running" as const,
needsRestart: true,
restartDiff: DIFF_ENTRIES,
};
const PERSONA_AGENT = {
pubkey: TEST_IDENTITIES.bob.pubkey,
name: "Persona Agent",
personaId: "builtin:fizz",
status: "running" as const,
needsRestart: true,
restartDiff: DIFF_ENTRIES,
};
/** Running agent with no config drift — restart action must be absent. */
const NO_DRIFT_AGENT = {
pubkey: TEST_IDENTITIES.tyler.pubkey,
name: "Stable Agent",
status: "running" as const,
needsRestart: false,
};
/**
* Inactive agent with a friendly error AND a restart diff. Opening its card
* forces the panel to open on the Runtime tab (opensRuntimeTab logic).
* Used to assert: Runtime tab is active, hero badge visible, uncapped banner.
*/
const INACTIVE_FRIENDLY_ERROR_AGENT = {
pubkey: TEST_IDENTITIES.outsider.pubkey,
name: "Error Restart Agent",
status: "stopped" as const,
needsRestart: true,
restartDiff: DIFF_ENTRIES,
lastError: "Agent reported error (code -32002): llm model not found",
lastErrorCode: -32002,
};
const RESTART_AGENT = {
pubkey: "cd".repeat(32),
name: "Restart Agent",
status: "running" as const,
needsRestart: true,
restartDiff: DIFF_ENTRIES,
};
const START_AGENT = {
pubkey: "ef".repeat(32),
name: "Start Agent",
status: "stopped" as const,
needsRestart: false,
};
async function gotoAgentsView(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("open-agents-view")).toBeVisible({
timeout: 10_000,
});
await page.getByTestId("open-agents-view").click();
await expect(page.getByTestId("agents-library-personas")).toBeVisible({
timeout: 10_000,
});
}
const WCAG_AA_NORMAL_TEXT_CONTRAST = 4.5;
async function renderedTextContrast(
locator: import("@playwright/test").Locator,
): Promise<number> {
return locator.evaluate((element) => {
type Rgba = { r: number; g: number; b: number; a: number };
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) throw new Error("Could not create contrast test canvas");
const parseColor = (color: string): Rgba => {
context.clearRect(0, 0, 1, 1);
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
const [r, g, b, alpha] = context.getImageData(0, 0, 1, 1).data;
return { r, g, b, a: alpha / 255 };
};
const composite = (foreground: Rgba, background: Rgba): Rgba => {
const alpha = foreground.a + background.a * (1 - foreground.a);
if (alpha === 0) return { r: 0, g: 0, b: 0, a: 0 };
return {
r:
(foreground.r * foreground.a +
background.r * background.a * (1 - foreground.a)) /
alpha,
g:
(foreground.g * foreground.a +
background.g * background.a * (1 - foreground.a)) /
alpha,
b:
(foreground.b * foreground.a +
background.b * background.a * (1 - foreground.a)) /
alpha,
a: alpha,
};
};
const backgroundLayers: Rgba[] = [];
let current: Element | null = element;
while (current) {
backgroundLayers.push(
parseColor(window.getComputedStyle(current).backgroundColor),
);
current = current.parentElement;
}
let renderedBackground: Rgba = { r: 255, g: 255, b: 255, a: 1 };
for (const layer of backgroundLayers.reverse()) {
renderedBackground = composite(layer, renderedBackground);
}
const renderedForeground = composite(
parseColor(window.getComputedStyle(element).color),
renderedBackground,
);
const luminance = (color: Rgba) => {
const channels = [color.r, color.g, color.b].map((value) => {
const channel = value / 255;
return channel <= 0.04045
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
};
const foregroundLuminance = luminance(renderedForeground);
const backgroundLuminance = luminance(renderedBackground);
return (
(Math.max(foregroundLuminance, backgroundLuminance) + 0.05) /
(Math.min(foregroundLuminance, backgroundLuminance) + 0.05)
);
});
}
test.describe("restart-diff screenshots", () => {
test.use({ viewport: { width: 1280, height: 900 } });
test.beforeEach(async ({ page }) => {
page.on("pageerror", (err) => {
console.error(
"PAGE ERROR:",
err.message,
err.stack?.split("\n").slice(0, 5).join("\n"),
);
});
});
// ── Badge presence ──────────────────────────────────────────────────────────
test("01-grid-standalone-restart-badge", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [STANDALONE_AGENT, NO_DRIFT_AGENT],
});
await gotoAgentsView(page);
const agentCard = page.getByTestId(
`managed-agent-${STANDALONE_AGENT.pubkey}`,
);
await expect(agentCard).toBeVisible({ timeout: 10_000 });
await expect(agentCard.getByText("Restart", { exact: true })).toBeVisible();
await expect(agentCard.getByTestId("restart-diff-badge")).toHaveCount(0);
const stableCard = page.getByTestId(
`managed-agent-${NO_DRIFT_AGENT.pubkey}`,
);
await expect(stableCard).toBeVisible({ timeout: 10_000 });
await expect(stableCard.getByText("Restart", { exact: true })).toHaveCount(
0,
);
await waitForAnimations(page);
await agentCard.screenshot({
path: `${SHOTS}/01-grid-standalone-restart-badge.png`,
});
});
test("02-grid-persona-restart-badge", async ({ page }) => {
await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"],
managedAgents: [PERSONA_AGENT],
});
await gotoAgentsView(page);
const personaCard = page.getByTestId(
`persona-agent-row-${PERSONA_AGENT.personaId}`,
);
await expect(personaCard).toBeVisible({ timeout: 10_000 });
await expect(
personaCard.getByText("Restart", { exact: true }),
).toBeVisible();
await expect(personaCard.getByTestId("restart-diff-badge")).toHaveCount(0);
await waitForAnimations(page);
await personaCard.screenshot({
path: `${SHOTS}/02-grid-persona-restart-badge.png`,
});
});
test("03-running-restart-action", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [RESTART_AGENT],
});
await gotoAgentsView(page);
const agentCard = page.getByTestId(`managed-agent-${RESTART_AGENT.pubkey}`);
await expect(agentCard).toBeVisible({ timeout: 10_000 });
const restartAction = page.getByTestId(
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
);
await expect(restartAction).toHaveText("Restart");
await expect(restartAction).toHaveAttribute("aria-label", "Restart Agent");
await expect(restartAction).toHaveClass(/bg-transparent/);
await expect(restartAction).toHaveClass(/text-amber-800/);
await expect(restartAction).toHaveClass(/dark:text-amber-400/);
await expect(restartAction.locator("svg")).toHaveCount(0);
await expect(restartAction).toHaveCSS("width", "72px");
await expect(restartAction).toHaveCSS("height", "36px");
await expect(agentCard.getByTestId("restart-diff-badge")).toHaveCount(0);
await expect(page.locator("html")).toHaveClass(/light/);
expect(await renderedTextContrast(restartAction)).toBeGreaterThanOrEqual(
WCAG_AA_NORMAL_TEXT_CONTRAST,
);
await waitForAnimations(page);
await agentCard.screenshot({
path: `${SHOTS}/03-running-restart-action.png`,
});
});
test("restart action meets WCAG AA contrast in dark mode", async ({
page,
}) => {
await page.addInitScript(() => {
window.localStorage.setItem("buzz-theme", "catppuccin-mocha");
});
await installMockBridge(page, {
managedAgents: [RESTART_AGENT],
});
await gotoAgentsView(page);
const restartAction = page.getByTestId(
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
);
await expect(restartAction).toBeVisible();
await expect(page.locator("html")).toHaveClass(/dark/);
expect(await renderedTextContrast(restartAction)).toBeGreaterThanOrEqual(
WCAG_AA_NORMAL_TEXT_CONTRAST,
);
});
test("start and restart pills share geometry except for width", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [START_AGENT, RESTART_AGENT],
});
await gotoAgentsView(page);
const startAction = page.getByTestId(
`agent-runtime-start-${START_AGENT.pubkey}`,
);
const restartAction = page.getByTestId(
`agent-runtime-start-${RESTART_AGENT.pubkey}`,
);
await expect(startAction).toHaveText("Start");
await expect(restartAction).toHaveText("Restart");
const relativeGeometry = async (locator: typeof startAction) =>
locator.evaluate((element) => {
const frame = element.parentElement?.parentElement?.parentElement;
if (!frame) throw new Error("Could not resolve avatar frame");
const bounds = element.getBoundingClientRect();
const frameBounds = frame.getBoundingClientRect();
return {
centerX: bounds.x + bounds.width / 2 - frameBounds.x,
centerY: bounds.y + bounds.height / 2 - frameBounds.y,
height: bounds.height,
width: bounds.width,
};
});
const [start, restart] = await Promise.all([
relativeGeometry(startAction),
relativeGeometry(restartAction),
]);
expect(start.height).toBeCloseTo(restart.height, 2);
expect(start.centerX).toBeCloseTo(restart.centerX, 2);
expect(start.centerY).toBeCloseTo(restart.centerY, 2);
expect(start.width).toBeCloseTo(56, 2);
expect(restart.width).toBeCloseTo(72, 2);
});
// ── Runtime-tab banner with full uncapped diff ────────────────────────────
test("07-runtime-tab-restart-banner-with-diff", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [STANDALONE_AGENT],
});
await gotoAgentsView(page);
const agentButton = page.getByRole("button", {
name: `${STANDALONE_AGENT.name} agent profile`,
});
await expect(agentButton).toBeVisible({ timeout: 10_000 });
await agentButton.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible({ timeout: 10_000 });
// Switch to the Runtime tab
await panel.getByRole("tab", { name: "Runtime" }).click();
const banner = panel.getByTestId("needs-restart-banner");
await expect(banner).toBeVisible({ timeout: 10_000 });
// Banner shows the full uncapped diff list (8 entries, no "and N more")
const diffList = banner.getByTestId("restart-diff-list");
await expect(diffList).toBeVisible();
// All 8 entries visible in banner (no cap).
// exact: true prevents substring collision with "Agent args:" label.
await expect(diffList.getByText("Args:", { exact: true })).toBeVisible();
await waitForAnimations(page);
await banner.screenshot({
path: `${SHOTS}/07-runtime-tab-banner-with-diff.png`,
});
});
test("08-runtime-tab-restart-banner-auto-on", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [STANDALONE_AGENT],
});
await gotoAgentsView(page);
const agentButton = page.getByRole("button", {
name: `${STANDALONE_AGENT.name} agent profile`,
});
await agentButton.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible({ timeout: 10_000 });
await panel.getByRole("tab", { name: "Runtime" }).click();
const banner = panel.getByTestId("needs-restart-banner");
await expect(banner).toBeVisible({ timeout: 10_000 });
await expect(
banner.getByText("Buzz can restart it automatically"),
).toBeVisible();
await waitForAnimations(page);
await banner.screenshot({
path: `${SHOTS}/08-runtime-tab-banner-auto-on.png`,
});
});
test("09-runtime-tab-restart-banner-auto-off", async ({ page }) => {
const agentAutoOff = {
...STANDALONE_AGENT,
autoRestartOnConfigChange: false,
};
await installMockBridge(page, {
managedAgents: [agentAutoOff],
});
await gotoAgentsView(page);
const agentButton = page.getByRole("button", {
name: `${agentAutoOff.name} agent profile`,
});
await agentButton.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible({ timeout: 10_000 });
await panel.getByRole("tab", { name: "Runtime" }).click();
const banner = panel.getByTestId("needs-restart-banner");
await expect(banner).toBeVisible({ timeout: 10_000 });
await expect(banner.getByText("Automatic restart is off")).toBeVisible();
await waitForAnimations(page);
await banner.screenshot({
path: `${SHOTS}/09-runtime-tab-banner-auto-off.png`,
});
});
// ── Side-panel badge on default (Info) tab ─────────────────────────────────
test("10-profile-panel-badge-on-default-info-tab", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [STANDALONE_AGENT],
});
await gotoAgentsView(page);
// Open profile panel via the agent card button — opens on Info tab by default
const agentButton = page.getByRole("button", {
name: `${STANDALONE_AGENT.name} agent profile`,
});
await expect(agentButton).toBeVisible({ timeout: 10_000 });
await agentButton.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible({ timeout: 10_000 });
// Tab-independent badge: visible even on the Info tab (not only Runtime)
const heroBadge = panel.getByTestId("restart-diff-badge");
await expect(heroBadge).toBeVisible({ timeout: 5_000 });
// Hero badge tooltip is functional: hover shows the diff list
await heroBadge.hover();
const heroTooltip = page.locator("[role=tooltip]");
await expect(heroTooltip).toBeVisible({ timeout: 5_000 });
await expect(heroTooltip.getByText("Model:")).toBeVisible();
// The Runtime tab banner is NOT visible on Info tab
await expect(panel.getByTestId("needs-restart-banner")).toHaveCount(0);
await waitForAnimations(page);
await panel.screenshot({
path: `${SHOTS}/10-panel-badge-default-info-tab.png`,
});
});
// ── Inactive + friendly-error: panel opens on Runtime, hero badge + banner ─
test("11-inactive-friendly-error-panel-opens-runtime-tab", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [INACTIVE_FRIENDLY_ERROR_AGENT],
});
await gotoAgentsView(page);
// The card for an inactive agent with a friendly error forces Runtime tab on open.
const agentCard = page.getByTestId(
`managed-agent-${INACTIVE_FRIENDLY_ERROR_AGENT.pubkey}`,
);
await expect(agentCard).toBeVisible({ timeout: 10_000 });
await agentCard.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible({ timeout: 10_000 });
// Panel opens on Runtime tab (opensRuntimeTab = true for inactive+friendlyError)
const runtimeTab = panel.getByRole("tab", { name: "Runtime" });
await expect(runtimeTab).toHaveAttribute("aria-selected", "true", {
timeout: 5_000,
});
// Hero badge is visible on the Runtime tab (tab-independent)
const heroBadge = panel.getByTestId("restart-diff-badge");
await expect(heroBadge).toBeVisible({ timeout: 5_000 });
// Hero badge tooltip works on the Runtime tab too
await heroBadge.hover();
const heroTooltip = page.locator("[role=tooltip]");
await expect(heroTooltip).toBeVisible({ timeout: 5_000 });
await expect(heroTooltip.getByText("Model:")).toBeVisible();
// Tooltip is capped at 6 + "and 2 more"
await expect(heroTooltip.getByText("and 2 more")).toBeVisible();
// Full uncapped banner also visible on Runtime tab
const banner = panel.getByTestId("needs-restart-banner");
await expect(banner).toBeVisible({ timeout: 5_000 });
const diffList = banner.getByTestId("restart-diff-list");
await expect(diffList).toBeVisible();
// All 8 entries visible in banner (no cap) — last entry "args" is present.
// exact: true prevents substring collision with "Agent args:" label.
await expect(diffList.getByText("Args:", { exact: true })).toBeVisible();
await waitForAnimations(page);
await panel.screenshot({
path: `${SHOTS}/11-inactive-error-panel-runtime-tab.png`,
});
});
});