Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 40 additions & 37 deletions src/classes/TestResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,20 @@ export default class TestResult extends BubblingEventTarget {
this.stats.total = this.test.testCount;
this.stats.pending = this.stats.total;
this.finished = new Promise(resolve =>
this.addEventListener("finish", resolve, { once: true }));
this.addEventListener(
"finish",
async () => {
await Promise.allSettled((this.tests ?? []).map(t => t.finished));
if (!this.parent?.error) {
try {
await this.test.afterAll?.();
}
catch {}
}
resolve();
},
{ once: true },
));

let tests = this.test.tests;
let childOptions = Object.assign({}, this.options);
Expand Down Expand Up @@ -218,48 +231,38 @@ export default class TestResult extends BubblingEventTarget {

this.tests = this.test.tests?.map(t => new TestResult(t, this, childOptions));

delay(1)
.then(async () => {
let error = this.parent?.error;
delay(1).then(async () => {
let error = this.parent?.error;

if (!error && !this.options.signal?.aborted) {
try {
await this.test.beforeAll?.();
}
catch (e) {
e.source = "beforeAll";
error = e;
}
if (!error && !this.options.signal?.aborted) {
try {
await this.test.beforeAll?.();
}

if (error) {
this.error = error;
catch (e) {
e.source = "beforeAll";
error = e;
}
}

if (this.test.isTest) {
if (this.test.skip && this.test.skip !== "fail") {
this.skip();
}
else if (error) {
this.details = [`${error.source}: ${error.message}`];
this.skip();
}
else {
this.run();
}
}
if (error) {
this.error = error;
}

return Promise.allSettled((this.tests ?? []).map(test => test.runAll()));
})
.then(() => this.finished)
.finally(async () => {
if (!this.parent?.error) {
try {
await this.test.afterAll?.();
}
catch {}
if (this.test.isTest) {
if (this.test.skip && this.test.skip !== "fail") {
this.skip();
}
else if (error) {
this.details = [`${error.source}: ${error.message}`];
this.skip();
}
});
else {
this.run();
}
}

return Promise.allSettled((this.tests ?? []).map(test => test.runAll()));
});

return this;
}
Expand Down
24 changes: 12 additions & 12 deletions src/env/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,29 +370,29 @@ export default {
},
start (target, options, event, root) {
// `start` bubbles — skip descendants so we only fire once, on the root's own start.
if (options.signal?.aborted || !isInteractive || target !== root) {
return;
}

currentRoot = root;
// Open the interactive tree now so progress shows immediately — otherwise nothing renders until the first `done` event.
interactiveTree(root, options, { rerun: () => rerun(options) });
},
done (result, options, event, root) {
if (options.signal?.aborted) {
if (options.signal?.aborted || target !== root) {
return;
}

if (!isInteractive) {
if (root.stats.pending === 0) {
root.finished.then(() => {
let messages = root.toString(options);
let tree = getTree(messages).toString();
tree = process.stdout.isTTY ? format(tree) : stripFormatting(tree);

console[root.stats.fail > 0 ? "error" : "log"](tree);

process.exit(root.stats.fail > 0 ? 1 : 0);
}
});
return;
}

currentRoot = root;
// Open the interactive tree now so progress shows immediately — otherwise nothing renders until the first `done` event.
interactiveTree(root, options, { rerun: () => rerun(options) });
},
done (result, options, event, root) {
if (options.signal?.aborted || !isInteractive) {
return;
}

Expand Down
62 changes: 62 additions & 0 deletions tests/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,68 @@ export default {
},
],
},
{
name: "afterAll lifecycle",
tests: [
{
name: "Runs at every level, bottom-up, before result.finished resolves",
async run () {
let ran = [];
let test = new Test({
afterAll: () => ran.push("root"),
tests: [
{
afterAll: () => ran.push("level1"),
tests: [
{
async afterAll () {
await new Promise(r => setTimeout(r, 50));
ran.push("level2");
},
tests: [
{ run: () => "foo", expect: "foo" },
{ run: () => "bar", expect: "bar" },
],
},
],
},
],
});
let result = new TestResult(test, null);
result.runAll();
await result.finished;
return ran;
},
expect: ["level2", "level1", "root"],
},
{
name: "Completes before process.exit in --ci mode (issue #168)",
skip: typeof globalThis.process === "undefined",
async run () {
let { spawnSync } = await import("node:child_process");
let { writeFileSync, rmSync } = await import("node:fs");
let { tmpdir } = await import("node:os");
let { join } = await import("node:path");

let fixture = join(tmpdir(), "htest-168.js");
writeFileSync(
fixture,
`export default { async afterAll () { await new Promise(r => setTimeout(r, 50)); process.stderr.write("[test] afterAll ran"); }, tests: [{ run: () => 1, expect: 1 }] };`,
);

let { stderr } = spawnSync(
process.execPath,
[join(process.cwd(), "bin/htest.js"), fixture, "--ci"],
{ encoding: "utf8" },
);
rmSync(fixture);

return stderr.includes("[test] afterAll ran");
},
expect: true,
},
],
},
{
name: "Aborting",
tests: [
Expand Down