Files
kilocode/packages/kilo-vscode/tests/visual-regression.spec.ts
T
2026-08-19 12:16:44 +02:00

142 lines
5.0 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test"
import { platform } from "node:os"
const IS_DARWIN = platform() === "darwin"
// Screenshot baselines are captured on Linux CI — font rendering and anti-aliasing
// differ on macOS, which causes false-positive diffs. Skip the entire suite there.
if (IS_DARWIN) {
console.warn("Visual regression tests must be run on CI, skipping on local macOS.")
test.skip()
}
type Story = {
id: string
title: string
name: string
}
type StoriesIndex = {
stories?: Record<string, Story>
entries?: Record<string, Story>
}
const STORYBOOK_URL = "http://localhost:6007"
// Fetched once per worker process — cheap HTTP call to the already-running Storybook
async function fetchStories(): Promise<Story[]> {
const res = await fetch(`${STORYBOOK_URL}/index.json`).catch(() => fetch(`${STORYBOOK_URL}/stories.json`))
if (!res.ok) throw new Error(`Storybook index fetch failed: ${res.status} ${res.statusText}`)
const data = (await res.json()) as StoriesIndex
const map = data.entries ?? data.stories ?? {}
return Object.values(map).filter((s) => s.id && !s.id.endsWith("--docs"))
}
async function disableAnimations(page: Page) {
await page.addStyleTag({
content: `
*, *::before, *::after {
animation: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`,
})
}
async function settle(page: Page) {
const frames = () =>
page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
}),
)
await page.evaluate(async () => {
await document.fonts.ready
})
await frames()
await page.waitForFunction(
() => {
const root = document.querySelector("#storybook-root")
return root && !root.querySelector('pre > code[data-lang]:not([data-lang="mermaid"])')
},
undefined,
{ timeout: 5_000 },
)
await frames()
}
// Stories to skip from visual regression (add IDs here if needed)
// Spinner animation captures at an indeterminate frame, causing flaky diffs.
// Permission dock config-preloaded has non-deterministic toggle rendering.
// Sandboxing rows can settle at different scroll heights after settings context updates.
// Side terminal tabs mount live xterm instances whose websocket error text
// lands at indeterminate times.
// The session-dock stability story exists to measure geometry across the
// working/idle swap and carries a debug toggle button, so it is not a meaningful
// appearance baseline.
const SKIP = new Set<string>([
"chat--chat-view-session-dock-stability",
"agentmanager--worktree-item-busy",
"agentmanager--full-screen-diff-agent-edit-scroll",
"agentmanager--side-terminal-panel-tabs",
"composite-webview--permission-dock-config-preloaded",
"settings--sandboxing-allowlist",
"settings--sandboxing-panel",
])
const DOCS = new Map<string, string[]>([
[
"chat--task-header-with-todos",
[
"packages/kilo-docs/pages/code-with-ai/features/task-todo-list.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-todos-chromium-linux.png",
],
],
[
"composite-webview--todo-write-docs-overview",
[
"packages/kilo-docs/pages/code-with-ai/features/task-todo-list.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/todo-write-docs-overview-chromium-linux.png",
],
],
[
"settings--agent-behaviour-workflows",
[
"packages/kilo-docs/pages/customize/workflows.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-workflows-chromium-linux.png",
],
],
])
// Generate one test() per story so Playwright's scheduler can distribute
// them freely across workers — no manual sharding needed.
// Skip fetching stories on macOS since test.skip() above already marks the file skipped.
const stories = IS_DARWIN ? [] : (await fetchStories()).filter((s) => !SKIP.has(s.id))
for (const story of stories) {
test(`${story.title} / ${story.name}`, async ({ page }) => {
for (const ref of DOCS.get(story.id) ?? []) {
test.info().annotations.push({ type: "docs", description: ref })
}
// Width-suffixed stories cover layouts outside the default sidebar viewport.
const width = story.id.endsWith("-200") ? 200 : story.id.endsWith("-1280") ? 1280 : 420
await page.setViewportSize({ width, height: 720 })
await page.goto(
`/iframe.html?id=${story.id}&viewMode=story&globals=colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern`,
{ waitUntil: "load" },
)
await disableAnimations(page)
await page.waitForSelector("#storybook-root *", { state: "attached" })
await settle(page)
const [component, variant] = story.id.split("--")
const root = page.locator("#storybook-root")
await expect(root).toHaveScreenshot(["visual-regression", component!, `${variant!}-chromium-linux.png`])
})
}