@c9up/helix 0.1.8 → 0.1.10
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/README.md +274 -5
- package/bin/helix.js +312 -13
- package/dist/cli/failed-cache.d.ts +27 -0
- package/dist/cli/failed-cache.d.ts.map +1 -0
- package/dist/cli/failed-cache.js +64 -0
- package/dist/cli/failed-cache.js.map +1 -0
- package/dist/cli/glob.d.ts +36 -0
- package/dist/cli/glob.d.ts.map +1 -0
- package/dist/cli/glob.js +193 -0
- package/dist/cli/glob.js.map +1 -0
- package/dist/cli/native.d.ts +4 -0
- package/dist/cli/native.d.ts.map +1 -1
- package/dist/cli/native.js.map +1 -1
- package/dist/cli/pool.d.ts +11 -0
- package/dist/cli/pool.d.ts.map +1 -1
- package/dist/cli/pool.js +13 -7
- package/dist/cli/pool.js.map +1 -1
- package/dist/cli/reporter.d.ts +11 -0
- package/dist/cli/reporter.d.ts.map +1 -1
- package/dist/cli/reporter.js +53 -0
- package/dist/cli/reporter.js.map +1 -1
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +181 -21
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runner.d.ts +19 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +15 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suites.d.ts +102 -0
- package/dist/cli/suites.d.ts.map +1 -0
- package/dist/cli/suites.js +226 -0
- package/dist/cli/suites.js.map +1 -0
- package/dist/index.d.ts +10 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/japa/core.d.ts +106 -0
- package/dist/japa/core.d.ts.map +1 -0
- package/dist/japa/core.js +128 -0
- package/dist/japa/core.js.map +1 -0
- package/dist/runtime/assert.d.ts +174 -0
- package/dist/runtime/assert.d.ts.map +1 -0
- package/dist/runtime/assert.js +591 -0
- package/dist/runtime/assert.js.map +1 -0
- package/dist/runtime/asymmetric-brand.d.ts +20 -0
- package/dist/runtime/asymmetric-brand.d.ts.map +1 -0
- package/dist/runtime/asymmetric-brand.js +18 -0
- package/dist/runtime/asymmetric-brand.js.map +1 -0
- package/dist/runtime/asymmetric.d.ts +2 -10
- package/dist/runtime/asymmetric.d.ts.map +1 -1
- package/dist/runtime/asymmetric.js +3 -7
- package/dist/runtime/asymmetric.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +47 -0
- package/dist/runtime/bootstrap.d.ts.map +1 -0
- package/dist/runtime/bootstrap.js +166 -0
- package/dist/runtime/bootstrap.js.map +1 -0
- package/dist/runtime/cli-args.d.ts +77 -0
- package/dist/runtime/cli-args.d.ts.map +1 -0
- package/dist/runtime/cli-args.js +85 -0
- package/dist/runtime/cli-args.js.map +1 -0
- package/dist/runtime/cli-worker.js +1 -2
- package/dist/runtime/cli-worker.js.map +1 -1
- package/dist/runtime/configure.d.ts +214 -0
- package/dist/runtime/configure.d.ts.map +1 -0
- package/dist/runtime/configure.js +164 -0
- package/dist/runtime/configure.js.map +1 -0
- package/dist/runtime/context.d.ts +84 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +146 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/emitter.d.ts +141 -0
- package/dist/runtime/emitter.d.ts.map +1 -0
- package/dist/runtime/emitter.js +79 -0
- package/dist/runtime/emitter.js.map +1 -0
- package/dist/runtime/equals.d.ts.map +1 -1
- package/dist/runtime/equals.js +6 -10
- package/dist/runtime/equals.js.map +1 -1
- package/dist/runtime/global-hooks.d.ts +31 -0
- package/dist/runtime/global-hooks.d.ts.map +1 -0
- package/dist/runtime/global-hooks.js +126 -0
- package/dist/runtime/global-hooks.js.map +1 -0
- package/dist/runtime/index.d.ts +14 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +7 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/ipc-protocol.d.ts +21 -0
- package/dist/runtime/ipc-protocol.d.ts.map +1 -0
- package/dist/runtime/ipc-protocol.js +21 -0
- package/dist/runtime/ipc-protocol.js.map +1 -0
- package/dist/runtime/run.d.ts +49 -2
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +618 -94
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runner.d.ts +97 -0
- package/dist/runtime/runner.d.ts.map +1 -0
- package/dist/runtime/runner.js +160 -0
- package/dist/runtime/runner.js.map +1 -0
- package/dist/runtime/suite-config.d.ts +23 -0
- package/dist/runtime/suite-config.d.ts.map +1 -0
- package/dist/runtime/suite-config.js +52 -0
- package/dist/runtime/suite-config.js.map +1 -0
- package/dist/runtime/suite-taps.d.ts +122 -0
- package/dist/runtime/suite-taps.d.ts.map +1 -0
- package/dist/runtime/suite-taps.js +218 -0
- package/dist/runtime/suite-taps.js.map +1 -0
- package/dist/runtime/suite.d.ts +277 -8
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +255 -4
- package/dist/runtime/suite.js.map +1 -1
- package/dist/runtime/test-context.d.ts +50 -3
- package/dist/runtime/test-context.d.ts.map +1 -1
- package/dist/runtime/test-context.js +83 -5
- package/dist/runtime/test-context.js.map +1 -1
- package/dist/runtime/worker.d.ts.map +1 -1
- package/dist/runtime/worker.js +86 -29
- package/dist/runtime/worker.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +8 -1
package/dist/runtime/run.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* tree, non-`only` paths are downgraded to `skip` before execution.
|
|
8
8
|
*/
|
|
9
9
|
import { AssertionError } from "./assertion-error.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { buildTestContext } from "./context.js";
|
|
11
|
+
import { emitter, } from "./emitter.js";
|
|
12
|
+
import { interpolateDatasetTitle } from "./suite.js";
|
|
13
|
+
import { drainTestOutcomeHooks, getAssertionState, registerTestCleanup, setFrameContext, setFrameOutcome, setFrameTest, withTestContext, } from "./test-context.js";
|
|
14
|
+
function compileTagFilter(tags, matchAll) {
|
|
12
15
|
if (!tags || tags.length === 0)
|
|
13
16
|
return undefined;
|
|
14
17
|
const required = [];
|
|
@@ -17,14 +20,16 @@ function compileTagFilter(tags) {
|
|
|
17
20
|
const t = raw.trim();
|
|
18
21
|
if (!t)
|
|
19
22
|
continue;
|
|
20
|
-
|
|
23
|
+
// `~` is the Japa exclusion prefix; `!` is accepted too (helix legacy) so a
|
|
24
|
+
// stray `!` never silently becomes an impossible required tag.
|
|
25
|
+
if (t.startsWith("~") || t.startsWith("!"))
|
|
21
26
|
excluded.push(t.slice(1));
|
|
22
27
|
else
|
|
23
28
|
required.push(t);
|
|
24
29
|
}
|
|
25
30
|
if (required.length === 0 && excluded.length === 0)
|
|
26
31
|
return undefined;
|
|
27
|
-
return { required, excluded };
|
|
32
|
+
return { required, excluded, matchAll };
|
|
28
33
|
}
|
|
29
34
|
function compileGrep(grep) {
|
|
30
35
|
if (!grep)
|
|
@@ -39,29 +44,68 @@ function compileGrep(grep) {
|
|
|
39
44
|
}
|
|
40
45
|
function tagMatches(node, filter) {
|
|
41
46
|
const have = new Set(node.tags ?? []);
|
|
42
|
-
|
|
43
|
-
if (!have.has(r))
|
|
44
|
-
return false;
|
|
47
|
+
// Exclusions always win.
|
|
45
48
|
for (const e of filter.excluded)
|
|
46
49
|
if (have.has(e))
|
|
47
50
|
return false;
|
|
48
|
-
|
|
51
|
+
if (filter.required.length === 0)
|
|
52
|
+
return true;
|
|
53
|
+
// OR by default (any required tag), AND under `--match-all` (every one).
|
|
54
|
+
return filter.matchAll
|
|
55
|
+
? filter.required.every((r) => have.has(r))
|
|
56
|
+
: filter.required.some((r) => have.has(r));
|
|
57
|
+
}
|
|
58
|
+
/** Nearest enclosing `test.group(...)` title, for the Japa `--groups` filter. */
|
|
59
|
+
function enclosingGroupTitle(node) {
|
|
60
|
+
for (let s = node.parent; s !== undefined; s = s.parent) {
|
|
61
|
+
if (s.isGroup)
|
|
62
|
+
return s.name;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
49
65
|
}
|
|
66
|
+
/** Whether the test's enclosing group is one of the `--groups` titles. */
|
|
67
|
+
function isInGroups(node, groups) {
|
|
68
|
+
const title = enclosingGroupTitle(node);
|
|
69
|
+
return title !== undefined && groups.has(title);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The value actually thrown, carried alongside its serialized form.
|
|
73
|
+
*
|
|
74
|
+
* A result crosses the worker→CLI IPC boundary, so it can only hold plain data.
|
|
75
|
+
* The EMITTER, though, runs in the worker — where the thrown `Error` still
|
|
76
|
+
* exists, and where a Japa reporter or plugin expects `errors[].error` to be an
|
|
77
|
+
* `Error` instance. A symbol key is invisible to `JSON.stringify` and to
|
|
78
|
+
* `Object.keys`, so the original rides along without ever reaching the frame.
|
|
79
|
+
*/
|
|
80
|
+
const THROWN = Symbol("helix.thrown");
|
|
81
|
+
/** The value a {@link SerializedError} was built from, when it is still around. */
|
|
82
|
+
export function thrownValue(error) {
|
|
83
|
+
return Reflect.get(error, THROWN);
|
|
84
|
+
}
|
|
85
|
+
function withThrown(serialized, thrown) {
|
|
86
|
+
Object.defineProperty(serialized, THROWN, {
|
|
87
|
+
value: thrown,
|
|
88
|
+
enumerable: false,
|
|
89
|
+
});
|
|
90
|
+
return serialized;
|
|
91
|
+
}
|
|
92
|
+
/** The reason Japa attaches to a test skipped because the run bailed. */
|
|
93
|
+
const BAIL_SKIP_REASON = "Skipped due to bail mode";
|
|
50
94
|
function serializeError(err) {
|
|
51
95
|
if (err instanceof AssertionError) {
|
|
52
|
-
return {
|
|
96
|
+
return withThrown({
|
|
53
97
|
name: err.name,
|
|
54
98
|
message: err.message,
|
|
55
99
|
stack: err.stack,
|
|
56
100
|
actual: err.actual,
|
|
57
101
|
expected: err.expected,
|
|
58
102
|
operator: err.operator,
|
|
59
|
-
};
|
|
103
|
+
}, err);
|
|
60
104
|
}
|
|
61
105
|
if (err instanceof Error) {
|
|
62
|
-
return { name: err.name, message: err.message, stack: err.stack };
|
|
106
|
+
return withThrown({ name: err.name, message: err.message, stack: err.stack }, err);
|
|
63
107
|
}
|
|
64
|
-
return { name: "NonError", message: String(err) };
|
|
108
|
+
return withThrown({ name: "NonError", message: String(err) }, err);
|
|
65
109
|
}
|
|
66
110
|
function hasOnly(node) {
|
|
67
111
|
if (node.mode === "only")
|
|
@@ -107,10 +151,12 @@ function collectHookChain(leaf, type) {
|
|
|
107
151
|
}
|
|
108
152
|
return chain;
|
|
109
153
|
}
|
|
110
|
-
async function runHooks(hooks, registerCleanups = false) {
|
|
154
|
+
async function runHooks(hooks, registerCleanups = false, subject) {
|
|
111
155
|
for (const h of hooks) {
|
|
112
156
|
try {
|
|
113
|
-
|
|
157
|
+
// Japa parity: test hooks receive the Test instance, group hooks the
|
|
158
|
+
// Group instance. Zero-arg hooks simply ignore it.
|
|
159
|
+
const ret = await h(subject);
|
|
114
160
|
// A `beforeEach` returning a function registers it as a test-scoped
|
|
115
161
|
// cleanup (Vitest/Japa parity). Ignored for `afterEach`.
|
|
116
162
|
if (registerCleanups && typeof ret === "function") {
|
|
@@ -144,115 +190,477 @@ function combineErrors(primary, secondary) {
|
|
|
144
190
|
operator: primary.operator,
|
|
145
191
|
};
|
|
146
192
|
}
|
|
147
|
-
|
|
148
|
-
if (ms <= 0)
|
|
149
|
-
return await p;
|
|
193
|
+
function makeTimeoutController(ms, label) {
|
|
150
194
|
let timer;
|
|
151
|
-
|
|
195
|
+
let rejectTimeout;
|
|
196
|
+
let current = ms;
|
|
197
|
+
const arm = () => {
|
|
198
|
+
if (current <= 0)
|
|
199
|
+
return;
|
|
152
200
|
timer = setTimeout(() => {
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
// Don't keep the event loop alive just for this watchdog.
|
|
201
|
+
rejectTimeout?.(new Error(`${label} exceeded ${current}ms timeout`));
|
|
202
|
+
}, current);
|
|
156
203
|
timer.unref?.();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
race(work) {
|
|
207
|
+
if (current <= 0)
|
|
208
|
+
return Promise.resolve(work);
|
|
209
|
+
const timeout = new Promise((_, reject) => {
|
|
210
|
+
rejectTimeout = reject;
|
|
211
|
+
arm();
|
|
212
|
+
});
|
|
213
|
+
return Promise.race([
|
|
214
|
+
Promise.resolve(work).then((v) => {
|
|
215
|
+
if (timer)
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
return v;
|
|
218
|
+
}),
|
|
219
|
+
timeout,
|
|
220
|
+
]);
|
|
221
|
+
},
|
|
222
|
+
reset(newMs) {
|
|
223
|
+
if (timer)
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
if (newMs !== undefined)
|
|
226
|
+
current = newMs;
|
|
227
|
+
arm();
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether a test survives the run's filters (Japa's refiner). `--grep` is a
|
|
233
|
+
* helix extra applied per resolved title, so it lives at the call sites that
|
|
234
|
+
* know the interpolated name rather than here.
|
|
235
|
+
*/
|
|
236
|
+
function isFilteredOut(node, ctx) {
|
|
237
|
+
return ((ctx.tagFilter !== undefined && !tagMatches(node, ctx.tagFilter)) ||
|
|
238
|
+
(ctx.testTitles !== undefined && !ctx.testTitles.has(node.name)) ||
|
|
239
|
+
(ctx.groupTitles !== undefined && !isInGroups(node, ctx.groupTitles)) ||
|
|
240
|
+
(ctx.onlyActive && !pathLeadsToOnly(node)));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Whether any test under this suite survives the filters — Japa's
|
|
244
|
+
* `Refiner#isGroupAllowed`: a group announces itself only when it has at least
|
|
245
|
+
* one runnable test, so a fully filtered-out group is invisible to reporters.
|
|
246
|
+
*/
|
|
247
|
+
function suiteHasRunnableTest(node, ctx) {
|
|
248
|
+
for (const child of node.children) {
|
|
249
|
+
if (child.kind === "test") {
|
|
250
|
+
if (!isFilteredOut(child, ctx))
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
else if (suiteHasRunnableTest(child, ctx)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
160
256
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The `meta` bag exposed as `ctx.test.options.meta` and on the test events —
|
|
261
|
+
* Japa's `{ suite, group, fileName, abort }`.
|
|
262
|
+
*/
|
|
263
|
+
function testMeta(node, ctx) {
|
|
264
|
+
return {
|
|
265
|
+
suite: ctx.suite,
|
|
266
|
+
group: enclosingGroup(node),
|
|
267
|
+
fileName: ctx.file,
|
|
268
|
+
// Japa's escape hatch: fail the running test with a given message.
|
|
269
|
+
abort: (message) => {
|
|
270
|
+
throw new Error(message);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Nearest enclosing `test.group(...)` instance, if any (Japa `meta.group`). */
|
|
275
|
+
function enclosingGroup(node) {
|
|
276
|
+
for (let s = node.parent; s !== undefined; s = s.parent) {
|
|
277
|
+
if (s.isGroup)
|
|
278
|
+
return s.groupInstance;
|
|
164
279
|
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The `test:start` payload for one concrete test (a plain test, or one dataset
|
|
284
|
+
* row). `test:end` is this object plus the outcome — exactly how Japa builds
|
|
285
|
+
* the two nodes.
|
|
286
|
+
*/
|
|
287
|
+
function buildTestStartNode(node, expandedTitle, ctx, dataset, flags) {
|
|
288
|
+
// Japa's `TestOptions`, field for field. `title`, `tags`, `timeout`, `meta`,
|
|
289
|
+
// `isPinned` — and, as its `Test` constructor initialises them, `isTodo` and
|
|
290
|
+
// `retries` — always carry a value. The rest appear only when the matching
|
|
291
|
+
// modifier was used, so a reporter reading `"isSkipped" in node` sees what
|
|
292
|
+
// Japa would show it. Pinned down by the golden tests, which compare the raw
|
|
293
|
+
// key set of both runners' nodes.
|
|
294
|
+
return {
|
|
295
|
+
title: { original: node.name, expanded: expandedTitle },
|
|
296
|
+
tags: node.tags ?? [],
|
|
297
|
+
timeout: node.timeoutMs ?? ctx.timeoutMs,
|
|
298
|
+
retries: node.retries ?? inheritedEach(node, "eachRetries") ?? ctx.retries,
|
|
299
|
+
waitsForDone: node.waitForDone === true ? true : undefined,
|
|
300
|
+
executor: node.fn ?? node.datasetBody,
|
|
301
|
+
isTodo: flags.isTodo,
|
|
302
|
+
isSkipped: flags.isSkipped ? true : undefined,
|
|
303
|
+
isFailing: node.failing === true ? true : undefined,
|
|
304
|
+
skipReason: flags.isSkipped ? node.reason : undefined,
|
|
305
|
+
failReason: node.failing === true ? node.reason : undefined,
|
|
306
|
+
isPinned: node.pinned === true,
|
|
307
|
+
meta: testMeta(node, ctx),
|
|
308
|
+
dataset,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** The `errors` array of a `test:end` / `group:end` node. */
|
|
312
|
+
function toEmittedErrors(error, phase) {
|
|
313
|
+
if (error === undefined)
|
|
314
|
+
return [];
|
|
315
|
+
// Japa types `errors[].error` as `Error`; hand listeners the real one when
|
|
316
|
+
// this process still has it, and fall back to the serialized shape for a
|
|
317
|
+
// result that was rebuilt from a frame.
|
|
318
|
+
const thrown = thrownValue(error);
|
|
319
|
+
return [{ phase, error: thrown instanceof Error ? thrown : error }];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Emit the Japa `test:start` / `test:end` pair for a test that never runs a
|
|
323
|
+
* body — a `todo`, an explicit `.skip()`, or a test whose skip condition threw.
|
|
324
|
+
* Japa announces those through its `DummyRunner`, back-to-back.
|
|
325
|
+
*
|
|
326
|
+
* Tests dropped by a FILTER (`--tags`/`--tests`/`--groups`/`--grep`/`.only`)
|
|
327
|
+
* are deliberately NOT emitted: Japa's refiner removes them before they can
|
|
328
|
+
* announce themselves, so a reporter never hears about them. They still show up
|
|
329
|
+
* as `skip` in {@link FileResult}, which is what the (Vitest-shaped) CLI
|
|
330
|
+
* reporter consumes.
|
|
331
|
+
*/
|
|
332
|
+
function emitTestResult(node, result, ctx, dataset) {
|
|
333
|
+
const start = buildTestStartNode(node, result.name, ctx, dataset, {
|
|
334
|
+
isTodo: result.status === "todo",
|
|
335
|
+
isSkipped: result.status === "skip",
|
|
336
|
+
});
|
|
337
|
+
emitter.emit("test:start", start);
|
|
338
|
+
emitter.emit("test:end", {
|
|
339
|
+
...start,
|
|
340
|
+
duration: result.durationMs,
|
|
341
|
+
hasError: result.status === "fail",
|
|
342
|
+
errors: toEmittedErrors(result.error, result.errorPhase ?? "test"),
|
|
343
|
+
});
|
|
165
344
|
}
|
|
166
345
|
async function runTest(node, parentFullName, ctx) {
|
|
167
|
-
const
|
|
168
|
-
|
|
346
|
+
const baseFullName = joinName(parentFullName, node.name);
|
|
347
|
+
// Filter gates (Japa's refiner). Computed first because a filtered-out test
|
|
348
|
+
// emits NOTHING — not even the `test:start`/`test:end` pair a `.skip()` or a
|
|
349
|
+
// `todo` still announces. `grep` is per-name (per row for datasets), so it
|
|
350
|
+
// stays below.
|
|
351
|
+
const filteredOut = isFilteredOut(node, ctx);
|
|
352
|
+
// A dataset test with no body (`test('x').with(rows)` and no `.run(fn)` / no
|
|
353
|
+
// body in `test`) is a `todo` — same as a bodiless plain test (Japa parity).
|
|
354
|
+
const datasetTodo = node.datasetFn !== undefined && node.datasetBody === undefined;
|
|
355
|
+
if (node.mode === "todo" || datasetTodo) {
|
|
169
356
|
const r = {
|
|
170
357
|
name: node.name,
|
|
171
|
-
fullName,
|
|
358
|
+
fullName: baseFullName,
|
|
172
359
|
status: "todo",
|
|
173
360
|
durationMs: 0,
|
|
174
361
|
};
|
|
175
362
|
ctx.flatTests.push(r);
|
|
176
|
-
|
|
363
|
+
if (!filteredOut)
|
|
364
|
+
emitTestResult(node, r, ctx);
|
|
365
|
+
return [r];
|
|
177
366
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
367
|
+
// Deferred skip condition (Japa: a `skip(fn)` callback — possibly async — is
|
|
368
|
+
// evaluated here at run time, not eagerly at collection). A throwing
|
|
369
|
+
// condition fails the test rather than silently skipping it.
|
|
370
|
+
let deferredSkip = false;
|
|
371
|
+
if (node.skipCondition !== undefined) {
|
|
372
|
+
try {
|
|
373
|
+
deferredSkip = Boolean(await node.skipCondition());
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
const r = {
|
|
377
|
+
name: node.name,
|
|
378
|
+
fullName: baseFullName,
|
|
379
|
+
status: "fail",
|
|
380
|
+
durationMs: 0,
|
|
381
|
+
error: serializeError(err),
|
|
382
|
+
errorPhase: "setup",
|
|
383
|
+
};
|
|
384
|
+
ctx.flatTests.push(r);
|
|
385
|
+
if (!filteredOut)
|
|
386
|
+
emitTestResult(node, r, ctx);
|
|
387
|
+
return [r];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Node-level gates that apply to the whole test (and every dataset row).
|
|
391
|
+
// `grep` is applied per-name below (per row for datasets). A bailed run skips
|
|
392
|
+
// what is left — Japa marks them `skip`, it does not drop them.
|
|
393
|
+
const nodeSkipped = node.mode === "skip" || deferredSkip || filteredOut || ctx.bailed;
|
|
394
|
+
// Japa marks a bailed-over test with `skip(true, "Skipped due to bail mode")`,
|
|
395
|
+
// so the reason travels on the node and reaches `skipReason` on the events.
|
|
396
|
+
// Mirrored here, on the same node, for the same reason.
|
|
397
|
+
if (ctx.bailed && node.reason === undefined)
|
|
398
|
+
node.reason = BAIL_SKIP_REASON;
|
|
399
|
+
const makeSkip = (name, fullName, emit, dataset) => {
|
|
400
|
+
const r = { name, fullName, status: "skip", durationMs: 0 };
|
|
189
401
|
ctx.flatTests.push(r);
|
|
402
|
+
if (emit)
|
|
403
|
+
emitTestResult(node, r, ctx, dataset);
|
|
190
404
|
return r;
|
|
405
|
+
};
|
|
406
|
+
const grepOut = (fullName) => ctx.grep !== undefined && !ctx.grep.test(fullName);
|
|
407
|
+
// Dataset expansion (Japa `test(name, fn).with(rows)`): resolve the rows at
|
|
408
|
+
// run time (awaiting an async source), then run one test per row with an
|
|
409
|
+
// interpolated title. The full resolved dataset is exposed as `ctx.test.dataset`.
|
|
410
|
+
if (node.datasetFn !== undefined) {
|
|
411
|
+
let rows;
|
|
412
|
+
try {
|
|
413
|
+
rows =
|
|
414
|
+
typeof node.datasetFn === "function"
|
|
415
|
+
? await node.datasetFn()
|
|
416
|
+
: node.datasetFn;
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const r = {
|
|
420
|
+
name: node.name,
|
|
421
|
+
fullName: baseFullName,
|
|
422
|
+
status: "fail",
|
|
423
|
+
durationMs: 0,
|
|
424
|
+
error: serializeError(err),
|
|
425
|
+
errorPhase: "setup",
|
|
426
|
+
};
|
|
427
|
+
ctx.flatTests.push(r);
|
|
428
|
+
if (!filteredOut)
|
|
429
|
+
emitTestResult(node, r, ctx);
|
|
430
|
+
return [r];
|
|
431
|
+
}
|
|
432
|
+
const results = [];
|
|
433
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
434
|
+
const title = interpolateDatasetTitle(node.name, rows[i], i);
|
|
435
|
+
const fullName = joinName(parentFullName, title);
|
|
436
|
+
const dataset = {
|
|
437
|
+
size: rows.length,
|
|
438
|
+
index: i,
|
|
439
|
+
row: rows[i],
|
|
440
|
+
};
|
|
441
|
+
if (nodeSkipped || grepOut(fullName)) {
|
|
442
|
+
const emit = !filteredOut && !grepOut(fullName);
|
|
443
|
+
results.push(makeSkip(title, fullName, emit, dataset));
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
results.push(await runOneTest(node, title, fullName, ctx, rows, rows[i], dataset));
|
|
447
|
+
}
|
|
448
|
+
return results;
|
|
449
|
+
}
|
|
450
|
+
// Single (non-dataset) test.
|
|
451
|
+
if (nodeSkipped || grepOut(baseFullName)) {
|
|
452
|
+
const emit = !filteredOut && !grepOut(baseFullName);
|
|
453
|
+
return [makeSkip(node.name, baseFullName, emit)];
|
|
191
454
|
}
|
|
455
|
+
return [
|
|
456
|
+
await runOneTest(node, node.name, baseFullName, ctx, undefined, undefined, undefined),
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Run one concrete test (a plain test, or one row of a dataset) through its
|
|
461
|
+
* retry loop and record the result. `title`/`fullName` are already resolved
|
|
462
|
+
* (interpolated for datasets); `dataset`/`row` are set for a dataset row.
|
|
463
|
+
*/
|
|
464
|
+
async function runOneTest(node, title, fullName, ctx, dataset, row, datasetNode) {
|
|
192
465
|
const before = collectHookChain(node.parent, "beforeEach");
|
|
193
466
|
const after = collectHookChain(node.parent, "afterEach");
|
|
194
|
-
|
|
195
|
-
|
|
467
|
+
// Resolution order: per-test override → nearest group `each.timeout`/`retry`
|
|
468
|
+
// → run-wide default.
|
|
469
|
+
const perTestTimeout = node.timeoutMs ?? inheritedEach(node, "eachTimeout") ?? ctx.timeoutMs;
|
|
470
|
+
const perTestRetries = node.retries ?? inheritedEach(node, "eachRetries") ?? ctx.retries;
|
|
471
|
+
const attempts = 1 + Math.max(0, perTestRetries);
|
|
196
472
|
const start = Date.now();
|
|
473
|
+
// Japa announces the test ONCE, before the first attempt; the retry loop
|
|
474
|
+
// lives inside the start/end pair and only the final attempt is reported.
|
|
475
|
+
const startNode = buildTestStartNode(node, title, ctx, datasetNode, {
|
|
476
|
+
isTodo: false,
|
|
477
|
+
isSkipped: false,
|
|
478
|
+
});
|
|
479
|
+
emitter.emit("test:start", startNode);
|
|
197
480
|
// Retry loop: each attempt runs the FULL cycle (beforeEach + body +
|
|
198
481
|
// afterEach) inside its own per-test frame so cleanups / outcome hooks /
|
|
199
482
|
// assertion counters reset between attempts. Passes on the first success.
|
|
200
483
|
let last;
|
|
484
|
+
// 1-based number of the attempt that produced `last` (Japa counts the first
|
|
485
|
+
// run as attempt 1), and only reported when the test opted into retries.
|
|
486
|
+
let attemptNumber = 1;
|
|
201
487
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
202
|
-
|
|
488
|
+
attemptNumber = attempt + 1;
|
|
489
|
+
last = await withTestContext(() => runAttempt(node, title, fullName, before, after, perTestTimeout, perTestRetries, start, dataset, row, testMeta(node, ctx)));
|
|
203
490
|
if (last.status === "pass")
|
|
204
491
|
break;
|
|
205
492
|
}
|
|
206
493
|
ctx.flatTests.push(last);
|
|
494
|
+
if (ctx.bail && last.status === "fail")
|
|
495
|
+
ctx.bailed = true;
|
|
496
|
+
emitter.emit("test:end", {
|
|
497
|
+
...startNode,
|
|
498
|
+
retryAttempt: perTestRetries > 0 ? attemptNumber : undefined,
|
|
499
|
+
duration: last.durationMs,
|
|
500
|
+
hasError: last.status === "fail",
|
|
501
|
+
errors: toEmittedErrors(last.error, last.errorPhase ?? "test"),
|
|
502
|
+
});
|
|
207
503
|
return last;
|
|
208
504
|
}
|
|
505
|
+
function noop() { }
|
|
506
|
+
/** Narrow an unknown value to a thenable without a cast. */
|
|
507
|
+
function isThenable(v) {
|
|
508
|
+
return (v !== null &&
|
|
509
|
+
(typeof v === "object" || typeof v === "function") &&
|
|
510
|
+
typeof Reflect.get(v, "then") === "function");
|
|
511
|
+
}
|
|
512
|
+
/** Nearest ancestor group's `each.timeout`/`each.retry` default for a test. */
|
|
513
|
+
function inheritedEach(node, key) {
|
|
514
|
+
for (let suite = node.parent; suite !== undefined; suite = suite.parent) {
|
|
515
|
+
const value = suite[key];
|
|
516
|
+
if (value !== undefined)
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
209
521
|
/** Run one full attempt of a test inside the active per-test frame. */
|
|
210
|
-
async function runAttempt(node, fullName, before, after, timeoutMs, start) {
|
|
211
|
-
|
|
522
|
+
async function runAttempt(node, title, fullName, before, after, timeoutMs, retries, start, dataset, row, meta) {
|
|
523
|
+
// A re-armable body timeout so `ctx.test.resetTimeout()` can push the deadline.
|
|
524
|
+
const timeoutCtl = makeTimeoutController(timeoutMs, `test "${fullName}"`);
|
|
525
|
+
// The running test's instance — injected as `ctx.test`, passed to the test
|
|
526
|
+
// hooks (Japa parity), and threaded into the frame so cleanups receive it.
|
|
527
|
+
const testInstance = {
|
|
528
|
+
title,
|
|
529
|
+
fullName,
|
|
530
|
+
options: {
|
|
531
|
+
title,
|
|
532
|
+
timeout: timeoutMs,
|
|
533
|
+
retries,
|
|
534
|
+
tags: node.tags ?? [],
|
|
535
|
+
isTodo: false,
|
|
536
|
+
// `test.fails()` — a Japa plugin reads it to know an error was the
|
|
537
|
+
// point (`@japa/assert` skips its assertion check for such a test).
|
|
538
|
+
isFailing: node.failing === true,
|
|
539
|
+
meta,
|
|
540
|
+
},
|
|
541
|
+
dataset: dataset ?? node.dataset,
|
|
542
|
+
isPinned: node.pinned === true,
|
|
543
|
+
resetTimeout: (ms) => timeoutCtl.reset(ms),
|
|
544
|
+
cleanup: (fn) => {
|
|
545
|
+
registerTestCleanup(fn);
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
setFrameTest(testInstance);
|
|
549
|
+
// Build the injected context BEFORE the `beforeEach` chain so hooks can reach
|
|
550
|
+
// it as `$test.context` (Japa parity) — the SAME context flows to the body.
|
|
551
|
+
// Built inside the per-test frame so `ctx.cleanup` / getters bind here.
|
|
552
|
+
const context = buildTestContext(testInstance);
|
|
553
|
+
// A Japa `Test.executed` hook reads `test.context` — `@japa/assert` validates
|
|
554
|
+
// its assertion count through it after every test.
|
|
555
|
+
setFrameContext(context);
|
|
556
|
+
testInstance.context = context;
|
|
557
|
+
const beforeErr = await runHooks(before, true, testInstance);
|
|
212
558
|
if (beforeErr) {
|
|
213
|
-
const afterErrBE = await runHooks(after);
|
|
559
|
+
const afterErrBE = await runHooks(after, false, testInstance);
|
|
214
560
|
await drainTestOutcomeHooks(true);
|
|
215
561
|
return {
|
|
216
|
-
name:
|
|
562
|
+
name: title,
|
|
217
563
|
fullName,
|
|
218
564
|
status: "fail",
|
|
219
565
|
durationMs: Date.now() - start,
|
|
220
566
|
error: combineErrors(beforeErr, afterErrBE),
|
|
567
|
+
errorPhase: "setup",
|
|
221
568
|
};
|
|
222
569
|
}
|
|
223
570
|
let testErr;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
// `test.fails()` inverts the body outcome: a throw is success, a clean
|
|
234
|
-
// run is a failure. Applied before assertion-count checks.
|
|
235
|
-
if (node.failing) {
|
|
236
|
-
testErr = testErr
|
|
237
|
-
? undefined
|
|
238
|
-
: {
|
|
239
|
-
name: "AssertionError",
|
|
240
|
-
message: `test "${fullName}" was expected to fail (test.fails) but passed`,
|
|
241
|
-
};
|
|
571
|
+
// Which phase produced `testErr` — surfaced on `test:end` (Japa parity).
|
|
572
|
+
let errorPhase;
|
|
573
|
+
// Per-test setup hooks (`test.setup`) run after the group `each.setup` chain,
|
|
574
|
+
// just before the body. A failing setup fails the test without running it.
|
|
575
|
+
const setupErr = await runHooks(node.setups ?? [], true, testInstance);
|
|
576
|
+
if (setupErr) {
|
|
577
|
+
testErr = setupErr;
|
|
578
|
+
errorPhase = "setup";
|
|
242
579
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
580
|
+
else {
|
|
581
|
+
try {
|
|
582
|
+
// `done` callback (Japa `waitForDone`): the test completes when the
|
|
583
|
+
// body calls done()/done(error). Built even when unused (harmless).
|
|
584
|
+
let doneResolve = noop;
|
|
585
|
+
let doneReject = noop;
|
|
586
|
+
const donePromise = new Promise((resolve, reject) => {
|
|
587
|
+
doneResolve = resolve;
|
|
588
|
+
doneReject = reject;
|
|
589
|
+
});
|
|
590
|
+
let doneCalled = false;
|
|
591
|
+
const done = (error) => {
|
|
592
|
+
if (doneCalled)
|
|
593
|
+
return;
|
|
594
|
+
doneCalled = true;
|
|
595
|
+
if (error !== undefined)
|
|
596
|
+
doneReject(error);
|
|
597
|
+
else
|
|
598
|
+
doneResolve();
|
|
599
|
+
};
|
|
600
|
+
// A dataset test runs its `datasetBody(ctx, row)`; a plain test its
|
|
601
|
+
// `fn(ctx, done)`.
|
|
602
|
+
const result = node.datasetFn !== undefined
|
|
603
|
+
? node.datasetBody?.(context, row)
|
|
604
|
+
: node.fn?.(context, done);
|
|
605
|
+
if (node.waitForDone) {
|
|
606
|
+
// Complete on done(); a body rejection still fails fast, but a body
|
|
607
|
+
// that merely RESOLVES does not complete the test (it must call done).
|
|
608
|
+
const body = isThenable(result) ? result : Promise.resolve();
|
|
609
|
+
const bodyRejectsOnly = body.then(() => new Promise(noop), (err) => {
|
|
610
|
+
throw err;
|
|
611
|
+
});
|
|
612
|
+
await timeoutCtl.race(Promise.race([donePromise, bodyRejectsOnly]));
|
|
613
|
+
}
|
|
614
|
+
else if (isThenable(result)) {
|
|
615
|
+
await timeoutCtl.race(result);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
testErr = serializeError(err);
|
|
620
|
+
errorPhase = "test";
|
|
621
|
+
}
|
|
622
|
+
// `test.fails()` inverts the body outcome: a throw is success, a clean
|
|
623
|
+
// run is a failure. Applied before assertion-count checks. Only when the
|
|
624
|
+
// body actually ran (a setup error is a hard failure, not an expected one).
|
|
625
|
+
if (node.failing) {
|
|
626
|
+
testErr = testErr
|
|
627
|
+
? undefined
|
|
628
|
+
: {
|
|
629
|
+
name: "AssertionError",
|
|
630
|
+
message: `test "${fullName}" was expected to fail (test.fails) but passed`,
|
|
631
|
+
};
|
|
632
|
+
errorPhase = testErr === undefined ? undefined : "test";
|
|
633
|
+
}
|
|
634
|
+
// Assertion-count enforcement (`expect.assertions(n)` / `hasAssertions()`).
|
|
635
|
+
if (!testErr) {
|
|
636
|
+
testErr = checkAssertionCount(fullName);
|
|
637
|
+
if (testErr !== undefined)
|
|
638
|
+
errorPhase = "test";
|
|
639
|
+
}
|
|
246
640
|
}
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
await
|
|
641
|
+
// Per-test teardown hooks (`test.teardown`) run before the group `each.teardown`.
|
|
642
|
+
const teardownErr = await runHooks(node.teardowns ?? [], false, testInstance);
|
|
643
|
+
const afterErr = await runHooks(after, false, testInstance);
|
|
644
|
+
const finalErr = combineErrors(combineErrors(testErr, teardownErr), afterErr);
|
|
645
|
+
// Record the outcome BEFORE the frame's cleanup drain (finally) fires, so
|
|
646
|
+
// `ctx.cleanup((hasError, test) => …)` sees the right `hasError`.
|
|
647
|
+
setFrameOutcome(finalErr !== undefined);
|
|
648
|
+
// A `Test.executed` hook is a verdict, not a teardown: `@japa/assert`
|
|
649
|
+
// validates `assert.plan(n)` there. Its throw has to reach the result, or a
|
|
650
|
+
// plugin's whole reason for existing passes green.
|
|
651
|
+
const executedErr = await drainTestOutcomeHooks(finalErr !== undefined);
|
|
652
|
+
const outcome = executedErr === undefined
|
|
653
|
+
? finalErr
|
|
654
|
+
: combineErrors(finalErr, serializeError(executedErr));
|
|
250
655
|
return {
|
|
251
|
-
name:
|
|
656
|
+
name: title,
|
|
252
657
|
fullName,
|
|
253
|
-
status:
|
|
658
|
+
status: outcome ? "fail" : "pass",
|
|
254
659
|
durationMs: Date.now() - start,
|
|
255
|
-
error:
|
|
660
|
+
error: outcome,
|
|
661
|
+
// A failure with no recorded phase came from the teardown chain, or from a
|
|
662
|
+
// `Test.executed` hook, which runs at the same point.
|
|
663
|
+
errorPhase: outcome === undefined ? undefined : (errorPhase ?? "teardown"),
|
|
256
664
|
};
|
|
257
665
|
}
|
|
258
666
|
/** Verify `expect.assertions(n)` / `hasAssertions()` for the active frame. */
|
|
@@ -290,9 +698,11 @@ function attributeHookFailure(node, parentFullName, err, ctx) {
|
|
|
290
698
|
status: child.mode === "todo" ? "todo" : "fail",
|
|
291
699
|
durationMs: 0,
|
|
292
700
|
error: child.mode === "todo" ? undefined : err,
|
|
701
|
+
errorPhase: child.mode === "todo" ? undefined : "setup",
|
|
293
702
|
};
|
|
294
703
|
children.push(r);
|
|
295
704
|
ctx.flatTests.push(r);
|
|
705
|
+
emitTestResult(child, r, ctx);
|
|
296
706
|
}
|
|
297
707
|
else {
|
|
298
708
|
const innerChildren = attributeHookFailure(child, fullName, err, ctx);
|
|
@@ -319,19 +729,67 @@ async function runSuite(node, parentFullName, ctx) {
|
|
|
319
729
|
if (skipEntire)
|
|
320
730
|
return runSkippedSuite(node, fullName, ctx);
|
|
321
731
|
const hookErrors = [];
|
|
732
|
+
// The group's instance (Japa parity) passed to group hooks, and cleanups a
|
|
733
|
+
// `group.setup()` returns — run in the afterAll phase with `(hadError, group)`.
|
|
734
|
+
// For a `test.group()` this is the SAME object the body received and the call
|
|
735
|
+
// returned (`self === group`); a plain `describe` suite has no group handle,
|
|
736
|
+
// so fall back to a bare identity object.
|
|
737
|
+
const group = node.groupInstance ?? {
|
|
738
|
+
title: node.name,
|
|
739
|
+
fullName,
|
|
740
|
+
};
|
|
741
|
+
const groupCleanups = [];
|
|
742
|
+
// A group with no runnable test announces nothing — Japa's refiner drops it
|
|
743
|
+
// before `Group.exec()` runs, so a reporter never sees it.
|
|
744
|
+
const groupFiltered = !suiteHasRunnableTest(node, ctx);
|
|
745
|
+
if (!groupFiltered) {
|
|
746
|
+
emitter.emit("group:start", {
|
|
747
|
+
title: node.name,
|
|
748
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
749
|
+
});
|
|
750
|
+
}
|
|
322
751
|
// beforeAll: when one throws, every descendant test inherits the failure
|
|
323
752
|
// and the children list is replaced with those attributed results.
|
|
324
|
-
const attributed = await runBeforeAllHooks(node, parentFullName, ctx, hookErrors);
|
|
753
|
+
const attributed = await runBeforeAllHooks(node, parentFullName, ctx, hookErrors, group, groupCleanups);
|
|
325
754
|
const beforeAllFailed = attributed !== null;
|
|
326
755
|
const children = attributed ?? [];
|
|
756
|
+
// `--bail-layer=group` confines a bail to the group it happened in, so the
|
|
757
|
+
// next group starts clean. The wider layers leave the flag set.
|
|
758
|
+
const bailedOnEntry = ctx.bailed;
|
|
327
759
|
if (!beforeAllFailed) {
|
|
328
760
|
for (const child of node.children) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
761
|
+
if (child.kind === "test") {
|
|
762
|
+
// A dataset test expands to one result per row (Japa parity).
|
|
763
|
+
for (const r of await runTest(child, fullName, ctx))
|
|
764
|
+
children.push(r);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
children.push(await runSuite(child, fullName, ctx));
|
|
768
|
+
}
|
|
332
769
|
}
|
|
333
770
|
}
|
|
334
|
-
|
|
771
|
+
if (ctx.bailLayer === "group")
|
|
772
|
+
ctx.bailed = bailedOnEntry;
|
|
773
|
+
const groupHadError = hookErrors.length > 0 || children.some((c) => c.status === "fail");
|
|
774
|
+
// Errors recorded so far come from `beforeAll`; anything appended by
|
|
775
|
+
// `runAfterAllHooks` below belongs to the teardown phase.
|
|
776
|
+
const setupErrorCount = hookErrors.length;
|
|
777
|
+
await runAfterAllHooks(node, hookErrors, group, groupCleanups, groupHadError);
|
|
778
|
+
if (!groupFiltered) {
|
|
779
|
+
emitter.emit("group:end", {
|
|
780
|
+
title: node.name,
|
|
781
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
782
|
+
// True when ANYTHING under the group failed — a hook or a test — which
|
|
783
|
+
// is what Japa's GroupRunner reports.
|
|
784
|
+
hasError: groupHadError || hookErrors.length > 0,
|
|
785
|
+
// The group's own `errors` are its hook failures only; a test's failure
|
|
786
|
+
// travels on that test's `test:end` (Japa parity).
|
|
787
|
+
errors: hookErrors.map((error, i) => ({
|
|
788
|
+
phase: i < setupErrorCount ? "setup" : "teardown",
|
|
789
|
+
error,
|
|
790
|
+
})),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
335
793
|
// Surface afterAll failures as a synthetic test so `totals.fail` reflects
|
|
336
794
|
// them and CI exits nonzero.
|
|
337
795
|
if (!beforeAllFailed && hookErrors.length > 0) {
|
|
@@ -357,6 +815,12 @@ async function runSuite(node, parentFullName, ctx) {
|
|
|
357
815
|
/** Build skip/todo results for an entirely-skipped suite (no hooks run). */
|
|
358
816
|
async function runSkippedSuite(node, fullName, ctx) {
|
|
359
817
|
const children = [];
|
|
818
|
+
// The group still announces itself — its tests are skipped, not filtered
|
|
819
|
+
// out — so a reporter can nest them under it.
|
|
820
|
+
emitter.emit("group:start", {
|
|
821
|
+
title: node.name,
|
|
822
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
823
|
+
});
|
|
360
824
|
for (const child of node.children) {
|
|
361
825
|
if (child.kind === "test") {
|
|
362
826
|
const r = {
|
|
@@ -367,11 +831,18 @@ async function runSkippedSuite(node, fullName, ctx) {
|
|
|
367
831
|
};
|
|
368
832
|
children.push(r);
|
|
369
833
|
ctx.flatTests.push(r);
|
|
834
|
+
emitTestResult(child, r, ctx);
|
|
370
835
|
}
|
|
371
836
|
else {
|
|
372
837
|
children.push(await runSuiteSkip(child, fullName, ctx));
|
|
373
838
|
}
|
|
374
839
|
}
|
|
840
|
+
emitter.emit("group:end", {
|
|
841
|
+
title: node.name,
|
|
842
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
843
|
+
hasError: false,
|
|
844
|
+
errors: [],
|
|
845
|
+
});
|
|
375
846
|
return {
|
|
376
847
|
name: node.name,
|
|
377
848
|
fullName,
|
|
@@ -386,12 +857,16 @@ async function runSkippedSuite(node, fullName, ctx) {
|
|
|
386
857
|
* error to `hookErrors` and return the descendant tests attributed with that
|
|
387
858
|
* failure; return `null` when all hooks pass.
|
|
388
859
|
*/
|
|
389
|
-
async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors) {
|
|
860
|
+
async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors, group, groupCleanups) {
|
|
390
861
|
for (const h of node.hooks) {
|
|
391
862
|
if (h.type !== "beforeAll")
|
|
392
863
|
continue;
|
|
393
864
|
try {
|
|
394
|
-
|
|
865
|
+
// Japa parity: group hooks receive the Group instance; a returned
|
|
866
|
+
// function becomes a group-scoped cleanup (run in the afterAll phase).
|
|
867
|
+
const ret = await h.fn(group);
|
|
868
|
+
if (typeof ret === "function")
|
|
869
|
+
groupCleanups.push(ret);
|
|
395
870
|
}
|
|
396
871
|
catch (err) {
|
|
397
872
|
const serialized = serializeError(err);
|
|
@@ -406,12 +881,22 @@ async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors) {
|
|
|
406
881
|
* Run `afterAll` hooks unconditionally so partial setup from a failed beforeAll
|
|
407
882
|
* can be released. Errors are captured into `hookErrors`, not thrown.
|
|
408
883
|
*/
|
|
409
|
-
async function runAfterAllHooks(node, hookErrors) {
|
|
884
|
+
async function runAfterAllHooks(node, hookErrors, group, groupCleanups, hadError) {
|
|
885
|
+
// Group-scoped cleanups (returned by `group.setup()`) run first, in reverse
|
|
886
|
+
// insertion order, receiving `(hadError, group)` — Japa lifecycle parity.
|
|
887
|
+
for (let i = groupCleanups.length - 1; i >= 0; i -= 1) {
|
|
888
|
+
try {
|
|
889
|
+
await groupCleanups[i](hadError, group);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
hookErrors.push(serializeError(err));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
410
895
|
for (const h of node.hooks) {
|
|
411
896
|
if (h.type !== "afterAll")
|
|
412
897
|
continue;
|
|
413
898
|
try {
|
|
414
|
-
await h.fn();
|
|
899
|
+
await h.fn(group);
|
|
415
900
|
}
|
|
416
901
|
catch (err) {
|
|
417
902
|
hookErrors.push(serializeError(err));
|
|
@@ -431,6 +916,10 @@ function suiteStatus(children, hookErrors) {
|
|
|
431
916
|
async function runSuiteSkip(node, parentFullName, ctx) {
|
|
432
917
|
const fullName = joinName(parentFullName, node.name);
|
|
433
918
|
const children = [];
|
|
919
|
+
emitter.emit("group:start", {
|
|
920
|
+
title: node.name,
|
|
921
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
922
|
+
});
|
|
434
923
|
for (const child of node.children) {
|
|
435
924
|
if (child.kind === "test") {
|
|
436
925
|
const r = {
|
|
@@ -441,11 +930,18 @@ async function runSuiteSkip(node, parentFullName, ctx) {
|
|
|
441
930
|
};
|
|
442
931
|
children.push(r);
|
|
443
932
|
ctx.flatTests.push(r);
|
|
933
|
+
emitTestResult(child, r, ctx);
|
|
444
934
|
}
|
|
445
935
|
else {
|
|
446
936
|
children.push(await runSuiteSkip(child, fullName, ctx));
|
|
447
937
|
}
|
|
448
938
|
}
|
|
939
|
+
emitter.emit("group:end", {
|
|
940
|
+
title: node.name,
|
|
941
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
942
|
+
hasError: false,
|
|
943
|
+
errors: [],
|
|
944
|
+
});
|
|
449
945
|
return {
|
|
450
946
|
name: node.name,
|
|
451
947
|
fullName,
|
|
@@ -464,9 +960,31 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
464
960
|
timeoutMs: options.timeoutMs ?? 0,
|
|
465
961
|
retries: options.retries ?? 0,
|
|
466
962
|
grep: compileGrep(options.grep),
|
|
467
|
-
tagFilter: compileTagFilter(options.tags),
|
|
963
|
+
tagFilter: compileTagFilter(options.tags, options.matchAll === true),
|
|
964
|
+
testTitles: options.tests && options.tests.length > 0
|
|
965
|
+
? new Set(options.tests)
|
|
966
|
+
: undefined,
|
|
967
|
+
groupTitles: options.groups && options.groups.length > 0
|
|
968
|
+
? new Set(options.groups)
|
|
969
|
+
: undefined,
|
|
970
|
+
file,
|
|
971
|
+
suite: { name: options.suite ?? "default" },
|
|
972
|
+
bail: options.bail === true,
|
|
973
|
+
// Japa spells the runner layer as an empty string on its CLI.
|
|
974
|
+
bailLayer: options.bailLayer === undefined || options.bailLayer === ""
|
|
975
|
+
? "runner"
|
|
976
|
+
: options.bailLayer,
|
|
977
|
+
bailed: false,
|
|
468
978
|
};
|
|
469
979
|
const suites = [];
|
|
980
|
+
// Japa's runner/suite frame. Helix has no named-suite layer (one process per
|
|
981
|
+
// file), so the FILE is the suite — `suite:start` carries its name.
|
|
982
|
+
// Japa skips a suite whose every test is filtered out — the suite never
|
|
983
|
+
// announces itself, though the runner still opens and closes the run.
|
|
984
|
+
const suiteRunnable = suiteHasRunnableTest(root, ctx);
|
|
985
|
+
emitter.emit("runner:start", {});
|
|
986
|
+
if (suiteRunnable)
|
|
987
|
+
emitter.emit("suite:start", { name: ctx.suite.name });
|
|
470
988
|
// Root-level beforeAll: run once before anything, root-level afterAll:
|
|
471
989
|
// once after everything. Errors attribute to a synthetic test entry so
|
|
472
990
|
// they show up in totals.
|
|
@@ -496,17 +1014,14 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
496
1014
|
if (!rootBeforeAllFailed) {
|
|
497
1015
|
for (const child of root.children) {
|
|
498
1016
|
if (child.kind === "test") {
|
|
499
|
-
|
|
1017
|
+
// A dataset test expands to one result per row (Japa parity).
|
|
1018
|
+
const trs = await runTest(child, "", ctx);
|
|
500
1019
|
suites.push({
|
|
501
1020
|
name: "",
|
|
502
1021
|
fullName: "",
|
|
503
|
-
children:
|
|
504
|
-
status:
|
|
505
|
-
|
|
506
|
-
: tr.status === "pass"
|
|
507
|
-
? "pass"
|
|
508
|
-
: "skip",
|
|
509
|
-
durationMs: tr.durationMs,
|
|
1022
|
+
children: trs,
|
|
1023
|
+
status: suiteStatus(trs, []),
|
|
1024
|
+
durationMs: trs.reduce((sum, t) => sum + t.durationMs, 0),
|
|
510
1025
|
hookErrors: [],
|
|
511
1026
|
});
|
|
512
1027
|
}
|
|
@@ -538,6 +1053,15 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
538
1053
|
const totals = { pass: 0, fail: 0, skip: 0, todo: 0 };
|
|
539
1054
|
for (const t of ctx.flatTests)
|
|
540
1055
|
totals[t.status] += 1;
|
|
1056
|
+
const hasError = totals.fail > 0;
|
|
1057
|
+
if (suiteRunnable) {
|
|
1058
|
+
emitter.emit("suite:end", {
|
|
1059
|
+
name: ctx.suite.name,
|
|
1060
|
+
hasError,
|
|
1061
|
+
errors: rootHookErrors.map((error) => ({ phase: "setup", error })),
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
emitter.emit("runner:end", { hasError });
|
|
541
1065
|
return {
|
|
542
1066
|
file,
|
|
543
1067
|
suites,
|