# Testing

> Testing workflow for the Rstest monorepo. Use when running unit tests, e2e tests, browser e2e, example tests, watch-mode checks, writing test fixtures, debugging local or CI test failures, validating code changes, or reproducing bugs from external projects.

- Skill: `web-infra-dev/testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add web-infra-dev/testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/web-infra-dev/testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: web-infra-dev (https://skillmd.com/u/web-infra-dev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/web-infra-dev/testing

---


# Testing Workflow

This skill owns the **mechanics**: how to run and write tests, fixtures, and builds. Whether a change needs test work is routed by the `development` skill; what counts as evidence that a change works is defined by the `verify` skill.

## Running tests

### Package/unit tests from repository root (single file — preferred)

```bash
pnpm rstest packages/core/tests/core/rsbuild.test.ts
```

Use this form for tests discovered by the root workspace config.
Do **not** pass `e2e/...` paths to the root `pnpm rstest` command.

### Package-level tests

```bash
pnpm --filter @rstest/core test
pnpm --filter @rstest/core test -- tests/core/rsbuild.test.ts  # single file
```

### E2E tests

Run e2e tests from inside the `e2e/` directory.
When passing the test path, strip the `e2e/` prefix because the working directory is already `e2e/`.

```bash
cd e2e && pnpm test <path-to-test>
```

To run tests in a fixture directory directly:

```bash
cd e2e/<test>/fixtures/<fixture>/ && npx rstest
```

## Rebuild before E2E

E2E tests and examples execute against **built package output**, not TypeScript sources. If you changed package source code, you **must rebuild** before running e2e:

```bash
pnpm --filter @rstest/core build    # or whichever package was changed
cd e2e && pnpm test <path>
```

Forgetting this step means e2e runs against stale output — a common source of false passes/failures.

When the change may affect multiple packages, prefer a full workspace package build first:

```bash
pnpm build
cd e2e && pnpm test <path>
```

Important:

- Do **not** start e2e while any package build is still running.
- Do **not** overlap `pnpm build` and `pnpm e2e` in separate sessions.
- Wait for the build command to exit successfully before starting e2e.
- If e2e fails immediately with a missing built file such as `packages/core/dist/rstestSuppressWarnings.cjs`, treat that as an incomplete build and rebuild before retrying.
- When unsure whether the build is fully finished, verify the expected artifact exists before running e2e.

## Browser E2E

- Fixtures default to `headless: true` — no browser windows locally
- Headed smoke tests are skipped locally by default (CI only)
- To opt in locally: `cd e2e && RSTEST_E2E_RUN_HEADED=true pnpm test browser-mode/basic.test.ts`

### Browser E2E debugging

- Rebuild affected packages before retrying; stale `dist` is a common false signal.
- Separate host/protocol/provider issues from UI issues before editing `@rstest/browser-ui`.
- For flakes, check shared ports/cwd, persistent `dist/.rstest-temp/`, unawaited events, and order-dependent state before increasing timeouts.

## Watch-mode and long-running tests

- Use watch-mode tests only for file-change/rerun/invalidation behavior.
- Assert stable events/final output, clean up watchers/processes, and inspect open handles before changing production code for hangs.

## Test behavior, not source shape

Protect runtime invariants at the narrowest existing interface that exposes their observable result. Use integration or E2E coverage for cross-module orchestration. Do not add a production seam or abstraction solely to make an isolated unit test possible; use fakes or spies only when the code already has a natural interface for them.

- Treat first-party implementation source text as private. Do not add tests that read it and use regexes, strings, snapshots, or counts to pin helper names, call sites, imports, or control-flow shape; comments, formatting, and behavior-preserving refactors make those assertions lie.
- Reading source is appropriate when that source is itself the runtime input under test, such as a raw injected module or a transform fixture. Inspecting emitted bundles and generated files as product outputs is also appropriate.

## Unit tests are OS-agnostic

CI runs unit tests (the `ut` job) on ubuntu only; OS-specific coverage lives in the e2e job's macOS/Windows rows. Enforced by the `rstest/os-agnostic-tests` rule in `rslint.config.mts` as part of `pnpm lint`; the rule itself is unit-tested by `scripts/lint/os-agnostic-rule.test.ts` (in the `lint` project).

- Do not write unit tests whose behavior or expectations depend on the host OS (reading `process.platform`, `os.platform()`, etc.). CI would only ever exercise the Linux branch.
- To cover platform-dependent code paths in a unit test, stub the platform for the test's duration so every branch runs deterministically on any host — see `withPlatform` in `packages/core/tests/core/related.test.ts`.
- If the behavior cannot be stubbed (real filesystem case-sensitivity, native binaries, shell differences), cover it in `e2e/` instead.

## Fixture strategy

Before adding a fixture, list existing ones in the same area (`ls e2e/<area>/fixtures`) and name the closest match. Prefer extending it:

- Adding a project, config flag, or test file is additive reuse — "different config" alone does not justify a new fixture. **After extending, re-run every test using that fixture** to confirm none broke.
- A new fixture is right when reuse would force an **incompatible** change to config other tests depend on, or contort the fixture's intent. Name the mechanism that blocks reuse; config expressible per file (environment docblocks, per-file options) does not make a difference incompatible.
- The same rule applies **inside** a fixture: before adding a test file or helper module, look for one whose structure already matches — same shared module, same peer-file pairing — and extend it instead. Adding exports to an existing helper, or cases to an existing test file, is additive reuse.
- Prefer **one consolidated regression fixture** that exercises the whole surface over many near-duplicate per-feature files. When several cases share a structural root cause, assert them together.

## E2E rstest spawns with persistent `dist/.rstest-temp/`

A fixture that enables `performance.buildCache` (or `dev.writeToDisk: true`) forces rstest to persist the built bundle under `<cwd>/dist/.rstest-temp/`. If sibling test files share that `cwd`, those persistent writes race against any test asserting on the default path. Other disk writers like coverage or blob reporters land in `<cwd>/coverage/` or `<cwd>/.rstest-reports/` — different surfaces, not covered by this rule.

When your fixture enables persistent build output:

- Set `cwd` to the fixture subdirectory, not the parent test directory.
- In that fixture's config, drop any `include` referencing paths outside the fixture (e.g. `'./fixtures/<name>/index.test.ts'`). Fixture-local globs like `'./*.test.ts'` are fine.
- Read `dist/.rstest-temp/` assertions from that isolated fixture dir, not from a shared parent.

## Snapshot policy

- Only update snapshots when the behavioral change is **intentional**
- For package/unit tests from repository root, use `-u` / `--update`: `pnpm rstest -u packages/core/tests/core/rsbuild.test.ts`
- Do **not** use snapshot updates as a default way to silence test failures — investigate first
- When updating, review the snapshot diff to confirm it matches expected changes

## External repro projects

For external repos/fixtures: read README/scripts/deps, reproduce with the smallest command, classify the source, then port only the minimal regression case into this repo.

## Performance and benchmark validation

For performance work, compare like-for-like runs: same command/fixture/env/cache policy, enough samples to separate noise, and state whether the result covers startup, execution, transform/cache, browser startup, or reporter overhead.

## Validation before wrapping up

- Do not stop at targeted tests only. Before finishing a code change, run the narrowest relevant tests first, then decide whether broader validation is needed.
- `pnpm run check-unused` is part of the default validation pass for code changes in this repo. Run it before wrapping up, even when focused tests already pass.
- Treat `check-unused` failures as real regressions unless you have confirmed the reported item is an intentional temporary state.

