Skip to content

Commit b866755

Browse files
pmarchinitargos
authored andcommittedMar 11, 2025
test: test runner run plan
PR-URL: #57304 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent 77668ff commit b866755

12 files changed

+295
-0
lines changed
 
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import test from 'node:test';
2+
3+
test('less assertions than planned', (t) => {
4+
t.plan(2);
5+
t.assert.ok(true, 'only one assertion');
6+
// Missing second assertion
7+
});
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import test from 'node:test';
2+
3+
test('matching assertions', (t) => {
4+
t.plan(2);
5+
t.assert.ok(true, 'first assertion');
6+
t.assert.ok(true, 'second assertion');
7+
});
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import test from 'node:test';
2+
3+
test('more assertions than planned', (t) => {
4+
t.plan(1);
5+
t.assert.ok(true, 'first assertion');
6+
t.assert.ok(true, 'extra assertion'); // This should cause failure
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import test from 'node:test';
2+
3+
test('deeply nested tests', async (t) => {
4+
t.plan(1);
5+
6+
await t.test('level 1', async (t) => {
7+
t.plan(1);
8+
9+
await t.test('level 2', (t) => {
10+
t.plan(1);
11+
t.assert.ok(true, 'deepest assertion');
12+
});
13+
});
14+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import test from 'node:test';
2+
3+
test('failing planning by options', { plan: 1 }, () => {
4+
// Should fail - no assertions
5+
});
6+
7+
test('passing planning by options', { plan: 1 }, (t) => {
8+
t.assert.ok(true);
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import test from 'node:test';
2+
import { Readable } from 'node:stream';
3+
4+
test('planning with streams', (t, done) => {
5+
function* generate() {
6+
yield 'a';
7+
yield 'b';
8+
yield 'c';
9+
}
10+
const expected = ['a', 'b', 'c'];
11+
t.plan(expected.length);
12+
const stream = Readable.from(generate());
13+
stream.on('data', (chunk) => {
14+
t.assert.strictEqual(chunk, expected.shift());
15+
});
16+
17+
stream.on('end', () => {
18+
done();
19+
});
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import test from 'node:test';
2+
3+
test('parent test', async (t) => {
4+
t.plan(1);
5+
await t.test('child test', (t) => {
6+
t.plan(1);
7+
t.assert.ok(true, 'child assertion');
8+
});
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import test from 'node:test';
2+
3+
test('planning with wait should PASS within timeout', async (t) => {
4+
t.plan(1, { wait: 5000 });
5+
setTimeout(() => {
6+
t.assert.ok(true);
7+
}, 250);
8+
});
9+
10+
test('planning with wait should FAIL within timeout', async (t) => {
11+
t.plan(1, { wait: 5000 });
12+
setTimeout(() => {
13+
t.assert.ok(false);
14+
}, 250);
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import test from 'node:test';
2+
3+
test('planning should FAIL when wait time expires before plan is met', (t) => {
4+
t.plan(2, { wait: 500 });
5+
setTimeout(() => {
6+
t.assert.ok(true);
7+
}, 30_000).unref();
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import test from 'node:test';
2+
3+
test('should not wait for assertions and fail immediately', async (t) => {
4+
t.plan(1, { wait: false });
5+
6+
// Set up an async operation that won't complete before the test finishes
7+
// Since wait:false, the test should fail immediately without waiting
8+
setTimeout(() => {
9+
t.assert.ok(true);
10+
}, 1000).unref();
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import test from 'node:test';
2+
3+
test('should pass when assertions are eventually met', async (t) => {
4+
t.plan(1, { wait: true });
5+
6+
setTimeout(() => {
7+
t.assert.ok(true);
8+
}, 250);
9+
});
10+
11+
test('should fail when assertions fail', async (t) => {
12+
t.plan(1, { wait: true });
13+
14+
setTimeout(() => {
15+
t.assert.ok(false);
16+
}, 250).unref();
17+
});

‎test/parallel/test-runner-plan.mjs

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as common from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { describe, it, run } from 'node:test';
4+
import { join } from 'node:path';
5+
6+
const testFixtures = fixtures.path('test-runner', 'plan');
7+
8+
describe('input validation', () => {
9+
it('throws if options is not an object', (t) => {
10+
t.assert.throws(() => {
11+
t.plan(1, null);
12+
}, {
13+
code: 'ERR_INVALID_ARG_TYPE',
14+
message: /The "options" argument must be of type object/,
15+
});
16+
});
17+
18+
it('throws if options.wait is not a number or a boolean', (t) => {
19+
t.assert.throws(() => {
20+
t.plan(1, { wait: 'foo' });
21+
}, {
22+
code: 'ERR_INVALID_ARG_TYPE',
23+
message: /The "options\.wait" property must be one of type boolean or number\. Received type string/,
24+
});
25+
});
26+
27+
it('throws if count is not a number', (t) => {
28+
t.assert.throws(() => {
29+
t.plan('foo');
30+
}, {
31+
code: 'ERR_INVALID_ARG_TYPE',
32+
message: /The "count" argument must be of type number/,
33+
});
34+
});
35+
});
36+
37+
describe('test planning', () => {
38+
it('should pass when assertions match plan', async () => {
39+
const stream = run({
40+
files: [join(testFixtures, 'match.mjs')]
41+
});
42+
43+
stream.on('test:fail', common.mustNotCall());
44+
stream.on('test:pass', common.mustCall(1));
45+
46+
// eslint-disable-next-line no-unused-vars
47+
for await (const _ of stream);
48+
});
49+
50+
it('should fail when less assertions than planned', async () => {
51+
const stream = run({
52+
files: [join(testFixtures, 'less.mjs')]
53+
});
54+
55+
stream.on('test:fail', common.mustCall(1));
56+
stream.on('test:pass', common.mustNotCall());
57+
58+
// eslint-disable-next-line no-unused-vars
59+
for await (const _ of stream);
60+
});
61+
62+
it('should fail when more assertions than planned', async () => {
63+
const stream = run({
64+
files: [join(testFixtures, 'more.mjs')]
65+
});
66+
67+
stream.on('test:fail', common.mustCall(1));
68+
stream.on('test:pass', common.mustNotCall());
69+
70+
// eslint-disable-next-line no-unused-vars
71+
for await (const _ of stream);
72+
});
73+
74+
it('should handle plan with subtests correctly', async () => {
75+
const stream = run({
76+
files: [join(testFixtures, 'subtest.mjs')]
77+
});
78+
79+
stream.on('test:fail', common.mustNotCall());
80+
stream.on('test:pass', common.mustCall(2)); // Parent + child test
81+
82+
// eslint-disable-next-line no-unused-vars
83+
for await (const _ of stream);
84+
});
85+
86+
it('should handle plan via options', async () => {
87+
const stream = run({
88+
files: [join(testFixtures, 'plan-via-options.mjs')]
89+
});
90+
91+
stream.on('test:fail', common.mustCall(1));
92+
stream.on('test:pass', common.mustCall(1));
93+
94+
// eslint-disable-next-line no-unused-vars
95+
for await (const _ of stream);
96+
});
97+
98+
it('should handle streaming with plan', async () => {
99+
const stream = run({
100+
files: [join(testFixtures, 'streaming.mjs')]
101+
});
102+
103+
stream.on('test:fail', common.mustNotCall());
104+
stream.on('test:pass', common.mustCall(1));
105+
106+
// eslint-disable-next-line no-unused-vars
107+
for await (const _ of stream);
108+
});
109+
110+
it('should handle nested subtests with plan', async () => {
111+
const stream = run({
112+
files: [join(testFixtures, 'nested-subtests.mjs')]
113+
});
114+
115+
stream.on('test:fail', common.mustNotCall());
116+
stream.on('test:pass', common.mustCall(3)); // Parent + 2 levels of nesting
117+
118+
// eslint-disable-next-line no-unused-vars
119+
for await (const _ of stream);
120+
});
121+
122+
describe('with timeout', () => {
123+
it('should handle basic timeout scenarios', async () => {
124+
const stream = run({
125+
files: [join(testFixtures, 'timeout-basic.mjs')]
126+
});
127+
128+
stream.on('test:fail', common.mustCall(1));
129+
stream.on('test:pass', common.mustCall(1));
130+
131+
// eslint-disable-next-line no-unused-vars
132+
for await (const _ of stream);
133+
});
134+
135+
it('should fail when timeout expires before plan is met', async (t) => {
136+
const stream = run({
137+
files: [join(testFixtures, 'timeout-expired.mjs')]
138+
});
139+
140+
stream.on('test:fail', common.mustCall(1));
141+
stream.on('test:pass', common.mustNotCall());
142+
143+
// eslint-disable-next-line no-unused-vars
144+
for await (const _ of stream);
145+
});
146+
147+
it('should handle wait:true option specifically', async () => {
148+
const stream = run({
149+
files: [join(testFixtures, 'timeout-wait-true.mjs')]
150+
});
151+
152+
stream.on('test:fail', common.mustCall(1));
153+
stream.on('test:pass', common.mustCall(1));
154+
155+
// eslint-disable-next-line no-unused-vars
156+
for await (const _ of stream);
157+
});
158+
159+
it('should handle wait:false option (should not wait)', async () => {
160+
const stream = run({
161+
files: [join(testFixtures, 'timeout-wait-false.mjs')]
162+
});
163+
164+
stream.on('test:fail', common.mustCall(1)); // Fails because plan is not met immediately
165+
stream.on('test:pass', common.mustNotCall());
166+
167+
// eslint-disable-next-line no-unused-vars
168+
for await (const _ of stream);
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)
Please sign in to comment.