Skip to content

Browser mode: unify into the pool/run() finalize path (remove out-of-run() coverage generation) #1388

Description

@fi3ework

Background

Browser mode currently finalizes a run outside the unified node run() on two code paths in packages/core/src/core/runTests.ts:

  1. The browser-only fast path (hasBrowserProjects && !hasNodeProjects) — generates coverage inline, separate from run().
  2. The mixed browser + node early-return path (!hasNodeTestsToRun) — when a node project is configured but resolves to zero test files, control returns before run() is ever invoked.

Coverage report generation (generateCoverage = write reports + enforce thresholds) lives only inside run(). So whenever the browser self-finalizes without run(), that responsibility is orphaned and has to be re-homed with a browser-specific "generate coverage outside run()" branch.

This is the underlying reason a generateBrowserOnlyCoverage-style helper keeps wanting to exist. It is a symptom, not the root: browser mode is not yet isomorphic with the node pools (forks / threads), so it cannot simply flow through the same run() finalize path.

Deferred gap from #1363 / PR #1386

PR #1386 fixes the hang for a mixed browser + node config whose node project matches zero test files (the browser teardown was deferred to a run() that never happened).

It intentionally does not fix coverage for that same path. In that configuration, the browser project's coverage report is currently not generated (the empty node sibling suppresses it). Fixing it today would require adding another out-of-run() coverage call, which entrenches the anti-pattern above. We chose to defer it to the isomorphism work instead.

Repro shape (mixed config, coverage enabled, node project matches no files):

// rstest.config.mts
export default defineConfig({
  coverage: { enabled: true, include: ['src/**/*.ts'] },
  projects: [
    { name: 'browser', browser: { enabled: true, provider: 'playwright', headless: true }, include: ['tests/sum.test.ts'] },
    { name: 'node', include: ['tests/node-empty/**/*.test.ts'] }, // matches 0 files
  ],
});

Expected: a coverage report is written for the browser project. Actual: no report (no run() → no generateCoverage).

Related inconsistencies the unification should also reconcile

  • Failure guard divergence (verify): the browser-only coverage branch guards only on !browserResult.unhandledErrors?.length, whereas run() guards on !isFailure || coverage.reportOnFailure. The browser path appears not to honor reportOnFailure or test-level failures — worth confirming and aligning.
  • generateCoverage project scope: untested-file instrumentation iterates context.projects (all configured), but the swc transform is registered per-environment only when that environment is built (coverage-istanbul/src/plugin.ts, keyed by environment.name). Iterating a configured-but-unbuilt project throws swc transform function for <env> is not registered. The correct contract is "iterate only the projects built this round," which is latent even for pure-node multi-project configs with an empty sibling.

Desired end state

Make browser mode a first-class pool type (like forks / threads) that flows through the unified run(). Then coverage generation, reporter finalize, and teardown have a single home, the out-of-run() browser coverage branch disappears, and the deferred #1363 coverage gap is resolved for free.

Links

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions