--- name: testing description: 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. metadata: internal: true --- # 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 ``` To run tests in a fixture directory directly: ```bash cd e2e//fixtures// && 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 ``` 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 ``` 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//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 `/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 `/coverage/` or `/.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//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.