Skip to content

Commit 0d41fde

Browse files
kt3knathanwhit
authored andcommitted
fix(ext/node): add basic support of suite/describe in node:test (#28847)
1 parent 67ca9f3 commit 0d41fde

File tree

3 files changed

+247
-13
lines changed

3 files changed

+247
-13
lines changed

ext/node/polyfills/testing.ts

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

33
import { primordials } from "ext:core/mod.js";
4-
const { PromisePrototypeThen } = primordials;
4+
const {
5+
PromisePrototypeThen,
6+
ArrayPrototypePush,
7+
SafePromiseAll,
8+
SafePromisePrototypeFinally,
9+
} = primordials;
510
import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts";
611

712
export function run() {
@@ -87,6 +92,49 @@ class NodeTestContext {
8792
}
8893
}
8994

95+
let currentSuite: TestSuite | null = null;
96+
97+
class TestSuite {
98+
#denoTestContext: Deno.TestContext;
99+
steps: Promise<boolean>[] = [];
100+
101+
constructor(t: Deno.TestContext) {
102+
this.#denoTestContext = t;
103+
}
104+
105+
addTest(name, options, fn, overrides) {
106+
const prepared = prepareOptions(name, options, fn, overrides);
107+
const step = this.#denoTestContext.step({
108+
name: prepared.name,
109+
fn: (denoTestContext) => {
110+
const newNodeTextContext = new NodeTestContext(denoTestContext);
111+
return prepared.fn(newNodeTextContext);
112+
},
113+
ignore: prepared.options.todo || prepared.options.skip,
114+
sanitizeExit: false,
115+
sanitizeOps: false,
116+
sanitizeResources: false,
117+
});
118+
ArrayPrototypePush(this.steps, step);
119+
}
120+
121+
addSuite(name, options, fn, overrides) {
122+
const prepared = prepareOptions(name, options, fn, overrides);
123+
// deno-lint-ignore prefer-primordials
124+
const { promise, resolve } = Promise.withResolvers();
125+
const step = this.#denoTestContext.step({
126+
name: prepared.name,
127+
fn: wrapSuiteFn(prepared.fn, resolve),
128+
ignore: prepared.options.todo || prepared.options.skip,
129+
sanitizeExit: false,
130+
sanitizeOps: false,
131+
sanitizeResources: false,
132+
});
133+
ArrayPrototypePush(this.steps, step);
134+
return promise;
135+
}
136+
}
137+
90138
function prepareOptions(name, options, fn, overrides) {
91139
if (typeof name === "function") {
92140
fn = name;
@@ -147,30 +195,104 @@ function prepareDenoTest(name, options, fn, overrides) {
147195
return promise;
148196
}
149197

150-
export function test(name, options, fn) {
151-
return prepareDenoTest(name, options, fn, {});
198+
function wrapSuiteFn(fn, resolve) {
199+
return function (t) {
200+
const prevSuite = currentSuite;
201+
const suite = currentSuite = new TestSuite(t);
202+
try {
203+
fn();
204+
} finally {
205+
currentSuite = prevSuite;
206+
}
207+
return SafePromisePrototypeFinally(SafePromiseAll(suite.steps), resolve);
208+
};
209+
}
210+
211+
function prepareDenoTestForSuite(name, options, fn, overrides) {
212+
const prepared = prepareOptions(name, options, fn, overrides);
213+
214+
// deno-lint-ignore prefer-primordials
215+
const { promise, resolve } = Promise.withResolvers();
216+
217+
const denoTestOptions = {
218+
name: prepared.name,
219+
fn: wrapSuiteFn(prepared.fn, resolve),
220+
only: prepared.options.only,
221+
ignore: prepared.options.todo || prepared.options.skip,
222+
sanitizeExit: false,
223+
sanitizeOps: false,
224+
sanitizeResources: false,
225+
};
226+
Deno.test(denoTestOptions);
227+
return promise;
228+
}
229+
230+
export function test(name, options, fn, overrides) {
231+
if (currentSuite) {
232+
return currentSuite.addTest(name, options, fn, overrides);
233+
}
234+
return prepareDenoTest(name, options, fn, overrides);
152235
}
153236

154237
test.skip = function skip(name, options, fn) {
155-
return prepareDenoTest(name, options, fn, { skip: true });
238+
return test(name, options, fn, { skip: true });
156239
};
157240

158241
test.todo = function todo(name, options, fn) {
159-
return prepareDenoTest(name, options, fn, { todo: true });
242+
return test(name, options, fn, { todo: true });
160243
};
161244

162245
test.only = function only(name, options, fn) {
163-
return prepareDenoTest(name, options, fn, { only: true });
246+
return test(name, options, fn, { only: true });
247+
};
248+
249+
export function describe(name, options, fn) {
250+
return suite(name, options, fn, {});
251+
}
252+
253+
describe.skip = function skip(name, options, fn) {
254+
return suite.skip(name, options, fn);
255+
};
256+
describe.todo = function todo(name, options, fn) {
257+
return suite.todo(name, options, fn);
258+
};
259+
describe.only = function only(name, options, fn) {
260+
return suite.only(name, options, fn);
164261
};
165262

166-
export function describe() {
167-
notImplemented("test.describe");
263+
export function suite(name, options, fn, overrides) {
264+
if (currentSuite) {
265+
return currentSuite.addSuite(name, options, fn, overrides);
266+
}
267+
return prepareDenoTestForSuite(name, options, fn, overrides);
168268
}
169269

170-
export function it() {
171-
notImplemented("test.it");
270+
suite.skip = function skip(name, options, fn) {
271+
return suite(name, options, fn, { skip: true });
272+
};
273+
suite.todo = function todo(name, options, fn) {
274+
return suite(name, options, fn, { todo: true });
275+
};
276+
suite.only = function only(name, options, fn) {
277+
return suite(name, options, fn, { only: true });
278+
};
279+
280+
export function it(name, options, fn) {
281+
return test(name, options, fn, {});
172282
}
173283

284+
it.skip = function skip(name, options, fn) {
285+
return test.skip(name, options, fn);
286+
};
287+
288+
it.todo = function todo(name, options, fn) {
289+
return test.todo(name, options, fn);
290+
};
291+
292+
it.only = function only(name, options, fn) {
293+
return test.only(name, options, fn);
294+
};
295+
174296
export function before() {
175297
notImplemented("test.before");
176298
}
@@ -187,6 +309,10 @@ export function afterEach() {
187309
notImplemented("test.afterEach");
188310
}
189311

312+
test.it = it;
313+
test.describe = describe;
314+
test.suite = suite;
315+
190316
export const mock = {
191317
fn: () => {
192318
notImplemented("test.mock.fn");

tests/specs/node/node_test_module/test.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// deno-lint-ignore-file
66

77
import assert from "node:assert";
8-
import test from "node:test";
8+
import test, { describe, it, suite } from "node:test";
99
import util from "node:util";
1010
import { setImmediate } from "node:timers";
1111

@@ -79,6 +79,57 @@ test("reject fail", () => {
7979
return Promise.reject(new Error("rejected from reject fail"));
8080
});
8181

82+
suite("suite", () => {
83+
test("test 1", () => {});
84+
test("test 2", () => {});
85+
86+
suite("sub suite 1", () => {
87+
test("nested test 1", () => {});
88+
test("nested test 2", () => {});
89+
});
90+
91+
suite("sub suite 2", () => {
92+
test("nested test 1", () => {});
93+
test("nested test 2", () => {});
94+
});
95+
});
96+
97+
describe("describe", () => {
98+
it("test 1", () => {});
99+
it("test 2", () => {});
100+
101+
describe("sub describe 1", () => {
102+
it("nested test 1", () => {});
103+
it("nested test 2", () => {});
104+
});
105+
106+
describe("sub describe 2", () => {
107+
it("nested test 1", () => {});
108+
it("nested test 2", () => {});
109+
});
110+
});
111+
112+
suite("suite", () => {
113+
test("test 1", () => {});
114+
test("test 2", () => {
115+
throw new Error("thrown from test 2");
116+
});
117+
118+
suite("sub suite 1", () => {
119+
test("nested test 1", () => {});
120+
test("nested test 2", () => {
121+
throw new Error("thrown from nested test 2");
122+
});
123+
});
124+
125+
suite("sub suite 2", () => {
126+
test("nested test 1", () => {
127+
throw new Error("thrown from nested test 1");
128+
});
129+
test("nested test 2", () => {});
130+
});
131+
});
132+
82133
test("unhandled rejection - passes but warns", () => {
83134
Promise.reject(new Error("rejected from unhandled rejection fail"));
84135
});

tests/specs/node/node_test_module/test.out

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[WILDCARD]
2-
running 63 tests from ./test.js
2+
running 66 tests from ./test.js
33
sync pass todo ...
44
------- output -------
55
Warning: Not implemented: test.TestContext.todo
@@ -56,6 +56,42 @@ async skip fail ... FAILED [WILDCARD]
5656
async assertion fail ... FAILED [WILDCARD]
5757
resolve pass ... ok [WILDCARD]
5858
reject fail ... FAILED [WILDCARD]
59+
suite ...
60+
test 1 ... ok ([WILDLINE])
61+
test 2 ... ok ([WILDLINE])
62+
sub suite 1 ...
63+
nested test 1 ... ok ([WILDLINE])
64+
nested test 2 ... ok ([WILDLINE])
65+
sub suite 1 ... ok ([WILDLINE])
66+
sub suite 2 ...
67+
nested test 1 ... ok ([WILDLINE])
68+
nested test 2 ... ok ([WILDLINE])
69+
sub suite 2 ... ok ([WILDLINE])
70+
suite ... ok ([WILDLINE])
71+
describe ...
72+
test 1 ... ok ([WILDLINE])
73+
test 2 ... ok ([WILDLINE])
74+
sub describe 1 ...
75+
nested test 1 ... ok ([WILDLINE])
76+
nested test 2 ... ok ([WILDLINE])
77+
sub describe 1 ... ok ([WILDLINE])
78+
sub describe 2 ...
79+
nested test 1 ... ok ([WILDLINE])
80+
nested test 2 ... ok ([WILDLINE])
81+
sub describe 2 ... ok ([WILDLINE])
82+
describe ... ok ([WILDLINE])
83+
suite ...
84+
test 1 ... ok ([WILDLINE])
85+
test 2 ... FAILED ([WILDLINE])
86+
sub suite 1 ...
87+
nested test 2 ... FAILED ([WILDLINE])
88+
nested test 1 ... ok ([WILDLINE])
89+
sub suite 1 ... FAILED (due to 1 failed step) ([WILDLINE])
90+
sub suite 2 ...
91+
nested test 1 ... FAILED ([WILDLINE])
92+
nested test 2 ... ok ([WILDLINE])
93+
sub suite 2 ... FAILED (due to 1 failed step) ([WILDLINE])
94+
suite ... FAILED (due to 3 failed steps) ([WILDLINE])
5995
unhandled rejection - passes but warns ...
6096
Uncaught error from ./test.js FAILED
6197
unhandled rejection - passes but warns ... cancelled ([WILDCARD])
@@ -151,6 +187,24 @@ error: Error: rejected from reject fail
151187
^
152188
at [WILDCARD]
153189

190+
suite ... test 2 => ./test.js:[WILDLINE]
191+
error: Error: thrown from test 2
192+
throw new Error("thrown from test 2");
193+
^
194+
at [WILDCARD]
195+
196+
suite ... sub suite 1 ... nested test 2 => ./test.js:[WILDLINE]
197+
error: Error: thrown from nested test 2
198+
throw new Error("thrown from nested test 2");
199+
^
200+
at [WILDCARD]
201+
202+
suite ... sub suite 2 ... nested test 1 => ./test.js:[WILDLINE]
203+
error: Error: thrown from nested test 1
204+
throw new Error("thrown from nested test 1");
205+
^
206+
at [WILDCARD]
207+
154208
./test.js (uncaught error)
155209
error: (in promise) Error: rejected from unhandled rejection fail
156210
Promise.reject(new Error("rejected from unhandled rejection fail"));
@@ -168,8 +222,11 @@ async throw fail => ./test.js:53:1
168222
async skip fail => ./test.js:64:1
169223
async assertion fail => ./test.js:69:1
170224
reject fail => ./test.js:78:1
225+
suite ... test 2 => ./test.js:[WILDLINE]
226+
suite ... sub suite 1 ... nested test 2 => ./test.js:[WILDLINE]
227+
suite ... sub suite 2 ... nested test 1 => ./test.js:[WILDLINE]
171228
./test.js (uncaught error)
172229

173-
FAILED | 9 passed (2 steps) | 51 failed | 4 ignored [WILDCARD]
230+
FAILED | 11 passed (21 steps) | 52 failed (5 steps) | 4 ignored [WILDCARD]
174231

175232
error: Test failed

0 commit comments

Comments
 (0)