Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 53 additions & 23 deletions contributor-docs/testing.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'd be worth rolling this into a testing skill similar to what we do for style-guide or not really?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm happy to try it out in a follow up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshblack curious what you think 👀 #8572

Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents

* [Strategy](#strategy)
* [Unit Tests](#unit-tests)
* [Overview](#overview)
* [Running Tests](#running-tests)
* [Visual Regression Tests](#visual-regression-tests)
* [Overview](#overview)
* [Prerequisites](#prerequisites)
* [Components](#components)
* [Visual Regression Testing](#visual-regression-testing)
* [Accessibility Verification Testing](#accessibility-verification-testing)
* [Interaction Tests](#interaction-tests)
* [As A Part Of Unit Tests](#as-a-part-of-unit-tests)
* [Storybook Interaction Tests](#storybook-interaction-tests)
* [Continuous Integration](#continuous-integration)
* [FAQ](#faq)
* [Why am I seeing `browserType.launch: Executable doesn't exist at ../path`?](#why-am-i-seeing-browsertypelaunch-executable-doesnt-exist-at-path)
- [Strategy](#strategy)
- [Unit Tests](#unit-tests)
- [Overview](#overview)
- [Browser locator pilot](#browser-locator-pilot)
- [Snapshots](#snapshots)
- [Running Tests](#running-tests)
- [Interaction Tests](#interaction-tests)
- [As A Part Of Unit Tests](#as-a-part-of-unit-tests)
- [Visual Regression Tests](#visual-regression-tests)
- [Prerequisites](#prerequisites)
- [Components](#components)
- [Visual Regression Testing](#visual-regression-testing)
- [Accessibility Verification Testing](#accessibility-verification-testing)
- [Continuous Integration](#continuous-integration)
- [FAQ](#faq)
- [Why am I seeing `browserType.launch: Executable doesn't exist at ../path`?](#why-am-i-seeing-browsertypelaunch-executable-doesnt-exist-at-path)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- prettier-ignore-end -->
Expand Down Expand Up @@ -74,6 +74,42 @@ To make assertions about the elements we use [Vitest](https://vitest.dev/) and [

\*: Read about the [differences between `fireEvent` and `UserEvent`](https://testing-library.com/docs/user-event/intro/#differences-from-fireevent).

### Browser locator pilot

The Button and TreeView suites pilot [Vitest Browser locators](https://vitest.dev/api/browser/locators.html) and browser-backed interactions. Component tests already run in Chromium through Vitest's Playwright provider; this pilot changes the test APIs, not the runner or browser environment.

Keep RTL for `render`, `rerender`, `unmount`, `renderHook`, cleanup, and explicit React `act` boundaries. Import browser queries and interactions from `vitest/browser`:

```tsx
import {render} from '@testing-library/react'
import {expect, it, vi} from 'vitest'
import {page, userEvent} from 'vitest/browser'
import {Button} from '@primer/react'

it('calls onClick', async () => {
const onClick = vi.fn()
render(<Button onClick={onClick}>Submit</Button>)

const button = page.getByRole('button', {name: 'Submit', exact: true})
await expect.element(button).toBeVisible()
await userEvent.click(button)
expect(onClick).toHaveBeenCalledTimes(1)
})
```

Use `expect.element` for eventual DOM assertions and `expect.poll` for eventual computed values, such as live-region messages. Keep ordinary `expect` for synchronous contracts. Prefer role and accessible-name queries, but don't change what a test asserts just to remove a DOM lookup:

- Locators resolve again when used. An expanded TreeView can make a previously unique partial name match several items. Use an exact name or scope the locator; don't hide ambiguity with `.first()`.
- DOM presence and visibility are different contracts. Button's loading announcement is checked by its exact ID and text because the spinner also contains "Loading".
- Keep intentional synthetic events for disabled-click suppression, event cancellation, and specific event payloads. Browser actions wait for actionable elements and don't expose the result of `dispatchEvent`.
- Keep the pointer-only TreeView chevron's programmatic `.click()` exception inside `act`; browser pointer focus on it emits a known focus-zone warning.
- Await browser interactions directly, without wrapping them in `act`. The browser setup leaves React's act environment to RTL instead of forcing `IS_REACT_ACT_ENVIRONMENT` on for the whole suite.
- Keep programmatic focus in `act` when testing focus redirection. Preserve fake timers and synchronous assertions after explicit timer advancement.
- Import `act` from `@testing-library/react` for manual updates, including focus, timers, hooks, and hydration. RTL scopes the act environment to that operation and restores it afterward.
- Use `.element()` for snapshots, computed styles, structural checks, or capturing a particular node before it unmounts. A locator follows current DOM state; it doesn't retain the identity of a detached node.

The pilot leaves shared helpers, Node/SSR tests, and the separate Playwright visual and accessibility suites unchanged. It doesn't require removing RTL dependencies.

### Snapshots

We are slowly moving away from using snapshots as a way to test visual changes of our components. You can read more about the decision in this [ADR](https://github.com/primer/react/blob/main/contributor-docs/adrs/adr-011-snapshot-tests.md). We are in the process of migrating our existing snapshot tests to use [Playwright](https://playwright.dev/) for visual regression testing. If you are writing a new test and you need to test the visual changes of the component, please refer to the [Visual Regression Tests](#visual-regression-tests) section.
Expand All @@ -94,13 +130,7 @@ By default, Vitest fails when tests emit unexpected console output in CI. For lo

We write our unit tests from a user perspective rather than focusing on implementation details. Simulating events to test user interactions is a core part of our testing practice.

We write user interaction tests leveraging [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/)’s [userEvent](<[userEvent](https://testing-library.com/docs/user-event/intro/)>) testing helper.

### Storybook Interaction Tests

We use [Storybook interactions tests](https://storybook.js.org/docs/react/writing-tests/interaction-testing) to simulate and test some complex user interactions. They are particularly useful for cases where writing unit tests is not practical due to the limitation of the mock browser functionalities of JSDOM. For example testing out a component's overflow behaviour whose responsiveness is managed by its own dynamic width.

Storybook tests are authored within the components's source directory with the file name of `interactions.stories.tsx`
For browser-backed interactions, use locators and `userEvent` from `vitest/browser`, keeping RTL for rendering and lifecycle helpers. Follow the [browser locator pilot](#browser-locator-pilot) for examples and guidance.

## Visual Regression Tests

Expand Down
4 changes: 0 additions & 4 deletions packages/react/config/vitest/browser/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ afterEach(() => {
cleanup()
})

// @ts-expect-error this is needed for act() from React
// @see https://react.dev/reference/react/act#error-the-current-testing-environment-is-not-configured-to-support-act
globalThis.IS_REACT_ACT_ENVIRONMENT = true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this override lets Button and TreeView tests await browser interactions directly. The eight test suites unrelated to those (DataTableGrouping, UnderlinePanels, etc) still perform manual timer, hydration, or observer updates. RTL’s act enables the environment for those operations and restores it afterward, whereas React’s raw act doesn’t.


// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
document.documentElement.setAttribute('data-color-mode', 'auto')
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
Expand Down
Loading
Loading