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.
- package/docs/supported-suites.md +24 -3
- package/package.json +11 -9
- package/runtime/javascript/jest.cjs +37 -7
- package/runtime/javascript/jest.config.mjs +10 -1
- package/runtime/javascript/jestReporter.mjs +13 -2
- package/runtime/javascript/jestRuntime.cjs +12 -0
- package/runtime/javascript/register.mjs +9 -3
- package/runtime/javascript/vitestBrowser.mjs +22 -0
- package/runtime/javascript/vitestReporter.mjs +9 -0
package/docs/supported-suites.md
CHANGED
|
@@ -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
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
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.
|
|
101
|
-
"@supercov/cli-darwin-x64": "0.0.
|
|
102
|
-
"@supercov/cli-linux-arm64-gnu": "0.0.
|
|
103
|
-
"@supercov/cli-linux-arm64-musl": "0.0.
|
|
104
|
-
"@supercov/cli-linux-x64-gnu": "0.0.
|
|
105
|
-
"@supercov/cli-linux-x64-musl": "0.0.
|
|
106
|
-
"@supercov/cli-win32-arm64": "0.0.
|
|
107
|
-
"@supercov/cli-win32-x64": "0.0.
|
|
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
|
|
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
|
|
91
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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)
|