Skip to content

Commit 1f8e1df

Browse files
docs(testing): test native dismissal in the Playwright spec, not play functions (#6474)
* docs(testing): document trusted vs synthetic input for native dismissal Storybook play functions using @storybook/test's synthetic userEvent cannot trigger browser-native popover/dialog light-dismiss (Escape, outside/backdrop click), which requires trusted input. Document using @vitest/browser/context's real Playwright-backed userEvent for that path, in the storybook-testing style guide (canonical), the migration-testing skill, and the Phase 6 workflow. * docs(testing): put native-dismissal tests in the playwright spec, not play functions * docs(testing): clarify trusted input event assertions - make the custom-event detail example depend on a documented event contract - clarify that isTrusted checks belong on native input events --------- Co-authored-by: Patrick Fulton <360251+pfulton@users.noreply.github.com>
1 parent e2a7e4e commit 1f8e1df

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

.ai/skills/migration-testing/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ Read the migration plan at `CONTRIBUTOR-DOCS/03_project-planning/03_components/[
3838
Follow **[Phase 6: Testing](../../../CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md#phase-6-testing)** in the washing machine workflow doc — it covers what to do, what to check, common problems, and the quality gate for this phase.
3939

4040
If the implementation or the needed test coverage has drifted from the migration plan, follow [`migration-plan-contract`](../migration-prep/references/migration-plan-contract.md).
41+
42+
## Native dismissal and trusted input
43+
44+
`@storybook/test`'s `userEvent` dispatches synthetic events (`isTrusted === false`), which browser-native `popover`/`<dialog>` light-dismiss (Escape, outside/backdrop click) ignores, so a synthetic Escape does not dismiss a native overlay. Test that native path in the Playwright accessibility spec (`test/<component>.a11y.spec.ts`), which drives real, trusted input, not in a play function. Play-function files (`test/<component>.test.ts`) are indexed as dev Storybook stories, and importing `vitest/browser` there throws outside the Vitest runner, breaking the dev server. Component-authored JavaScript handlers (click-to-toggle, focus, keydown backstops) work with the synthetic `@storybook/test` `userEvent` and stay in play functions. See [Native dismissal and trusted input](../../../CONTRIBUTOR-DOCS/02_style-guide/04_testing/02_storybook-testing.md#native-dismissal-and-trusted-input) for the full pattern.

CONTRIBUTOR-DOCS/02_style-guide/04_testing/02_storybook-testing.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Testing static color variants](#testing-static-color-variants)
3232
- [Testing dev mode warnings](#testing-dev-mode-warnings)
3333
- [Testing composed components](#testing-composed-components)
34+
- [Native dismissal and trusted input](#native-dismissal-and-trusted-input)
3435

3536
</details>
3637

@@ -528,3 +529,58 @@ export const ComposedComponentTest: Story = {
528529
},
529530
};
530531
```
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.

CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/02_step-by-step/01_washing-machine-workflow.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ Follow the two-file layout (`test/<component>.test.ts`, `test/<component>.a11y.s
508508
| Async timing | Use `await nextFrame()` or `element.updateComplete` as needed. |
509509
| Shadow DOM | Use `shadowRoot.querySelector` and the test utils the repo provides. |
510510
| A11y rules too strict | Tune rules in the a11y config if needed; do not disable without team agreement. |
511+
| Native dismissal (Escape, outside/backdrop click) won't fire | `@storybook/test`'s `userEvent` is synthetic (`isTrusted === false`); browser-native `popover`/`<dialog>` light-dismiss ignores it. Test the native path in the Playwright a11y spec (`test/<component>.a11y.spec.ts`) with trusted input, not in a play function (`vitest/browser` throws in the dev Storybook). See [Native dismissal and trusted input](../../../../02_style-guide/04_testing/02_storybook-testing.md#native-dismissal-and-trusted-input). |
511512

512513
### Quality gate
513514

0 commit comments

Comments
 (0)