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.
@@ -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.53",
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.53",
101
- "@supercov/cli-darwin-x64": "0.0.53",
102
- "@supercov/cli-linux-arm64-gnu": "0.0.53",
103
- "@supercov/cli-linux-arm64-musl": "0.0.53",
104
- "@supercov/cli-linux-x64-gnu": "0.0.53",
105
- "@supercov/cli-linux-x64-musl": "0.0.53",
106
- "@supercov/cli-win32-arm64": "0.0.53",
107
- "@supercov/cli-win32-x64": "0.0.53"
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
+ });