@c9up/helix 0.1.7 → 0.1.9
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 +282 -11
- package/bin/helix.js +329 -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 +185 -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 +12 -0
- 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.d.ts +34 -0
- package/dist/runtime/asymmetric.d.ts.map +1 -0
- package/dist/runtime/asymmetric.js +78 -0
- package/dist/runtime/asymmetric.js.map +1 -0
- 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/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 +21 -0
- package/dist/runtime/equals.js.map +1 -1
- package/dist/runtime/expect.d.ts +22 -1
- package/dist/runtime/expect.d.ts.map +1 -1
- package/dist/runtime/expect.js +15 -1
- package/dist/runtime/expect.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 +16 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +7 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/matchers.d.ts +5 -0
- package/dist/runtime/matchers.d.ts.map +1 -1
- package/dist/runtime/matchers.js +117 -0
- package/dist/runtime/matchers.js.map +1 -1
- package/dist/runtime/run.d.ts +63 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +696 -79
- 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 +316 -10
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +307 -8
- package/dist/runtime/suite.js.map +1 -1
- package/dist/runtime/test-context.d.ts +88 -1
- package/dist/runtime/test-context.d.ts.map +1 -1
- package/dist/runtime/test-context.js +170 -2
- package/dist/runtime/test-context.js.map +1 -1
- package/dist/runtime/vi/fake-timers.d.ts.map +1 -1
- package/dist/runtime/vi/fake-timers.js +3 -4
- package/dist/runtime/vi/fake-timers.js.map +1 -1
- package/dist/runtime/worker.d.ts.map +1 -1
- package/dist/runtime/worker.js +87 -5
- 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 +10 -1
package/dist/runtime/run.js
CHANGED
|
@@ -7,22 +7,105 @@
|
|
|
7
7
|
* tree, non-`only` paths are downgraded to `skip` before execution.
|
|
8
8
|
*/
|
|
9
9
|
import { AssertionError } from "./assertion-error.js";
|
|
10
|
-
import {
|
|
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) {
|
|
15
|
+
if (!tags || tags.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
const required = [];
|
|
18
|
+
const excluded = [];
|
|
19
|
+
for (const raw of tags) {
|
|
20
|
+
const t = raw.trim();
|
|
21
|
+
if (!t)
|
|
22
|
+
continue;
|
|
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("!"))
|
|
26
|
+
excluded.push(t.slice(1));
|
|
27
|
+
else
|
|
28
|
+
required.push(t);
|
|
29
|
+
}
|
|
30
|
+
if (required.length === 0 && excluded.length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
return { required, excluded, matchAll };
|
|
33
|
+
}
|
|
34
|
+
function compileGrep(grep) {
|
|
35
|
+
if (!grep)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
return new RegExp(grep);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A malformed regex falls back to a literal substring match.
|
|
42
|
+
return new RegExp(grep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function tagMatches(node, filter) {
|
|
46
|
+
const have = new Set(node.tags ?? []);
|
|
47
|
+
// Exclusions always win.
|
|
48
|
+
for (const e of filter.excluded)
|
|
49
|
+
if (have.has(e))
|
|
50
|
+
return false;
|
|
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;
|
|
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";
|
|
11
94
|
function serializeError(err) {
|
|
12
95
|
if (err instanceof AssertionError) {
|
|
13
|
-
return {
|
|
96
|
+
return withThrown({
|
|
14
97
|
name: err.name,
|
|
15
98
|
message: err.message,
|
|
16
99
|
stack: err.stack,
|
|
17
100
|
actual: err.actual,
|
|
18
101
|
expected: err.expected,
|
|
19
102
|
operator: err.operator,
|
|
20
|
-
};
|
|
103
|
+
}, err);
|
|
21
104
|
}
|
|
22
105
|
if (err instanceof Error) {
|
|
23
|
-
return { name: err.name, message: err.message, stack: err.stack };
|
|
106
|
+
return withThrown({ name: err.name, message: err.message, stack: err.stack }, err);
|
|
24
107
|
}
|
|
25
|
-
return { name: "NonError", message: String(err) };
|
|
108
|
+
return withThrown({ name: "NonError", message: String(err) }, err);
|
|
26
109
|
}
|
|
27
110
|
function hasOnly(node) {
|
|
28
111
|
if (node.mode === "only")
|
|
@@ -68,10 +151,17 @@ function collectHookChain(leaf, type) {
|
|
|
68
151
|
}
|
|
69
152
|
return chain;
|
|
70
153
|
}
|
|
71
|
-
async function runHooks(hooks) {
|
|
154
|
+
async function runHooks(hooks, registerCleanups = false, subject) {
|
|
72
155
|
for (const h of hooks) {
|
|
73
156
|
try {
|
|
74
|
-
|
|
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);
|
|
160
|
+
// A `beforeEach` returning a function registers it as a test-scoped
|
|
161
|
+
// cleanup (Vitest/Japa parity). Ignored for `afterEach`.
|
|
162
|
+
if (registerCleanups && typeof ret === "function") {
|
|
163
|
+
registerTestCleanup(ret);
|
|
164
|
+
}
|
|
75
165
|
}
|
|
76
166
|
catch (err) {
|
|
77
167
|
return serializeError(err);
|
|
@@ -100,89 +190,497 @@ function combineErrors(primary, secondary) {
|
|
|
100
190
|
operator: primary.operator,
|
|
101
191
|
};
|
|
102
192
|
}
|
|
103
|
-
|
|
104
|
-
if (ms <= 0)
|
|
105
|
-
return await p;
|
|
193
|
+
function makeTimeoutController(ms, label) {
|
|
106
194
|
let timer;
|
|
107
|
-
|
|
195
|
+
let rejectTimeout;
|
|
196
|
+
let current = ms;
|
|
197
|
+
const arm = () => {
|
|
198
|
+
if (current <= 0)
|
|
199
|
+
return;
|
|
108
200
|
timer = setTimeout(() => {
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
// Don't keep the event loop alive just for this watchdog.
|
|
201
|
+
rejectTimeout?.(new Error(`${label} exceeded ${current}ms timeout`));
|
|
202
|
+
}, current);
|
|
112
203
|
timer.unref?.();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
116
256
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
120
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
|
+
});
|
|
121
344
|
}
|
|
122
345
|
async function runTest(node, parentFullName, ctx) {
|
|
123
|
-
const
|
|
124
|
-
|
|
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) {
|
|
125
356
|
const r = {
|
|
126
357
|
name: node.name,
|
|
127
|
-
fullName,
|
|
358
|
+
fullName: baseFullName,
|
|
128
359
|
status: "todo",
|
|
129
360
|
durationMs: 0,
|
|
130
361
|
};
|
|
131
362
|
ctx.flatTests.push(r);
|
|
132
|
-
|
|
363
|
+
if (!filteredOut)
|
|
364
|
+
emitTestResult(node, r, ctx);
|
|
365
|
+
return [r];
|
|
133
366
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 };
|
|
141
401
|
ctx.flatTests.push(r);
|
|
402
|
+
if (emit)
|
|
403
|
+
emitTestResult(node, r, ctx, dataset);
|
|
142
404
|
return r;
|
|
143
|
-
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 = {
|
|
156
420
|
name: node.name,
|
|
157
|
-
fullName,
|
|
421
|
+
fullName: baseFullName,
|
|
158
422
|
status: "fail",
|
|
159
|
-
durationMs:
|
|
160
|
-
error:
|
|
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],
|
|
161
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));
|
|
162
447
|
}
|
|
163
|
-
|
|
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)];
|
|
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) {
|
|
465
|
+
const before = collectHookChain(node.parent, "beforeEach");
|
|
466
|
+
const after = collectHookChain(node.parent, "afterEach");
|
|
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);
|
|
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);
|
|
480
|
+
// Retry loop: each attempt runs the FULL cycle (beforeEach + body +
|
|
481
|
+
// afterEach) inside its own per-test frame so cleanups / outcome hooks /
|
|
482
|
+
// assertion counters reset between attempts. Passes on the first success.
|
|
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;
|
|
487
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
488
|
+
attemptNumber = attempt + 1;
|
|
489
|
+
last = await withTestContext(() => runAttempt(node, title, fullName, before, after, perTestTimeout, perTestRetries, start, dataset, row, testMeta(node, ctx)));
|
|
490
|
+
if (last.status === "pass")
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
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
|
+
});
|
|
503
|
+
return last;
|
|
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
|
+
}
|
|
521
|
+
/** Run one full attempt of a test inside the active per-test frame. */
|
|
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);
|
|
558
|
+
if (beforeErr) {
|
|
559
|
+
const afterErrBE = await runHooks(after, false, testInstance);
|
|
560
|
+
await drainTestOutcomeHooks(true);
|
|
561
|
+
return {
|
|
562
|
+
name: title,
|
|
563
|
+
fullName,
|
|
564
|
+
status: "fail",
|
|
565
|
+
durationMs: Date.now() - start,
|
|
566
|
+
error: combineErrors(beforeErr, afterErrBE),
|
|
567
|
+
errorPhase: "setup",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
let testErr;
|
|
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";
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
164
581
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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);
|
|
169
616
|
}
|
|
170
617
|
}
|
|
171
618
|
catch (err) {
|
|
172
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";
|
|
173
639
|
}
|
|
174
|
-
|
|
175
|
-
|
|
640
|
+
}
|
|
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));
|
|
655
|
+
return {
|
|
656
|
+
name: title,
|
|
657
|
+
fullName,
|
|
658
|
+
status: outcome ? "fail" : "pass",
|
|
659
|
+
durationMs: Date.now() - start,
|
|
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"),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/** Verify `expect.assertions(n)` / `hasAssertions()` for the active frame. */
|
|
667
|
+
function checkAssertionCount(fullName) {
|
|
668
|
+
const state = getAssertionState();
|
|
669
|
+
if (!state)
|
|
670
|
+
return undefined;
|
|
671
|
+
if (state.expected !== undefined && state.count !== state.expected) {
|
|
176
672
|
return {
|
|
177
|
-
name:
|
|
178
|
-
fullName
|
|
179
|
-
status: finalErr ? "fail" : "pass",
|
|
180
|
-
durationMs: Date.now() - start,
|
|
181
|
-
error: finalErr,
|
|
673
|
+
name: "AssertionError",
|
|
674
|
+
message: `test "${fullName}" expected ${state.expected} assertion(s) but ran ${state.count}`,
|
|
182
675
|
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
676
|
+
}
|
|
677
|
+
if (state.hasAssertions && state.count === 0) {
|
|
678
|
+
return {
|
|
679
|
+
name: "AssertionError",
|
|
680
|
+
message: `test "${fullName}" expected at least one assertion but ran none`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return undefined;
|
|
186
684
|
}
|
|
187
685
|
/**
|
|
188
686
|
* Recursively mark every descendant test as failed due to an ancestor
|
|
@@ -200,9 +698,11 @@ function attributeHookFailure(node, parentFullName, err, ctx) {
|
|
|
200
698
|
status: child.mode === "todo" ? "todo" : "fail",
|
|
201
699
|
durationMs: 0,
|
|
202
700
|
error: child.mode === "todo" ? undefined : err,
|
|
701
|
+
errorPhase: child.mode === "todo" ? undefined : "setup",
|
|
203
702
|
};
|
|
204
703
|
children.push(r);
|
|
205
704
|
ctx.flatTests.push(r);
|
|
705
|
+
emitTestResult(child, r, ctx);
|
|
206
706
|
}
|
|
207
707
|
else {
|
|
208
708
|
const innerChildren = attributeHookFailure(child, fullName, err, ctx);
|
|
@@ -229,19 +729,67 @@ async function runSuite(node, parentFullName, ctx) {
|
|
|
229
729
|
if (skipEntire)
|
|
230
730
|
return runSkippedSuite(node, fullName, ctx);
|
|
231
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
|
+
}
|
|
232
751
|
// beforeAll: when one throws, every descendant test inherits the failure
|
|
233
752
|
// and the children list is replaced with those attributed results.
|
|
234
|
-
const attributed = await runBeforeAllHooks(node, parentFullName, ctx, hookErrors);
|
|
753
|
+
const attributed = await runBeforeAllHooks(node, parentFullName, ctx, hookErrors, group, groupCleanups);
|
|
235
754
|
const beforeAllFailed = attributed !== null;
|
|
236
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;
|
|
237
759
|
if (!beforeAllFailed) {
|
|
238
760
|
for (const child of node.children) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
|
242
769
|
}
|
|
243
770
|
}
|
|
244
|
-
|
|
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
|
+
}
|
|
245
793
|
// Surface afterAll failures as a synthetic test so `totals.fail` reflects
|
|
246
794
|
// them and CI exits nonzero.
|
|
247
795
|
if (!beforeAllFailed && hookErrors.length > 0) {
|
|
@@ -267,6 +815,12 @@ async function runSuite(node, parentFullName, ctx) {
|
|
|
267
815
|
/** Build skip/todo results for an entirely-skipped suite (no hooks run). */
|
|
268
816
|
async function runSkippedSuite(node, fullName, ctx) {
|
|
269
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
|
+
});
|
|
270
824
|
for (const child of node.children) {
|
|
271
825
|
if (child.kind === "test") {
|
|
272
826
|
const r = {
|
|
@@ -277,11 +831,18 @@ async function runSkippedSuite(node, fullName, ctx) {
|
|
|
277
831
|
};
|
|
278
832
|
children.push(r);
|
|
279
833
|
ctx.flatTests.push(r);
|
|
834
|
+
emitTestResult(child, r, ctx);
|
|
280
835
|
}
|
|
281
836
|
else {
|
|
282
837
|
children.push(await runSuiteSkip(child, fullName, ctx));
|
|
283
838
|
}
|
|
284
839
|
}
|
|
840
|
+
emitter.emit("group:end", {
|
|
841
|
+
title: node.name,
|
|
842
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
843
|
+
hasError: false,
|
|
844
|
+
errors: [],
|
|
845
|
+
});
|
|
285
846
|
return {
|
|
286
847
|
name: node.name,
|
|
287
848
|
fullName,
|
|
@@ -296,12 +857,16 @@ async function runSkippedSuite(node, fullName, ctx) {
|
|
|
296
857
|
* error to `hookErrors` and return the descendant tests attributed with that
|
|
297
858
|
* failure; return `null` when all hooks pass.
|
|
298
859
|
*/
|
|
299
|
-
async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors) {
|
|
860
|
+
async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors, group, groupCleanups) {
|
|
300
861
|
for (const h of node.hooks) {
|
|
301
862
|
if (h.type !== "beforeAll")
|
|
302
863
|
continue;
|
|
303
864
|
try {
|
|
304
|
-
|
|
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);
|
|
305
870
|
}
|
|
306
871
|
catch (err) {
|
|
307
872
|
const serialized = serializeError(err);
|
|
@@ -316,12 +881,22 @@ async function runBeforeAllHooks(node, parentFullName, ctx, hookErrors) {
|
|
|
316
881
|
* Run `afterAll` hooks unconditionally so partial setup from a failed beforeAll
|
|
317
882
|
* can be released. Errors are captured into `hookErrors`, not thrown.
|
|
318
883
|
*/
|
|
319
|
-
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
|
+
}
|
|
320
895
|
for (const h of node.hooks) {
|
|
321
896
|
if (h.type !== "afterAll")
|
|
322
897
|
continue;
|
|
323
898
|
try {
|
|
324
|
-
await h.fn();
|
|
899
|
+
await h.fn(group);
|
|
325
900
|
}
|
|
326
901
|
catch (err) {
|
|
327
902
|
hookErrors.push(serializeError(err));
|
|
@@ -341,6 +916,10 @@ function suiteStatus(children, hookErrors) {
|
|
|
341
916
|
async function runSuiteSkip(node, parentFullName, ctx) {
|
|
342
917
|
const fullName = joinName(parentFullName, node.name);
|
|
343
918
|
const children = [];
|
|
919
|
+
emitter.emit("group:start", {
|
|
920
|
+
title: node.name,
|
|
921
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
922
|
+
});
|
|
344
923
|
for (const child of node.children) {
|
|
345
924
|
if (child.kind === "test") {
|
|
346
925
|
const r = {
|
|
@@ -351,11 +930,18 @@ async function runSuiteSkip(node, parentFullName, ctx) {
|
|
|
351
930
|
};
|
|
352
931
|
children.push(r);
|
|
353
932
|
ctx.flatTests.push(r);
|
|
933
|
+
emitTestResult(child, r, ctx);
|
|
354
934
|
}
|
|
355
935
|
else {
|
|
356
936
|
children.push(await runSuiteSkip(child, fullName, ctx));
|
|
357
937
|
}
|
|
358
938
|
}
|
|
939
|
+
emitter.emit("group:end", {
|
|
940
|
+
title: node.name,
|
|
941
|
+
meta: { suite: ctx.suite, fileName: ctx.file },
|
|
942
|
+
hasError: false,
|
|
943
|
+
errors: [],
|
|
944
|
+
});
|
|
359
945
|
return {
|
|
360
946
|
name: node.name,
|
|
361
947
|
fullName,
|
|
@@ -372,8 +958,33 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
372
958
|
onlyActive,
|
|
373
959
|
flatTests: [],
|
|
374
960
|
timeoutMs: options.timeoutMs ?? 0,
|
|
961
|
+
retries: options.retries ?? 0,
|
|
962
|
+
grep: compileGrep(options.grep),
|
|
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,
|
|
375
978
|
};
|
|
376
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 });
|
|
377
988
|
// Root-level beforeAll: run once before anything, root-level afterAll:
|
|
378
989
|
// once after everything. Errors attribute to a synthetic test entry so
|
|
379
990
|
// they show up in totals.
|
|
@@ -403,17 +1014,14 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
403
1014
|
if (!rootBeforeAllFailed) {
|
|
404
1015
|
for (const child of root.children) {
|
|
405
1016
|
if (child.kind === "test") {
|
|
406
|
-
|
|
1017
|
+
// A dataset test expands to one result per row (Japa parity).
|
|
1018
|
+
const trs = await runTest(child, "", ctx);
|
|
407
1019
|
suites.push({
|
|
408
1020
|
name: "",
|
|
409
1021
|
fullName: "",
|
|
410
|
-
children:
|
|
411
|
-
status:
|
|
412
|
-
|
|
413
|
-
: tr.status === "pass"
|
|
414
|
-
? "pass"
|
|
415
|
-
: "skip",
|
|
416
|
-
durationMs: tr.durationMs,
|
|
1022
|
+
children: trs,
|
|
1023
|
+
status: suiteStatus(trs, []),
|
|
1024
|
+
durationMs: trs.reduce((sum, t) => sum + t.durationMs, 0),
|
|
417
1025
|
hookErrors: [],
|
|
418
1026
|
});
|
|
419
1027
|
}
|
|
@@ -445,6 +1053,15 @@ export async function executeRoot(root, file, options = {}) {
|
|
|
445
1053
|
const totals = { pass: 0, fail: 0, skip: 0, todo: 0 };
|
|
446
1054
|
for (const t of ctx.flatTests)
|
|
447
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 });
|
|
448
1065
|
return {
|
|
449
1066
|
file,
|
|
450
1067
|
suites,
|