supercov 0.0.54 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,9 +63,10 @@ one run and preserves runner identity wherever the runner exposes it.
63
63
 
64
64
  Vitest Browser Mode runs the test file in a real browser. Supercov measures it
65
65
  per test like any other Vitest run: lines, branches, MC/DC and assertion
66
- coverage, with no extra configuration. Every provider is supported, because
67
- evidence travels over Vitest's own browser command channel rather than anything
68
- provider-specific.
66
+ coverage. Combined Node/browser projects are configured after Vitest resolves
67
+ inline definitions, config-file references, globs and project selection.
68
+ Evidence travels over Vitest's own browser command channel; the compatibility
69
+ suite exercises Chromium through the Playwright provider.
69
70
 
70
71
  Component code is instrumented the same way as any other source, with one
71
72
  addition that matters most here. A JSX tree is a single statement, so an
@@ -83,6 +84,26 @@ where it is created.
83
84
  `expect.element(...)`, `expect.soft(...)` and `expect.poll(...)` are recognised
84
85
  as assertions, so their passing occurrences are available to assertion maps.
85
86
 
87
+ ### React and React Native
88
+
89
+ React components can use Testing Library with Vitest/jsdom, Vitest Browser Mode,
90
+ or Babel/Jest. React Native and Expo component tests run through their existing
91
+ Jest presets, including native mocks. This is JavaScript component-test coverage:
92
+ it does not measure Hermes, native modules, simulator/device execution, Detox or
93
+ Maestro. React SSR hydration is exercised in jsdom and Chromium; this does not
94
+ establish Next.js Server Components, streaming SSR or server actions.
95
+
96
+ An assertion map can explain which displayed value, accessible name, disabled
97
+ state or error message a test checks. JSX expression coverage and a passing
98
+ assertion are evidence for reviewing that explanation, not automatic semantic
99
+ proof. A button being rendered does not establish that its disabled state was
100
+ checked. Keep your existing runner and matchers.
101
+
102
+ The [React verification example](https://github.com/supercorp-ai/supercov/tree/main/examples/react-verification)
103
+ shows a fully executed checkout whose weak tests accept four UI regressions,
104
+ then adds four precise assertions and independently checks the broken copies.
105
+ The agent-authored maps credit only those four UI expressions.
106
+
86
107
  ### Builds and source formats
87
108
 
88
109
  JavaScript and TypeScript projects may use Vite, Next, Turbopack, Webpack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "Coverage for coding agents and software factories \ud83c\udf19",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,6 +54,7 @@
54
54
  "test:rustc-backend-spike": "node scripts/rustc-backend-spike.mjs",
55
55
  "test:rust-compiler-spikes": "cargo build -p supercov && node scripts/rust-libtest-companion-spike.mjs && node scripts/rust-async-attribution-spike.mjs && node scripts/rust-subprocess-attribution-spike.mjs && node scripts/rust-custom-harness-spike.mjs && node scripts/rust-libtest-builder-lifecycle-spike.mjs",
56
56
  "test": "cargo test --workspace",
57
+ "test:react": "cargo build -p supercov && node scripts/rust-vitest-projects-integration.mjs && npm --prefix examples/react-verification ci && npm --prefix examples/react-verification run demo && node scripts/react-hydration-compatibility.mjs",
57
58
  "test:runtime": "node --test tests/runtime/*.test.mjs",
58
59
  "test:fixture": "cargo build -p supercov && node scripts/rust-fixture-matrix.mjs",
59
60
  "test:packed-npx": "cargo build --release -p supercov && node scripts/packed-npx-integration.mjs",
@@ -97,14 +98,14 @@
97
98
  "test:launcher": "go vet ./cmd/... && go test ./cmd/..."
98
99
  },
