supercov 0.0.45 → 0.0.46

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.
@@ -87,7 +87,7 @@ function restoreUserError(error, depth = 0) {
87
87
  return error;
88
88
  }
89
89
  const registrationCounts = new Map();
90
- function wrappedRegistration(original, parentTestId) {
90
+ function wrappedRegistration(original, parentTestId, forcedStatus) {
91
91
  const wrapped = function supercovNodeTest(...args) {
92
92
  const index = callbackIndex(args);
93
93
  if (index < 0)
@@ -115,13 +115,15 @@ function wrappedRegistration(original, parentTestId) {
115
115
  const scope = runnerExecutionScope(identity);
116
116
  const options = testOptions(args, index);
117
117
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
118
+ if (options?.skip || options?.todo || forcedStatus)
119
+ writeRunnerEvidence(identity, "skipped", scope, evidenceDirectory);
118
120
  const next = [...args];
119
121
  const execute = (callbackThis, context, done) => {
120
122
  // A test or its hooks may intentionally modify Supercov's public
121
123
  // environment while testing integrations. Keep this attempt's transport
122
124
  // destination fixed to the value present when the test was registered.
123
125
  beginBufferedServerEvidence(scope);
124
- let status = options?.skip || options?.todo
126
+ let status = options?.skip || options?.todo || forcedStatus
125
127
  ? "skipped"
126
128
  : "passed";
127
129
  const contextProxy = new Proxy(context, {
@@ -138,6 +140,8 @@ function wrappedRegistration(original, parentTestId) {
138
140
  }
139
141
  if (property === "test" && typeof value === "function")
140
142
  return wrappedRegistration(value.bind(target), scope.testId);
143
+ if (["after", "before", "afterEach", "beforeEach"].includes(property) && typeof value === "function")
144
+ return restoringHook(value.bind(target), scope);
141
145
  return typeof value === "function" ? value.bind(target) : value;
142
146
  },
143
147
  });
@@ -149,6 +153,11 @@ function wrappedRegistration(original, parentTestId) {
149
153
  const flushedServerEvidence = flushBufferedServerEvidence(scope);
150
154
  writeRunnerEvidence(identity, nextStatus, scope, evidenceDirectory, takeNodeAssertionPhases(scope), flushedServerEvidence);
151
155
  };
156
+ const finishBody = () => {
157
+ // User t.after callbacks run after the body. Register last so
158
+ // their assertions and cleanup probes are part of this attempt.
159
+ context.after(() => emit());
160
+ };
152
161
  try {
153
162
  if (callback.length >= 2) {
154
163
  const callbackDone = (error) => {
@@ -156,7 +165,7 @@ function wrappedRegistration(original, parentTestId) {
156
165
  status = "failed";
157
166
  restoreUserError(error);
158
167
  }
159
- emit();
168
+ finishBody();
160
169
  done?.(error);
161
170
  };
162
171
  return withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy, callbackDone]));
@@ -164,19 +173,19 @@ function wrappedRegistration(original, parentTestId) {
164
173
  const result = withCoverageCarrier({ version: 1, scope }, () => Reflect.apply(callback, callbackThis, [contextProxy]));
165
174
  if (result && typeof result.then === "function")
166
175
  return Promise.resolve(result).then((value) => {
167
- emit();
176
+ finishBody();
168
177
  return value;
169
178
  }, (error) => {
170
179
  status = "failed";
171
- emit();
180
+ finishBody();
172
181
  throw restoreUserError(error);
173
182
  });
174
- emit();
183
+ finishBody();
175
184
  return result;
176
185
  }
177
186
  catch (error) {
178
187
  status = "failed";
179
- emit();
188
+ finishBody();
180
189
  throw restoreUserError(error);
181
190
  }
182
191
  };
@@ -200,45 +209,52 @@ function wrappedRegistration(original, parentTestId) {
200
209
  Object.defineProperty(wrapped, property, {
201
210
  configurable: true,
202
211
  enumerable: true,
203
- value: wrappedRegistration(member, parentTestId),
212
+ value: wrappedRegistration(member, parentTestId, property === "only" ? undefined : "skipped"),
204
213
  });
205
214
  }
206
215
  return wrapped;
207
216
  }
208
- // Hooks carry no coverage evidence, but an error thrown inside one still
209
- // reaches the report with the adapter's execution context in its stack.
210
- // Restore it at the same boundary the test wrapper uses.
211
- function restoringHook(original) {
217
+ // Shared hooks get their own setup scope. Their execution is visible but is
218
+ // never silently copied into every test. t.after and other per-test hooks keep
219
+ // the exact owning attempt supplied by the TestContext proxy.
220
+ function restoringHook(original, scope, hookName = "hook") {
212
221
  return function supercovNodeTestHook(...args) {
213
222
  const index = callbackIndex(args);
214
- if (index < 0)
215
- return Reflect.apply(original, this, args);
223
+ if (index < 0) return Reflect.apply(original, this, args);
216
224
  const callback = args[index];
225
+ const location = callerLocation(supercovNodeTestHook);
226
+ let invocation = 0;
227
+ const execute = (receiver, context, done) => {
228
+ const identity = { runner: "node:test", role: "setup", name: `[${hookName}] ${callback.name || "anonymous"}`,
229
+ ...location, registrationOrdinal: invocation++ };
230
+ const owner = scope ?? runnerExecutionScope(identity);
231
+ const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"];
232
+ if (!scope) beginBufferedServerEvidence(owner);
233
+ let emitted = false;
234
+ const finish = error => {
235
+ if (scope || emitted) return;
236
+ emitted = true;
237
+ writeRunnerEvidence(identity, error ? "failed" : "passed", owner, evidenceDirectory,
238
+ takeNodeAssertionPhases(owner), flushBufferedServerEvidence(owner));
239
+ };
240
+ try {
241
+ const result = withCoverageCarrier({ version: 1, scope: owner }, () =>
242
+ callback.length >= 2 ? callback.call(receiver, context, error => {
243
+ finish(error);
244
+ done?.(error ? restoreUserError(error) : undefined);
245
+ }) : callback.call(receiver, context));
246
+ if (callback.length >= 2) return result;
247
+ if (result && typeof result.then === "function")
248
+ return Promise.resolve(result).then(value => { finish(); return value; },
249
+ error => { finish(error); throw restoreUserError(error); });
250
+ finish();
251
+ return result;
252
+ } catch (error) { finish(error); throw restoreUserError(error); }
253
+ };
217
254
  const next = [...args];
218
- // node:test uses callback arity to distinguish promise/synchronous
219
- // hooks from the legacy done-callback form. Preserve it exactly.
220
255
  next[index] = callback.length >= 2
221
- ? function supercovNodeTestHookDoneCallback(context, done) {
222
- const restoringDone = (error) => {
223
- if (error)
224
- restoreUserError(error);
225
- done?.(error);
226
- };
227
- return callback.call(this, context, restoringDone);
228
- }
229
- : function supercovNodeTestHookCallback(context) {
230
- try {
231
- const result = callback.call(this, context);
232
- if (result && typeof result.then === "function")
233
- return Promise.resolve(result).then(undefined, (error) => {
234
- throw restoreUserError(error);
235
- });
236
- return result;
237
- }
238
- catch (error) {
239
- throw restoreUserError(error);
240
- }
241
- };
256
+ ? function supercovNodeTestHookDoneCallback(context, done) { return execute(this, context, done); }
257
+ : function supercovNodeTestHookCallback(context) { return execute(this, context); };
242
258
  return Reflect.apply(original, this, next);
243
259
  };
244
260
  }
@@ -246,10 +262,10 @@ export const test = wrappedRegistration(native.test);
246
262
  export const it = wrappedRegistration(native.it);
247
263
  export const suite = native.suite;
248
264
  export const describe = native.describe;
249
- export const before = restoringHook(native.before);
250
- export const after = restoringHook(native.after);
251
- export const beforeEach = restoringHook(native.beforeEach);
252
- export const afterEach = restoringHook(native.afterEach);
265
+ export const before = restoringHook(native.before, undefined, "before");
266
+ export const after = restoringHook(native.after, undefined, "after");
267
+ export const beforeEach = restoringHook(native.beforeEach, undefined, "beforeEach");
268
+ export const afterEach = restoringHook(native.afterEach, undefined, "afterEach");
253
269
  export const mock = native.mock;
254
270
  export const snapshot = native.snapshot;
255
271
  export const run = native.run;
@@ -7,7 +7,10 @@ const NORMALIZED_KINDS = [
7
7
  function classifiedKind(value) {
8
8
  if (!value)
9
9
  return undefined;
10
- return NORMALIZED_KINDS.find(([, pattern]) => pattern.test(value))?.[0];
10
+ // gatewayE2e.test.ts and responseIntegration.test.ts are conventional
11
+ // camel-case paths too. Do not infer kinds from test titles or API usage.
12
+ const words = value.replace(/([a-z0-9])([A-Z])/g, "$1-$2");
13
+ return NORMALIZED_KINDS.find(([, pattern]) => pattern.test(words))?.[0];
11
14
  }
12
15
  export function inferTestProvenance({ runner, file, project, explicitKind, }) {
13
16
  if (explicitKind?.trim()) {
@@ -69,17 +69,21 @@ if (verboseDiagnostics && !process.__SUPERCOV_DIAGNOSTIC_REPORTER__) {
69
69
  });
70
70
  }
71
71
  }
72
- // Runner adapters and instrumented modules import the runtime when they need
73
- // it. Keeping the preload itself thin avoids evaluating the full collector in
74
- // npm launchers, web-server supervisors, and other Node children that never
75
- // execute measured JavaScript.
76
- if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1") {
72
+ // Direct instrumentation must initialize the collector before script files
73
+ // with no import boundary evaluate. Other modes let the runner adapter or an
74
+ // instrumented module load it, except when durable remote evidence requires it.
75
+ if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1" ||
76
+ process.env.SUPERCOV_DIRECT_INSTRUMENTATION === "1") {
77
77
  // A translated remote/VM command can execute an ahead-of-run transformed
78
78
  // test through an opaque runner that bypasses the ordinary Playwright or
79
79
  // node:test import boundary. The remote-launch adapter marks that process;
80
80
  // initialize the runtime before its transformed module can evaluate.
81
81
  globalThis.__SUPERCOV_DIRECT_RUNTIME__ ??= await import("./runtime.mjs");
82
82
  process.__SUPERCOV_DIRECT_RUNTIME__ ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
83
+ // Generic builds can contain script files with no import/export syntax.
84
+ // They cannot import the collector without changing their module semantics,
85
+ // and may evaluate before any ESM instrumented file imports it.
86
+ globalThis.__supercovRuntime ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
83
87
  }
84
88
  // Workers are independent Node processes and an explicit `execArgv: []`
85
89
  // otherwise strips the preload that supplies the isolated runtime. Preserve
@@ -36,13 +36,15 @@ export function runnerTestId(identity) {
36
36
  ];
37
37
  // Preserve existing top-level, uniquely registered test IDs. Nested tests
38
38
  // and repeated registrations need more than a shared source/name identity.
39
+ if (identity.role === "setup")
40
+ parts.push("role", "setup");
39
41
  if (identity.parentTestId)
40
42
  parts.push("parent", identity.parentTestId);
41
43
  if (identity.registrationOrdinal)
42
44
  parts.push("registration", identity.registrationOrdinal);
43
45
  // Titles can themselves contain separator text. Domain-separate and encode
44
46
  // the extended identity structurally so it cannot alias a literal title.
45
- const key = identity.parentTestId || identity.registrationOrdinal
47
+ const key = identity.role === "setup" || identity.parentTestId || identity.registrationOrdinal
46
48
  ? JSON.stringify(["registration-v2", ...parts])
47
49
  : parts.join("\0");
48
50
  return `${identity.runner}:${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
@@ -116,6 +118,7 @@ export function writeRunnerEvidence(identity, status, scope, evidenceDirectoryOv
116
118
  title: identity.name.split(" > ").at(-1) ?? identity.name,
117
119
  retry: identity.retry ?? 0,
118
120
  status,
121
+ ...(identity.role ? { role: identity.role } : {}),
119
122
  provenance: inferTestProvenance({
120
123
  runner: identity.runner,
121
124
  file: testFile,
@@ -963,15 +963,56 @@ function requestCoverageContext(value) {
963
963
  const phaseId = typeof rawPhaseId === "string" && rawPhaseId.length > 0 && (!scope || phaseBelongsToAttempt(rawPhaseId, scope.attemptId)) ? rawPhaseId : void 0;
964
964
  return __spreadValues(__spreadValues({}, scope ? { scope } : {}), phaseId ? { phaseId } : {});
965
965
  }
966
- function withRequestPhase(handler) {
966
+ // Keep connection ownership on the emitter, without changing listener
967
+ // identity (removeListener/off and once still see the original callbacks).
968
+ const emitterContextMaps = runtimeGlobal.__SUPERCOV_EMITTER_CONTEXT_MAPS__ ??= new Map();
969
+ const emitterCoverageContexts = emitterContextMaps.get(runtimeInstance) ?? new WeakMap();
970
+ emitterContextMaps.set(runtimeInstance, emitterCoverageContexts);
971
+ const patchedEmitterInstances = runtimeGlobal.__SUPERCOV_EMITTER_PATCHED_INSTANCES__ ??= new Set();
972
+ function installNodeRequestPropagation() {
973
+ if (isBrowser || patchedEmitterInstances.has(runtimeInstance) || typeof process === "undefined") return;
974
+ const EventEmitter = process.getBuiltinModule?.("node:events")?.EventEmitter;
975
+ const Server = process.getBuiltinModule?.("node:http")?.Server;
976
+ const SecureServer = process.getBuiltinModule?.("node:https")?.Server;
977
+ if (!EventEmitter) return;
978
+ const original = EventEmitter.prototype.emit;
979
+ EventEmitter.prototype.emit = function(event, ...args) {
980
+ let context = emitterCoverageContexts.get(this);
981
+ if ((event === "request" || event === "upgrade") &&
982
+ ((Server && this instanceof Server) || (SecureServer && this instanceof SecureServer))) {
983
+ // HTTP headers establish ownership even when Express/SDK listeners and
984
+ // the server process were created by a shared before hook.
985
+ const incoming = requestCoverageContext(args[0]);
986
+ if (incoming) context = incoming.scope ? incoming : context ?? currentRequestContext();
987
+ }
988
+ const invoke = () => Reflect.apply(original, this, [event, ...args]);
989
+ try {
990
+ return context ? withCoverageCarrier({ version: 1, ...context }, invoke) : invoke();
991
+ } finally {
992
+ if (event === "close") emitterCoverageContexts.delete(this);
993
+ }
994
+ };
995
+ patchedEmitterInstances.add(runtimeInstance);
996
+ }
997
+ installNodeRequestPropagation();
998
+
999
+ function withRequestPhase(handler, event) {
967
1000
  if (!serverPhaseStorage)
968
1001
  return handler;
1002
+ const registeredContext = currentRequestContext();
969
1003
  return function coverageRequestPhase(...args) {
970
1004
  var _a8, _b, _c, _d;
971
1005
  const requestContext = args.map((argument) => requestCoverageContext(argument)).find((context2) => context2 !== void 0);
972
- const inheritedContext = requestContext === void 0 ? currentRequestContext() : {};
1006
+ // An untagged request to a test-owned server must not erase that server's
1007
+ // owner. Shared servers have no captured test scope and stay unattributed.
1008
+ const inheritedContext = requestContext?.scope ? {} : registeredContext.scope ? registeredContext : requestContext === void 0 ? currentRequestContext() : {};
973
1009
  const context = __spreadValues(__spreadValues({}, ((_a8 = requestContext == null ? void 0 : requestContext.scope) != null ? _a8 : inheritedContext.scope) ? { scope: (_b = requestContext == null ? void 0 : requestContext.scope) != null ? _b : inheritedContext.scope } : {}), ((_c = requestContext == null ? void 0 : requestContext.phaseId) != null ? _c : inheritedContext.phaseId) ? { phaseId: (_d = requestContext == null ? void 0 : requestContext.phaseId) != null ? _d : inheritedContext.phaseId } : {});
974
- const invoke = () => Reflect.apply(handler, this, args);
1010
+ const invoke = () => {
1011
+ if (event === "connection" && context.scope && args[0] &&
1012
+ typeof args[0] === "object" && typeof args[0].emit === "function")
1013
+ emitterCoverageContexts.set(args[0], context);
1014
+ return Reflect.apply(handler, this, args);
1015
+ };
975
1016
  return requestContext !== void 0 || context.scope || context.phaseId ? serverPhaseStorage.run(context, () => withProbeV2Context(context, invoke)) : invoke();
976
1017
  };
977
1018
  }