playwright-component-testing

作者: microsoft

使用故事畫廊搭配Playwright設定元件測試——透過內建的mount fixture搭建故事與畫廊開發頁面,無需專屬…

npx skills add https://github.com/microsoft/playwright --skill playwright-component-testing

Component Testing with Playwright

Test components with regular Playwright e2e tests against a small story gallery page hosted by the app's own dev server. No extra test runner, bundler integration or npm packages are required.

Concept

  • A story is a tiny wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories live next to the component in *.story.tsx (or .ts/.jsx/.js/.vue) files; each named export is one story.
  • The gallery is a single page you implement to references/gallery-spec.md: it exposes window.mount(params) / window.unmount() that render a story — resolved from your story files (e.g. with import.meta.glob) — into #root. It is framework-specific and yours to own — there is no template to copy for it.
  • Tests are plain Playwright tests. The built-in mount(storyId, props?) fixture (from @playwright/test) drives the gallery's window.mount and returns a Locator for the gallery root (#root). Scope the queries from there — component.getByRole('button').click(), not component.click(). Nothing to scaffold for it.

Everything the component needs must be set up inside the story (it runs in the browser); everything the test asserts must be observable through the page (DOM, URL, network). Where the 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. mount(id, props) passes plain serializable props to the story.

Setup workflow

  1. Detect the framework and bundler. React vs Vue decides the framework notes and example story to follow. Then:

    • App runs on Vite (has vite.config.*): the gallery is served by the existing dev server at /playwright/gallery/index.html — Vite serves any .html file under the project root, the app's plugins/aliases/CSS apply automatically, and vite build ignores it. No extra server needed.
    • Anything else (Next.js, webpack, no dev server): run a small standalone dev server (e.g. Vite) that serves the gallery page, and point baseURL at it. Requires vite and the framework plugin as devDependencies.
  2. Implement the gallery to references/gallery-spec.md: a page at <project>/playwright/gallery/ that renders the requested story into #root. Start from the worked example in the spec and the framework notes in references/react.md / references/vue.md. Keep story discovery (import.meta.glob) and the framework mount here — this is the only framework-specific glue, so keep it small. Import the app's global CSS the same way the app's own entry does.

  3. Configure Playwright — add to playwright.config.ts:

    projects: [
      {
        name: 'components',
        testDir: './tests/components',
        use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true },
      },
    ],
    webServer: {
      command: 'npm run dev',                                       // or: npx vite --config playwright/vite.config.ts
      url: 'http://localhost:5173/playwright/gallery/index.html',   // standalone server: http://localhost:3100/playwright/gallery/index.html
      reuseExistingServer: !process.env.CI,
    },
    

    Match the port to the dev server. mount navigates to baseURL, so set baseURL to the gallery's URL. serviceWorkers: 'block' keeps the app's own service worker from serving cached responses that would shadow your page.route() mocks. reuseContext: true reuses the browser context across tests in a worker (as the old component-testing runtime did) — a large speedup for component suites. If the config already has projects/webServer, merge instead of replacing.

  4. Write a first story next to an existing component, modeled on templates/<react|vue>/Button.story.*.

  5. Write a first spec, modeled on templates/react/button.spec.ts, importing test/expect from @playwright/test.

  6. Run: npx playwright test --project=components. Open http://localhost:5173/playwright/gallery/index.html in a browser to eyeball all stories.

Conventions

  • Story id: path under src/ without the .story.* extension, plus the export name — src/components/Button.story.tsx export Primarycomponents/Button/Primary. Any unique suffix works too: mount('Button/Primary'). A .story.vue single-file component is one story, addressable by its path alone (its default export). With gallery types (references/typing.md) ids are prefixed with the package name: acme-ui/components/Button/Primary.
  • One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states.

Testing patterns

Examples are React; the Vue equivalents differ only in story syntax.

Callbacks and events

The story owns the state and provides the callbacks. Where the component takes callbacks, create the state inside the story, wire the callbacks to it, and record the state into a hidden form next to the component. Tests perform operations and assert on the recorded values:

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>
  </>;
};
test('click should expand', async ({ mount }) => {
  const component = await mount('components/Expandable/Stateful');
  await component.locator('.codicon-chevron-right').click();
  await expect(component.getByTestId('expanded')).toHaveValue('true');
});

This keeps the whole scenario in the browser: no callback marshalling, the story doubles as documentation, and the recorded state is visible when eyeballing the gallery. Record each observed value in its own data-testid input (String(...) or JSON.stringify(...) for payloads) and assert with toHaveValue() — a web-first assertion that retries until the state lands. The negative direction works the same way: perform the operation, then assert the value did not change.

Per-test props

When a scenario is genuinely parametric (e.g. a boundary-value sweep), pass props as the second argument to mount; the gallery hands them to the story as its props. Keep props to plain serializable data — callbacks belong inside the story.

export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
  <Button title={title} />;
