|
31 | 31 | - [Testing static color variants](#testing-static-color-variants) |
32 | 32 | - [Testing dev mode warnings](#testing-dev-mode-warnings) |
33 | 33 | - [Testing composed components](#testing-composed-components) |
| 34 | + - [Native dismissal and trusted input](#native-dismissal-and-trusted-input) |
34 | 35 |
|
35 | 36 | </details> |
36 | 37 |
|
@@ -528,3 +529,58 @@ export const ComposedComponentTest: Story = { |
528 | 529 | }, |
529 | 530 | }; |
530 | 531 | ``` |
| 532 | + |
| 533 | +### Native dismissal and trusted input |
| 534 | + |
| 535 | +`@storybook/test`'s `userEvent` dispatches synthetic DOM events (`isTrusted === false`). Browser-native `popover` and `<dialog>` light-dismiss behavior (Escape, outside click, dialog backdrop click) only responds to genuine, trusted browser input, so synthetic events never trigger it. Component-authored JavaScript handlers (click-to-toggle, focus, keydown backstops) work fine with synthetic input and stay in play functions. |
| 536 | + |
| 537 | +**Do not import `vitest/browser` (or the older `@vitest/browser/context`) in a play function.** Play-function files (`test/<component>.test.ts`) are indexed as Storybook stories in dev mode, so the dev Storybook server evaluates them too. `vitest/browser` is a virtual module: the Vitest browser runner resolves it to the real Playwright-backed implementation, but everywhere else (including the dev Storybook server) it resolves to a shim that throws on evaluation. A static import crashes the dev server; a lazy import only defers the same crash to when the story's play runs. Either way the story is broken outside the Vitest runner. |
| 538 | + |
| 539 | +**Test native light-dismiss in the Playwright accessibility spec (`test/<component>.a11y.spec.ts`) instead.** Playwright drives real, trusted input (`page.keyboard`, `page.mouse`, `locator.click()`) against the running Storybook, so native dismissal fires. If the scenario is not an existing story, add a **render-only fixture** story (no `play`) to `test/<component>.test.ts` for Playwright to navigate to, and keep the interaction plus assertions in the spec. |
| 540 | + |
| 541 | +```typescript |
| 542 | +// test/<component>.a11y.spec.ts |
| 543 | +import { expect, test } from '@playwright/test'; |
| 544 | + |
| 545 | +import { gotoStory } from '../../../utils/a11y-helpers.js'; |
| 546 | + |
| 547 | +test('Escape closes the popover (native light-dismiss)', async ({ page }) => { |
| 548 | + // gotoStory waits on a visible custom element (an swc-button trigger), not the |
| 549 | + // display:contents popover host, which never reports as visible. |
| 550 | + await gotoStory( |
| 551 | + page, |
| 552 | + 'components-popover-tests--escape-dismiss', |
| 553 | + 'swc-button' |
| 554 | + ); |
| 555 | + const popover = page.locator('swc-popover'); |
| 556 | + |
| 557 | + await page.locator('#dismiss-trigger').click(); |
| 558 | + await expect(popover).toHaveJSProperty('open', true); |
| 559 | + await page.keyboard.press('Escape'); // trusted key press |
| 560 | + await expect(popover).toHaveJSProperty('open', false); |
| 561 | +}); |
| 562 | +``` |
| 563 | + |
| 564 | +To assert a component custom event's detail from Playwright, first confirm that the component's event contract includes that detail. For example, a popover close event documented as `CustomEvent<{ source: string }>` can be captured in the page and read back: |
| 565 | + |
| 566 | +```typescript |
| 567 | +await page.evaluate(() => { |
| 568 | + const popover = document.querySelector('swc-popover'); |
| 569 | + |
| 570 | + if (!popover) { |
| 571 | + throw new Error('swc-popover not found.'); |
| 572 | + } |
| 573 | + |
| 574 | + popover.addEventListener('swc-close', (event) => { |
| 575 | + (window as Window & { __closeSource?: string }).__closeSource = ( |
| 576 | + event as CustomEvent<{ source: string }> |
| 577 | + ).detail.source; |
| 578 | + }); |
| 579 | +}); |
| 580 | +// ... drive the interaction ... |
| 581 | +const source = await page.evaluate( |
| 582 | + () => (window as Window & { __closeSource?: string }).__closeSource |
| 583 | +); |
| 584 | +``` |
| 585 | + |
| 586 | +> A quick way to confirm which input you have: assert `event.isTrusted` on the native event you are testing, such as a `keydown` or `click` listener. `@storybook/test`'s `userEvent` yields `false` (synthetic); Playwright input yields `true` (trusted). Component-dispatched custom events are created by JavaScript and can still report `false`, even when trusted input caused them. |
0 commit comments