99
100
  "optionalDependencies": {
100
- "@supercov/cli-darwin-arm64": "0.0.54",
101
- "@supercov/cli-darwin-x64": "0.0.54",
102
- "@supercov/cli-linux-arm64-gnu": "0.0.54",
103
- "@supercov/cli-linux-arm64-musl": "0.0.54",
104
- "@supercov/cli-linux-x64-gnu": "0.0.54",
105
- "@supercov/cli-linux-x64-musl": "0.0.54",
106
- "@supercov/cli-win32-arm64": "0.0.54",
107
- "@supercov/cli-win32-x64": "0.0.54"
101
+ "@supercov/cli-darwin-arm64": "0.0.55",
102
+ "@supercov/cli-darwin-x64": "0.0.55",
103
+ "@supercov/cli-linux-arm64-gnu": "0.0.55",
104
+ "@supercov/cli-linux-arm64-musl": "0.0.55",
105
+ "@supercov/cli-linux-x64-gnu": "0.0.55",
106
+ "@supercov/cli-linux-x64-musl": "0.0.55",
107
+ "@supercov/cli-win32-arm64": "0.0.55",
108
+ "@supercov/cli-win32-x64": "0.0.55"
108
109
  },
109
110
  "peerDependencies": {
110
111
  "@playwright/test": ">=1.55.0",
@@ -126,6 +127,7 @@
126
127
  "@playwright/test": "1.62.1",
127
128
  "@swc/core": "^1.16.1",
128
129
  "@types/node": "^24.0.0",
130
+ "@vitest/browser-playwright": "4.1.11",
129
131
  "esbuild": "^0.28.2",
130
132
  "expect": "30.4.1",
131
133
  "jest": "30.5.1",
@@ -28,8 +28,32 @@ function localFile(path) {
28
28
  return relative(process.cwd(), path).split(sep).join("/");
29
29
  }
30
30
  // Must match jestReporter.mjs: the reporter has the same file and full name.
31
- function testIdentity(testFile, fullName) {
32
- return `jest:${digest(`${testFile}\0${fullName}`)}`;
31
+ function testIdentity(testFile, fullName, occurrence = 0) {
32
+ return `jest:${digest(`${testFile}\0${fullName}${occurrence ? `\0${occurrence}` : ""}`)}`;
33
+ }
34
+ // Circus keeps its declaration tree on the environment global. It distinguishes
35
+ // identical titles and survives retries; expect.currentTestName alone cannot.
36
+ function currentDeclaration() {
37
+ const symbol = Object.getOwnPropertySymbols(globalThis)
38
+ .find(key => key.description === "JEST_STATE_SYMBOL");
39
+ const state = symbol && globalThis[symbol];
40
+ if (!state?.currentlyRunningTest || !state.rootDescribeBlock) return undefined;
41
+ const names = new Map();
42
+ let found;
43
+ function visit(block, parents) {
44
+ for (const child of block.children ?? []) {
45
+ if (child.type === "describeBlock") visit(child, [...parents, child.name]);
46
+ else if (child.type === "test") {
47
+ const name = [...parents, child.name].join(" ");
48
+ const occurrence = names.get(name) ?? 0;
49
+ names.set(name, occurrence + 1);
50
+ if (child === state.currentlyRunningTest)
51
+ found = {occurrence, retry: Math.max((child.invocations ?? 1) - 1, 0)};
52
+ }
53
+ }
54
+ }
55
+ visit(state.rootDescribeBlock, []);
56
+ return found;
33
57
  }
34
58
  // Mirrors provenance.mjs, which is ESM and out of reach here.
35
59
  const KINDS = [
@@ -56,7 +80,7 @@ function writeEvidence(suffix, payload) {
56
80
  renameSync(temporary, target);
57
81
  }
58
82
 
59
- if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof afterEach === "function") {
83
+ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof afterAll === "function") {
60
84
  // One in-memory snapshot per test; the server JSONL transport would be
61
85
  // redundant and unattributed, as under Vitest.
62
86
  runtime.enableRuntimeSnapshotEvidence();
@@ -64,6 +88,7 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
64
88
  const emittedSetupFiles = new Set();
65
89
  let active;
66
90
  beforeEach(() => {
91
+ flushActive();
67
92
  const state = expect.getState();
68
93
  const testFile = localFile(state.testPath ?? "unknown");
69
94
  if (!emittedSetupFiles.has(testFile)) {
@@ -87,8 +112,9 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
87
112
  }
88
113
  }
89
114
  const fullName = state.currentTestName ?? "test";
90
- const testId = testIdentity(testFile, fullName);
91
- const retry = attempts.get(testId) ?? 0;
115
+ const declaration = currentDeclaration();
116
+ const testId = testIdentity(testFile, fullName, declaration?.occurrence);
117
+ const retry = declaration?.retry ?? attempts.get(testId) ?? 0;
92
118
  attempts.set(testId, retry + 1);
93
119
  const testKey = digest(testId);
94
120
  const scope = {
@@ -104,7 +130,10 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
104
130
  runtime.activateCoverageScope(scope);
105
131
  runtime.resetCoverage(testId);
106
132
  });
107
- afterEach(() => {
133
+ // Flush after all user afterEach hooks (including Testing Library cleanup).
134
+ // Our setup registers first, so an afterEach snapshot would run too early.
135
+ // The next beforeEach or this file's afterAll runs after test teardown.
136
+ function flushActive() {
108
137
  const current = active;
109
138
  active = undefined;
110
139
  if (!current)
@@ -130,5 +159,6 @@ if (runtime && evidenceDirectory && typeof beforeEach === "function" && typeof a
130
159
  // snapshot (Vitest isolates files; Jest does not).
131
160
  runtime.resetCoverage();
132
161
  runtime.activateCoverageScope();
133
- });
162
+ }
163
+ afterAll(flushActive);
134
164
  }
@@ -30,7 +30,16 @@ export default async function supercovJestConfig() {
30
30
  ...config,
31
31
  // Concurrent tests in one file would share the worker's scope.
32
32
  maxConcurrency: 1,
33
- setupFilesAfterEnv: [...asList(config.setupFilesAfterEnv), here("jest.cjs")],
33
+ setupFiles: [here("jestRuntime.cjs"), ...asList(config.setupFiles)],
34
+ // Babel-based presets (including React Native) lower the runtime
35
+ // side-effect import in instrumented tests to require(). Jest cannot
36
+ // require our ESM shim. Its environment already owns the runtime, so
37
+ // resolve only our generated import to a CommonJS bridge instead.
38
+ moduleNameMapper: {
39
+ "(?:^|/)\\.supercov/node_modules/runtime\\.mjs$": here("jestRuntime.cjs"),
40
+ ...config.moduleNameMapper,
41
+ },
42
+ setupFilesAfterEnv: [here("jest.cjs"), ...asList(config.setupFilesAfterEnv)],
34
43
  reporters: [
35
44
  ...(config.reporters === undefined ? ["default"] : asList(config.reporters)),
36
45
  here("jestReporter.mjs"),
@@ -30,12 +30,23 @@ function attemptStatus(status) {
30
30
  * record carries the attempt's coverage; the report joins the two.
31
31
  */
32
32
  export default class SupercovJestReporter {
33
- onTestCaseResult(test, result) {
33
+ onTestResult(test, fileResult) {
34
+ // Jest's final array follows declaration order, including skipped cases.
35
+ // Completion order does not: retries can finish after a namesake test.
36
+ const occurrences = new Map();
37
+ for (const result of fileResult.testResults ?? []) {
38
+ const occurrence = occurrences.get(result.fullName) ?? 0;
39
+ occurrences.set(result.fullName, occurrence + 1);
40
+ this.recordResult(test, result, occurrence);
41
+ }
42
+ }
43
+ recordResult(test, result, occurrence) {
34
44
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
35
45
  if (!evidenceDirectory)
36
46
  return;
37
47
  const testFile = localFile(test.path);
38
- const testId = `jest:${digest(`${testFile}\0${result.fullName}`)}`;
48
+ const identity = `${testFile}\0${result.fullName}${occurrence ? `\0${occurrence}` : ""}`;
49
+ const testId = `jest:${digest(identity)}`;
39
50
  const retry = Math.max((result.invocations ?? 1) - 1, 0);
40
51
  const status = attemptStatus(result.status);
41
52
  const provenance = inferTestProvenance({
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ // Keep Babel-transformed test modules on the same runtime as the Jest adapter.
4
+ // Loading a second runtime here would split assertion and coverage evidence.
5
+ // jsdom owns a separate global and copies process without the preload's
6
+ // private property. Retrieve the host runtime through Node's VM API, then
7
+ // expose it before user setup files can import instrumented application code.
8
+ const runtime = globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__ ??
9
+ require("node:vm").runInThisContext("globalThis.__SUPERCOV_DIRECT_RUNTIME__");
10
+ if (!runtime) throw new Error("[supercov] Jest runtime was not initialized by the preload");
11
+ globalThis.__SUPERCOV_DIRECT_RUNTIME__ = runtime;
12
+ module.exports = runtime;
@@ -190,22 +190,28 @@ if (isJestEntrypoint && process.env.SUPERCOV_EVIDENCE_DIR) {
190
190
  // Jest reads one configuration. Ours (jest.config.mjs) reads the user's
191
191
  // the way Jest would and adds the adapter and reporter; an explicit
192
192
  // --config on the command line reaches it through the environment.
193
+ const generatedJestConfig = fileURLToPath(new URL("./jest.config.mjs", import.meta.url));
193
194
  for (let index = 2; index < process.argv.length; index += 1) {
194
195
  const argument = process.argv[index];
195
196
  if (argument === "--config" || argument === "-c") {
196
197
  const value = process.argv[index + 1];
197
- if (value)
198
+ // Expo's Jest executable forwards argv to the real Jest process.
199
+ // Preserve the original config across that second preload; reading
200
+ // our own config as the user's would recurse until the heap fills.
201
+ if (value && resolve(value) !== resolve(generatedJestConfig))
198
202
  process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(value);
199
203
  process.argv.splice(index, value ? 2 : 1);
200
204
  index -= 1;
201
205
  }
202
206
  else if (argument?.startsWith("--config=")) {
203
- process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = resolve(argument.slice("--config=".length));
207
+ const value = resolve(argument.slice("--config=".length));
208
+ if (value !== resolve(generatedJestConfig))
209
+ process.env.SUPERCOV_ORIGINAL_JEST_CONFIG = value;
204
210
  process.argv.splice(index, 1);
205
211
  index -= 1;
206
212
  }
207
213
  }
208
- process.argv.push("--config", fileURLToPath(new URL("./jest.config.mjs", import.meta.url)));
214
+ process.argv.push("--config", generatedJestConfig);
209
215
  }
210
216
  if (generatedPlaywrightConfig &&
211
217
  isPlaywrightEntrypoint) {
@@ -17,6 +17,7 @@ const browserContext = await import("vitest/browser").catch(() =>
17
17
  const { commands } = browserContext;
18
18
  const attempts = new Map();
19
19
  const activeScopes = new Map();
20
+ const emittedSetupFiles = new Set();
20
21
  enableRuntimeSnapshotEvidence();
21
22
  function attemptStatus(state) {
22
23
  if (state === "pass")
@@ -62,6 +63,27 @@ async function sendEvidence(payload, suffix) {
62
63
  }
63
64
  beforeEach(async (context) => {
64
65
  const task = context.task;
66
+ // Module imports and shared setup execute before the first test. Save
67
+ // them before resetCoverage clears the snapshot, with a setup identity
68
+ // rather than crediting that execution to the first test.
69
+ if (!emittedSetupFiles.has(task.file.id)) {
70
+ const setupSnapshot = coverageSnapshot();
71
+ if (setupSnapshot.hits.length || setupSnapshot.decisions.length) {
72
+ await sendEvidence({
73
+ testId: `vitest:${task.file.id}:setup`,
74
+ test: `${task.file.name} > module setup`,
75
+ projectName: task.file.projectName,
76
+ title: "module setup",
77
+ retry: 0,
78
+ status: "passed",
79
+ role: "setup",
80
+ runtime: [setupSnapshot],
81
+ browser: [],
82
+ server: [],
83
+ }, `vitest-${task.file.id}-setup`);
84
+ }
85
+ emittedSetupFiles.add(task.file.id);
86
+ }
65
87
  const testId = `vitest:${task.id}`;
66
88
  const retry = attempts.get(testId) ?? 0;
67
89
  attempts.set(testId, retry + 1);
@@ -25,6 +25,15 @@ function rawAttemptStatus(state, expectedFailure) {
25
25
  /** Records final runner outcomes, including tests that never execute hooks. */
26
26
  export default class SupercovVitestReporter {
27
27
  reportedAttempts = new Set();
28
+ constructor(configureProjects) {
29
+ this.configureProjects = configureProjects;
30
+ }
31
+ onInit(vitest) {
32
+ this.configureProjects?.(vitest.projects);
33
+ }
34
+ onBrowserInit(project) {
35
+ this.configureProjects?.([project]);
36
+ }
28
37
  onTestCaseResult(testCase) {
29
38
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
30
39
  if (!evidenceDirectory)