supercov 0.0.53 → 0.0.54
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
CHANGED
|
@@ -49,7 +49,7 @@ Supercov reports the level it actually observed. It does not guess.
|
|
|
49
49
|
| Runner | Attribution |
|
|
50
50
|
| --- | --- |
|
|
51
51
|
| Playwright | Exact per test, worker, retry, outcome, action, and assertion phase |
|
|
52
|
-
| Vitest | Exact per test, with setup execution kept separate |
|
|
52
|
+
| Vitest | Exact per test, with setup execution kept separate; Browser Mode included |
|
|
53
53
|
| Jest | Exact per test, including parameterized tests, with the user's own configuration, setup files and reporters kept; passing `expect` occurrences are identified for assertion maps |
|
|
54
54
|
| `node:test` | Exact per test |
|
|
55
55
|
| AVA and Mocha | Aggregate structural coverage |
|
|
@@ -59,6 +59,30 @@ Supercov reports the level it actually observed. It does not guess.
|
|
|
59
59
|
One command may launch several runners. Supercov combines their evidence into
|
|
60
60
|
one run and preserves runner identity wherever the runner exposes it.
|
|
61
61
|
|
|
62
|
+
### Vitest Browser Mode
|
|
63
|
+
|
|
64
|
+
Vitest Browser Mode runs the test file in a real browser. Supercov measures it
|
|
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.
|
|
69
|
+
|
|
70
|
+
Component code is instrumented the same way as any other source, with one
|
|
71
|
+
addition that matters most here. A JSX tree is a single statement, so an
|
|
72
|
+
expression rendered inside it -- `aria-label={label(state)}`, a child
|
|
73
|
+
`{formatted(value)}` -- is measured on its own rather than counted as covered
|
|
74
|
+
because the component rendered once. This is also what lets a UI assertion be
|
|
75
|
+
explained precisely: `toHaveAccessibleName` names the attribute and
|
|
76
|
+
`toHaveTextContent` names the child, and each is credited separately.
|
|
77
|
+
|
|
78
|
+
Expressions that cannot independently fail to evaluate do not become
|
|
79
|
+
obligations. `{value}` is reached exactly when the tree is, and
|
|
80
|
+
`onClick={() => save()}` is measured where the handler is called rather than
|
|
81
|
+
where it is created.
|
|
82
|
+
|
|
83
|
+
`expect.element(...)`, `expect.soft(...)` and `expect.poll(...)` are recognised
|
|
84
|
+
as assertions, so their passing occurrences are available to assertion maps.
|
|
85
|
+
|
|
62
86
|
### Builds and source formats
|
|
63
87
|
|
|
64
88
|
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.54",
|
|
4
4
|
"description": "Coverage for coding agents and software factories \ud83c\udf19",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -97,14 +97,14 @@
|
|
|
97
97
|
"test:launcher": "go vet ./cmd/... && go test ./cmd/..."
|
|
98
98
|
},
|
|
99
99
|
"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.
|
|
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"
|
|
108
108
|
},
|
|
109
109
|
"peerDependencies": {
|
|
110
110
|
"@playwright/test": ">=1.55.0",
|
|
@@ -1207,6 +1207,15 @@ function selectionEnd(frame, value) {
|
|
|
1207
1207
|
}
|
|
1208
1208
|
return value;
|
|
1209
1209
|
}
|
|
1210
|
+
// A JSX expression container has nowhere to put a statement and cannot hold a
|
|
1211
|
+
// bare comma operator, so a rendered expression carries its probe as a call
|
|
1212
|
+
// that returns the value through. Like optionalSelect, it records after the
|
|
1213
|
+
// value evaluates: an expression that throws is reported unevaluated rather
|
|
1214
|
+
// than covered.
|
|
1215
|
+
function renderedValueV2(file, index, value) {
|
|
1216
|
+
coverageHitV2(file, index);
|
|
1217
|
+
return value;
|
|
1218
|
+
}
|
|
1210
1219
|
function optionalSelect(shortId, continuedId, value) {
|
|
1211
1220
|
coverageHit(value === null || value === void 0 ? shortId : continuedId);
|
|
1212
1221
|
return value;
|
|
@@ -1356,6 +1365,7 @@ const directRuntimeApi = {
|
|
|
1356
1365
|
optionalCallEnd,
|
|
1357
1366
|
optionalCallReached,
|
|
1358
1367
|
optionalSelect,
|
|
1368
|
+
renderedValueV2,
|
|
1359
1369
|
parenthesizedAssignmentValue,
|
|
1360
1370
|
phaseBelongsToAttempt,
|
|
1361
1371
|
registerProbeV2,
|
|
@@ -1406,6 +1416,7 @@ export {
|
|
|
1406
1416
|
optionalCallEnd,
|
|
1407
1417
|
optionalCallReached,
|
|
1408
1418
|
optionalSelect,
|
|
1419
|
+
renderedValueV2,
|
|
1409
1420
|
parenthesizedAssignmentValue,
|
|
1410
1421
|
phaseBelongsToAttempt,
|
|
1411
1422
|
registerProbeV2,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Vitest Browser Mode runs the test file in the browser, so the node setup
|
|
2
|
+
// cannot be used: it imports node:fs through atomic.mjs, and Vite externalises
|
|
3
|
+
// that for the client. Loading it fails the whole suite before a single test
|
|
4
|
+
// collects, which is what a browser-mode project saw.
|
|
5
|
+
//
|
|
6
|
+
// This setup mirrors the node one's payload contract exactly. What it cannot do
|
|
7
|
+
// in the browser -- resolve a path against the project root, infer provenance,
|
|
8
|
+
// write a file -- it hands to node over Vitest's browser command channel, so
|
|
9
|
+
// that contract has one implementation.
|
|
10
|
+
import { afterEach, beforeEach } from "vitest";
|
|
11
|
+
import { coverageSnapshot, activateCoverageScope, enableRuntimeSnapshotEvidence, resetCoverage, takeNodeAssertionPhases, } from "./runtime.mjs";
|
|
12
|
+
// Vitest exposes the command channel at "vitest/browser" from v4 and at
|
|
13
|
+
// "@vitest/browser/context" before that. Both are tried so a project is not
|
|
14
|
+
// forced onto one Vitest line to be measured.
|
|
15
|
+
const browserContext = await import("vitest/browser").catch(() =>
|
|
16
|
+
import("@vitest/browser/context"));
|
|
17
|
+
const { commands } = browserContext;
|
|
18
|
+
const attempts = new Map();
|
|
19
|
+
const activeScopes = new Map();
|
|
20
|
+
enableRuntimeSnapshotEvidence();
|
|
21
|
+
function attemptStatus(state) {
|
|
22
|
+
if (state === "pass")
|
|
23
|
+
return "passed";
|
|
24
|
+
if (state === "fail")
|
|
25
|
+
return "failed";
|
|
26
|
+
if (state === "skip" || state === "todo")
|
|
27
|
+
return "skipped";
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
function titlePath(task) {
|
|
31
|
+
const names = [task.name];
|
|
32
|
+
let suite = task.suite;
|
|
33
|
+
while (suite?.name) {
|
|
34
|
+
names.unshift(suite.name);
|
|
35
|
+
suite = suite.suite;
|
|
36
|
+
}
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
39
|
+
// The node setup keys an attempt by sha256(testId). Browsers have the same
|
|
40
|
+
// digest behind an async API, so the hook awaits it rather than substituting a
|
|
41
|
+
// different hash and giving the same test two identities across environments.
|
|
42
|
+
async function testKeyOf(testId) {
|
|
43
|
+
const bytes = new TextEncoder().encode(testId);
|
|
44
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
45
|
+
return [...new Uint8Array(digest)]
|
|
46
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
47
|
+
.join("")
|
|
48
|
+
.slice(0, 24);
|
|
49
|
+
}
|
|
50
|
+
// Evidence leaves the browser over Vitest's own command channel: node runs the
|
|
51
|
+
// handler, the test awaits it, and the failure is reported rather than
|
|
52
|
+
// swallowed -- silently losing a test's evidence is the worst shape a
|
|
53
|
+
// measurement bug takes.
|
|
54
|
+
async function sendEvidence(payload, suffix) {
|
|
55
|
+
try {
|
|
56
|
+
await commands.__supercovEvidence(payload, suffix);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error("[supercov] failed to record browser evidence:", error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
beforeEach(async (context) => {
|
|
64
|
+
const task = context.task;
|
|
65
|
+
const testId = `vitest:${task.id}`;
|
|
66
|
+
const retry = attempts.get(testId) ?? 0;
|
|
67
|
+
attempts.set(testId, retry + 1);
|
|
68
|
+
const testKey = await testKeyOf(testId);
|
|
69
|
+
const scope = {
|
|
70
|
+
version: 1,
|
|
71
|
+
runId: globalThis.__SUPERCOV_RUN_ID__ ?? "unscoped",
|
|
72
|
+
workerId: "vitest-browser",
|
|
73
|
+
testId,
|
|
74
|
+
testKey,
|
|
75
|
+
retry,
|
|
76
|
+
attemptId: `${testKey}-${retry}`,
|
|
77
|
+
};
|
|
78
|
+
activeScopes.set(task.id, scope);
|
|
79
|
+
activateCoverageScope(scope);
|
|
80
|
+
resetCoverage(testId);
|
|
81
|
+
});
|
|
82
|
+
afterEach(async (context) => {
|
|
83
|
+
const task = context.task;
|
|
84
|
+
const scope = activeScopes.get(task.id);
|
|
85
|
+
const retry = scope?.retry ?? task.result?.retryCount ?? 0;
|
|
86
|
+
// Node relativises the test file Vitest reports and infers provenance, so
|
|
87
|
+
// both environments describe a test the same way and neither trusts the
|
|
88
|
+
// browser realm to say which file it was.
|
|
89
|
+
const payload = {
|
|
90
|
+
testId: scope?.testId ?? `vitest:${task.id}`,
|
|
91
|
+
...(scope ? { scope } : {}),
|
|
92
|
+
test: [...titlePath(task)].join(" > "),
|
|
93
|
+
projectName: task.file?.projectName,
|
|
94
|
+
title: task.name,
|
|
95
|
+
retry,
|
|
96
|
+
status: attemptStatus(task.result?.state),
|
|
97
|
+
...(scope ? { phases: takeNodeAssertionPhases(scope) } : {}),
|
|
98
|
+
// Browser-mode evidence is a runtime snapshot like node's: the code
|
|
99
|
+
// under test runs in the same realm as the probes.
|
|
100
|
+
runtime: [coverageSnapshot()],
|
|
101
|
+
browser: [],
|
|
102
|
+
server: [],
|
|
103
|
+
};
|
|
104
|
+
await sendEvidence(payload, `vitest-${task.id}-${retry}`);
|
|
105
|
+
activeScopes.delete(task.id);
|
|
106
|
+
activateCoverageScope();
|
|
107
|
+
});
|