* feat(devex): component fixtures with headless screenshots for fast UI checks Colocated *.fixture.tsx files export named fixtures (render + optional store seed, setup, interact, width). A dev-only Vite entry (vite.fixtures.config.ts, extending vite.web.config.ts) renders one fixture in isolation with the real I18nProvider, TooltipProvider, theme class, default-seeded store, and the web client's preload bridge (installWebPreloadApi) as window.api. `pnpm fixtures:shot <id|glob> [--theme dark|light|both]` starts or reuses a detached server (Vite + one headless Chromium) and writes PNGs to .cache/fixtures/. A warm page re-renders in place (~100ms/shot) and reloads only after source changes. A stdio MCP server (registered in .mcp.json) exposes list_fixtures and screenshot_fixture. Fixtures load only through import.meta.glob in the fixture entry, which is not a production build input; oxlint rejects static imports of *.fixture. * Keep fixture config and agent config inside reviewed root entries * Baseline the fixture page's default-settings seed in the focus-read ratchet * docs(agents): shorten fixtures pointer * Allowlist the dev-only fixture explorer's labels in localization coverage
3.6 KiB
Component Fixtures
Render one renderer component in isolation and screenshot it headlessly in about a second, without booting Electron. Use it as the fast verify loop for presentational UI changes. Use the $electron skill when the change depends on real app state, IPC, terminals, or window chrome.
Writing a fixture
Put a *.fixture.tsx file next to the component. Each named export built with defineFixture is one fixture:
import { defineFixture } from '../../component-fixtures/component-fixture'
import { DeleteWorktreeDirtyChangeHint } from './DeleteWorktreeDirtyChangeHint'
export const dirty = defineFixture({
width: 360,
render: () => <DeleteWorktreeDirtyChangeHint changeCount={3} checkState="checking" />
})
Optional fields:
store: partialAppStatemerged into the store before render. The store starts from slice defaults plusgetDefaultSettings().setup(): runs before render, for example to override awindow.apimethod.interact(frame): runs after first paint, for example clicking a trigger so an open popover is captured. Content portaled to<body>(popovers, menus, dialogs) is included in the screenshot.width: frame width in CSS pixels. The height always fits the content.
Fixture ids are the file path under src/renderer/src/ with components/ and .fixture.tsx removed, followed by the export name. For example, sidebar/DeleteWorktreeDirtyChangeHint:dirty.
Every fixture renders inside I18nProvider, TooltipProvider, and the theme class. window.api is the web client's preload bridge (installWebPreloadApi) with no runtime paired. Calls that need a runtime reject, and namespaces without a web implementation return the web fallback defaults. The web-client flag is cleared, so components render their desktop branches.
Fixtures are dev-only. They load through import.meta.glob in the fixture explorer, which is not a production build input. Oxlint rejects static imports of *.fixture modules.
Screenshots
pnpm fixtures:shot sidebar/DeleteWorktreeDirtyChangeHint:dirty # one fixture
pnpm fixtures:shot sidebar/DeleteWorktreeDirtyChangeHint --theme both # every fixture in a file, both themes
pnpm fixtures:shot 'ui/*' --scale 2 # glob, at 2x
pnpm fixtures:shot --list
pnpm fixtures:shot --stop
PNGs go to .cache/fixtures/ (override with --out), and the command prints their paths. Read them with your image viewer tool. Render errors and page console errors are printed for each fixture, and any failure makes the command exit non-zero.
The first call starts a detached server: Vite (config/vite.fixtures.config.ts, which extends vite.web.config.ts) plus one headless Chromium. The first call takes 10–20 seconds. After that, each shot takes about 100 ms. The first shot after a source edit takes 1–2 seconds because the page reloads. The server restarts itself when the fixture scripts change and exits after 20 idle minutes.
Chromium resolution order: ORCA_FIXTURES_BROWSER (an executable path), then the Playwright-managed chromium-headless-shell, then an installed Chrome or Edge. Every option runs headless, so screenshots never open a window or take focus.
Browsing
pnpm dev:fixtures serves the explorer for humans. Open /fixtures for the index, or /fixtures?id=sidebar/DeleteWorktreeDirtyChangeHint:dirty&theme=light for a single fixture.
MCP
.mcp.json registers orca-component-fixtures, a stdio server at config/scripts/component-fixtures/fixtures-mcp-server.mjs. It has two tools:
list_fixtures({ filter? })screenshot_fixture({ id, theme?, scale? }): returns the PNGs inline (up to 6) along with their paths.