const component = await mount('components/Button/WithTitle', { title: 'Hello' });

Props are type-checked in two optional ways, see references/typing.md: pass the story type as a template argument (mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' }), no setup), or generate gallery types with a small Vite plugin so the id itself is typed (mount('acme-ui/components/Button/WithTitle', { title: 'Hello' }), with autocomplete and rename safety). Vue stories must additionally declare the props at runtime — see the Typed props sections in references/react.md / references/vue.md.

Prop transitions with update()

To test how a component reacts to a prop change without remounting (state preserved), call component.update(newProps) — it re-renders the same story with new props on the existing root:

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');

This requires the gallery to reuse its root/instance (references/gallery-spec.md); state survives as long as the story stays the same.

Multiple states in one test

Each mount() navigates fresh, so tests are fully isolated and mounting several stories in one test is cheap:

await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');

For visual comparison, screenshot the returned root locator (as above), not the page, to avoid asserting on browser chrome.

Network mocking

Use page.route() as usual — register routes before mount(), since mounting navigates. serviceWorkers: 'block' (set in the config above) keeps the app's own service worker from serving cached responses that shadow the routes. Teams with MSW handler libraries can start the worker inside a story or decorator instead.

Debugging stories

Open your gallery URL (baseURL) in a browser and call await window.mount({ story: 'components/Button/Primary' }) from the devtools console — that 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. To browse without the console, give your gallery an optional index page.

Decision points

  • Monorepos / non-src layouts: change the glob and the id derivation in your gallery (references/gallery-spec.md) to match, and prefix ids with the package name (references/typing.md).
  • Global providers (theme, i18n, store, router): create a shared decorator helper next to the gallery and wrap components in stories; see references/react.md / references/vue.md.

References

  • references/gallery-spec.md — the gallery endpoint contract to implement (start here).
  • references/typing.md — optional typing for mount: explicit story types vs generated gallery types, with the Vite plugin.
  • references/react.md — React walkthrough: providers, StrictMode, CSS.
  • references/vue.md — Vue walkthrough: .story.ts and .story.vue stories, plugins.
  • references/migration.md — migrating off @playwright/experimental-ct-react / -vue.

來自 microsoft 的更多技能

oss-growth
microsoft
開源增長駭客角色
agent-framework-azure-ai-py
microsoft
使用Microsoft Agent Framework Python SDK(agent-framework-azure-ai)构建Azure AI Foundry代理。适用于使用AzureAIAgentsProvider创建持久化代理、使用托管工具(代码解释器、文件搜索、网络搜索)、集成MCP服务器、管理对话线程或实现流式响应。涵盖函数工具、结构化输出和多工具代理。
development
airunway-aks-setup
microsoft
在AKS上設定AI Runway——從裸叢集到執行模型。涵蓋叢集驗證、控制器安裝、GPU評估、供應商設定及首次部署。時機:「設定AI Runway」、「上線AKS叢集」、「安裝AI Runway」、「airunway設定」、「部署模型至AKS」、「在AKS上進行GPU推論」、「在AKS上設定KAITO」、「在AKS上執行LLM」、「在AKS上使用vLLM」、「在AKS上設定模型服務」、「AI Runway控制器」。
devops
appinsights-instrumentation
microsoft
使用Azure Application Insights檢測Web應用程式的指南。提供遙測模式、SDK設定與組態參考。適用時機:如何檢測應用程式、App Insights SDK、遙測模式、什麼是App Insights、Application Insights指南、檢測範例、APM最佳實踐。
devops
applicationinsights-web-ts
microsoft
使用Application Insights JavaScript SDK(@microsoft/applicationinsights-web)為瀏覽器/Web應用程式進行檢測。適用於真實使用者監控(RUM)——頁面檢視、點擊、AJAX/fetch依賴、例外、自訂事件,以及與後端OpenTelemetry追蹤關聯的瀏覽器端GenAI代理追蹤。涵蓋SDK載入器指令碼與npm設定、框架擴充(React、React Native、Angular)、點擊分析、遙測初始化器,以及從瀏覽器發出的代理/工具/模型span的OTel GenAI語意慣例。
devops
azure-ai-anomalydetector-java
microsoft
使用適用於 Java 的 Azure AI 異常偵測器 SDK 建置異常偵測應用程式。在實作單變量/多變量異常偵測、時間序列分析或 AI 驅動監控時使用。
development
azure-ai-language-conversations-py
microsoft
使用 azure-ai-language-conversations Python SDK 實作對話語言理解(CLU)。當使用 ConversationAnalysisClient 分析對話意圖與實體、建置 NLP 功能,或將語言理解整合至應用程式時使用。
development
azure-ai-ml-py
microsoft
Azure Machine Learning SDK v2 for Python。用於機器學習工作區、作業、模型、資料集、計算資源與管線。 觸發詞:「azure-ai-ml」、「MLClient」、「workspace」、「model registry」、「training jobs」、「datasets」。
development