Files
kilocode/packages/kilo-vscode/tests/visual-regression.spec.mts
T
Marius 23039c0fb1 feat(agent-manager): support multiple side-panel terminals (#12633)
* feat(agent-manager): support multiple side-panel terminals

Add a terminal tab strip to the side panel header so a context can own
several side terminals: click to switch, drag to reorder, X to close a
single terminal, + to open another one. The strip reuses the top tab
bar's TerminalTabChrome and solid-dnd stack (with a width-pinned
DragOverlay so drags track the cursor without offset).

Terminal numbers fill gaps left by closed terminals, and tabs pick up
live titles from OSC escape codes so the shell or running program names
its own tab.

Closes #12597

* fix(agent-manager): address side terminal strip review findings

Split the strip into a scrollable tab list and a fixed add-button area
(mirroring .am-tab-list-wrap / .am-tab-add-wrap) so the + action never
scrolls away, and scope role=tablist to the tab list so axe
aria-required-children passes on the empty strip.

Validate closeSide targets a live side terminal before mutating state,
so a stray non-side id can no longer drop a record while leaking its
backend PTY.

Give SortableTabContainer a class prop and reuse it for side tabs
instead of a duplicated sortable wrapper.
2026-07-29 09:06:09 +00:00

138 lines
4.8 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.
const SKIP = new Set<string>([
"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`])
})
}