mount fixture returns Locator for gallery root
The mount(storyId, props?) fixture from @playwright/test drives the gallery's window.mount and returns a Locator for the gallery root (#root). Queries should be scoped from this returned locator, for example: component.getByRole('button').click() rather than component.click().
mount fixture passes serializable props to story
The mount(storyId, props?) fixture passes plain serializable props as the second argument. These props are handed by the gallery to the story as its props. Callbacks belong inside the story, not in props.
mount fixture signature and type checking
The mount fixture is generic over the story type: pass the story type as a template argument to type-check the props. Example: const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' }). This works for React and Vue stories.
component.update() re-renders story with new props without remounting
Call component.update(newProps) to test how a component reacts to a prop change without remounting. This re-renders the same story with new props on the existing root, preserving state. This requires the gallery to reuse its root/instance.
Story files location and naming convention
Stories live next to the component in *.story.tsx (or .ts/.jsx/.js/.vue) files; each named export is one story. Story id is the path under src/ without the .story.* extension, plus the export name. Example: src/components/Button.story.tsx export Primary → components/Button/Primary. A .story.vue single-file component is one story addressable by its path alone (its default export).
Gallery implementation requirements
The gallery is a single page that exposes window.mount(params) and window.unmount() to render a story into #root. It resolves stories from story files (e.g. with import.meta.glob) and is framework-specific and owned by the user — there is no template to copy. The mount fixture navigates to baseURL to access the gallery.
Playwright config for component testing with Vite app
For Vite apps, add a project to playwright.config.ts with: name: 'components', testDir: './tests/components', use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true }. Add webServer with command: 'npm run dev', url: 'http://localhost:5173/playwright/gallery/index.html', reuseExistingServer: !process.env.CI. The gallery is served at /playwright/gallery/index.html by the existing dev server.
Playwright config for component testing with non-Vite apps
For non-Vite apps (Next.js, webpack, no dev server), run a small standalone dev server (e.g. Vite) to serve the gallery page and point baseURL at it. This requires vite and the framework plugin as devDependencies. The webServer command might be: npx vite --config playwright/vite.config.ts with url pointing to the standalone server (e.g. http://localhost:3100/playwright/gallery/index.html).
serviceWorkers: 'block' configuration purpose
Setting serviceWorkers: 'block' in the Playwright config keeps the app's own service worker from serving cached responses that would shadow page.route() mocks used in tests.
reuseContext: true configuration purpose
Setting reuseContext: true in the Playwright config reuses the browser context across tests in a worker, providing a large speedup for component suites similar to the old component-testing runtime.
Recording component state for test assertion
Where a component takes callbacks, the story creates the state, provides the callbacks, and records the state into a hidden form for the test to assert on. Record each observed value in its own data-testid input using String(...) or JSON.stringify(...) for payloads, then assert with toHaveValue() — a web-first assertion that retries until the state lands.
Test callbacks and events example
Example story recording expanded state: export const Stateful = () => { const [expanded, setExpanded] = useState(false); return <><Expandable expanded={expanded} setExpanded={setExpanded} title="Title">Details</Expandable><form hidden><input data-testid="expanded" readOnly value={String(expanded)} /></form></>; }. Example test: const component = await mount('components/Expandable/Stateful'); await component.locator('.codicon-chevron-right').click(); await expect(component.getByTestId('expanded')).toHaveValue('true');
Per-test props example
Example story with props: export const WithTitle = ({ title = 'Default' }: { title?: string }) => <Button title={title} />. Example test: const component = await mount('components/Button/WithTitle', { title: 'Hello' });
Prop transitions with update() example
Example testing prop changes without remounting: const component = await mount('components/Counter/Default', { value: 1 }); await expect(component.getByTestId('value')).toHaveText('1'); await component.update({ value: 2 }); await expect(component.getByTestId('value')).toHaveText('2');
Multiple stories in one test example
Each mount() navigates fresh for full isolation; mounting several stories in one test is cheap. Example: await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png'); await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');
Network mocking in component tests
Use page.route() as usual for network mocking — register routes before mount(), since mounting navigates. serviceWorkers: 'block' (set in config) keeps the app's own service worker from serving cached responses that shadow the routes.
Debugging stories from browser devtools
Open the gallery URL (baseURL) in a browser and call await window.mount({ story: 'components/Button/Primary' }) from the devtools console — this is exactly what the mount fixture does. An unknown story rejects window.mount, which surfaces as the test's mount() throwing with a real stack.