Skip to content

Commit 061f049

Browse files
committed
test_runner: enhance expectFailure option
Update expectFailure to accept different types of values (RegExp, Function, Object) for error validation. This change introduces a more flexible API: - String: Acts as a failure label. - Matcher (RegExp, Function, Error): Validates the thrown error. - Object: Supports both 'label' and 'match' properties.
1 parent b6060db commit 061f049

File tree

3 files changed

+171
-27
lines changed

3 files changed

+171
-27
lines changed

doc/api/test.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,22 @@ it('should do the thing', { expectFailure: 'feature not implemented' }, () => {
252252

253253
it('should fail with specific error', {
254254
expectFailure: {
255-
with: /error message/,
256-
message: 'reason for failure',
255+
match: /error message/,
256+
label: 'reason for failure',
257257
},
258258
}, () => {
259259
assert.strictEqual(doTheThing(), true);
260260
});
261+
262+
it('should fail with regex', { expectFailure: /error message/ }, () => {
263+
assert.strictEqual(doTheThing(), true);
264+
});
265+
266+
it('should fail with function', {
267+
expectFailure: (err) => err.code === 'ERR_CODE',
268+
}, () => {
269+
assert.strictEqual(doTheThing(), true);
270+
});
261271
```
262272

263273
`skip` and/or `todo` are mutually exclusive to `expectFailure`, and `skip` or `todo`
@@ -1690,9 +1700,12 @@ changes:
16901700
thread. If `false`, only one test runs at a time.
16911701
If unspecified, subtests inherit this value from their parent.
16921702
**Default:** `false`.
1693-
* `expectFailure` {boolean|string} If truthy, the test is expected to fail.
1694-
If a string is provided, that string is displayed in the test results as the
1695-
reason why the test is expected to fail. **Default:** `false`.
1703+
* `expectFailure` {boolean|string|Object} If truthy, the test is expected to
1704+
fail. If a string is provided, that string is displayed in the test results
1705+
as the reason why the test is expected to fail. If an object is provided,
1706+
it can contain a `label` property (string) for the failure reason and a
1707+
`match` property (RegExp, Function, Object, or Error) to validate the error
1708+
thrown. **Default:** `false`.
16961709
* `only` {boolean} If truthy, and the test context is configured to run
16971710
`only` tests, then this test will be run. Otherwise, the test is skipped.
16981711
**Default:** `false`.

lib/internal/test_runner/test.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
MathMax,
1414
Number,
1515
NumberPrototypeToFixed,
16+
ObjectKeys,
1617
ObjectSeal,
1718
Promise,
1819
PromisePrototypeThen,
@@ -40,6 +41,7 @@ const {
4041
AbortError,
4142
codes: {
4243
ERR_INVALID_ARG_TYPE,
44+
ERR_INVALID_ARG_VALUE,
4345
ERR_TEST_FAILURE,
4446
},
4547
} = require('internal/errors');
@@ -57,7 +59,7 @@ const {
5759
setOwnProperty,
5860
} = require('internal/util');
5961
const assert = require('assert');
60-
const { isPromise } = require('internal/util/types');
62+
const { isPromise, isRegExp } = require('internal/util/types');
6163
const {
6264
validateAbortSignal,
6365
validateFunction,
@@ -487,6 +489,39 @@ class SuiteContext {
487489
}
488490
}
489491

492+
function parseExpectFailure(expectFailure) {
493+
if (expectFailure === undefined || expectFailure === false) {
494+
return false;
495+
}
496+
497+
if (typeof expectFailure === 'string') {
498+
return { __proto__: null, label: expectFailure, match: undefined };
499+
}
500+
501+
if (typeof expectFailure === 'function' || isRegExp(expectFailure)) {
502+
return { __proto__: null, label: undefined, match: expectFailure };
503+
}
504+
505+
if (typeof expectFailure !== 'object') {
506+
return { __proto__: null, label: undefined, match: undefined };
507+
}
508+
509+
const keys = ObjectKeys(expectFailure);
510+
if (keys.length === 0) {
511+
throw new ERR_INVALID_ARG_VALUE('options.expectFailure', expectFailure, 'must not be an empty object');
512+
}
513+
514+
if (keys.every((k) => k === 'match' || k === 'label')) {
515+
return {
516+
__proto__: null,
517+
label: expectFailure.label,
518+
match: expectFailure.match,
519+
};
520+
}
521+
522+
return { __proto__: null, label: undefined, match: expectFailure };
523+
}
524+
490525
class Test extends AsyncResource {
491526
reportedType = 'test';
492527
abortController;
@@ -636,13 +671,7 @@ class Test extends AsyncResource {
636671
this.plan = null;
637672
this.expectedAssertions = plan;
638673
this.cancelled = false;
639-
if (expectFailure === undefined || expectFailure === false) {
640-
this.expectFailure = false;
641-
} else if (typeof expectFailure === 'string' || typeof expectFailure === 'object') {
642-
this.expectFailure = expectFailure;
643-
} else {
644-
this.expectFailure = true;
645-
}
674+
this.expectFailure = parseExpectFailure(expectFailure);
646675
this.skipped = skip !== undefined && skip !== false;
647676
this.isTodo = (todo !== undefined && todo !== false) || this.parent?.isTodo;
648677
this.startTime = null;
@@ -956,11 +985,15 @@ class Test extends AsyncResource {
956985

957986
if (this.expectFailure) {
958987
if (typeof this.expectFailure === 'object' &&
959-
this.expectFailure.with !== undefined) {
960-
const { with: validation } = this.expectFailure;
988+
this.expectFailure.match !== undefined) {
989+
const { match: validation } = this.expectFailure;
961990
try {
962991
const { throws } = assert;
963-
throws(() => { throw err; }, validation);
992+
const errorToCheck = (err?.code === 'ERR_TEST_FAILURE' &&
993+
err?.failureType === kTestCodeFailure &&
994+
err.cause) ?
995+
err.cause : err;
996+
throws(() => { throw errorToCheck; }, validation);
964997
} catch (e) {
965998
this.passed = false;
966999
this.error = new ERR_TEST_FAILURE(
@@ -1388,7 +1421,7 @@ class Test extends AsyncResource {
13881421
directive = this.reporter.getTodo(this.message);
13891422
} else if (this.expectFailure) {
13901423
const message = typeof this.expectFailure === 'object' ?
1391-
this.expectFailure.message :
1424+
this.expectFailure.label :
13921425
this.expectFailure;
13931426
directive = this.reporter.getXFail(message);
13941427
}

test/parallel/test-runner-xfail.js

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,108 @@ if (process.env.CHILD_PROCESS === 'true') {
99
assert.fail('boom');
1010
});
1111

12-
test('fail with message object', { expectFailure: { message: 'reason object' } }, () => {
12+
test('fail with label object', { expectFailure: { label: 'reason object' } }, () => {
1313
assert.fail('boom');
1414
});
1515

16-
test('fail with validation regex', { expectFailure: { with: /boom/ } }, () => {
16+
test('fail with match regex', { expectFailure: { match: /boom/ } }, () => {
1717
assert.fail('boom');
1818
});
1919

20-
test('fail with validation object', { expectFailure: { with: { message: 'boom' } } }, () => {
20+
test('fail with match object', { expectFailure: { match: { message: 'boom' } } }, () => {
2121
assert.fail('boom');
2222
});
2323

24-
test('fail with validation class', { expectFailure: { with: assert.AssertionError } }, () => {
24+
test('fail with match class', { expectFailure: { match: assert.AssertionError } }, () => {
2525
assert.fail('boom');
2626
});
2727

28-
test('fail with validation error (wrong error)', { expectFailure: { with: /bang/ } }, () => {
28+
test('fail with match error (wrong error)', { expectFailure: { match: /bang/ } }, () => {
2929
assert.fail('boom'); // Should result in real failure because error doesn't match
3030
});
3131

3232
test('unexpected pass', { expectFailure: true }, () => {
3333
// Should result in real failure because it didn't fail
3434
});
3535

36+
test('fail with empty string', { expectFailure: '' }, () => {
37+
assert.fail('boom');
38+
});
39+
40+
// 1. Matcher: RegExp
41+
test('fails with regex matcher', { expectFailure: /expected error/ }, () => {
42+
throw new Error('this is the expected error');
43+
});
44+
45+
test('fails with regex matcher (mismatch)', { expectFailure: /expected error/ }, () => {
46+
throw new Error('wrong error'); // Should fail the test
47+
});
48+
49+
// 2. Matcher: Class
50+
test('fails with class matcher', { expectFailure: RangeError }, () => {
51+
throw new RangeError('out of bounds');
52+
});
53+
54+
test('fails with class matcher (mismatch)', { expectFailure: RangeError }, () => {
55+
throw new TypeError('wrong type'); // Should fail the test
56+
});
57+
58+
// 3. Matcher: Object (Properties)
59+
test('fails with object matcher', { expectFailure: { code: 'ERR_TEST' } }, () => {
60+
const err = new Error('boom');
61+
err.code = 'ERR_TEST';
62+
throw err;
63+
});
64+
65+
test('fails with object matcher (mismatch)', { expectFailure: { code: 'ERR_TEST' } }, () => {
66+
const err = new Error('boom');
67+
err.code = 'ERR_WRONG';
68+
throw err; // Should fail
69+
});
70+
71+
// 4. Configuration Object: Reason + Validation
72+
test('fails with config object (label + match)', {
73+
expectFailure: {
74+
label: 'Bug #124',
75+
match: /boom/
76+
}
77+
}, () => {
78+
throw new Error('boom');
79+
});
80+
81+
test('fails with config object (label only)', {
82+
expectFailure: { label: 'Bug #125' }
83+
}, () => {
84+
throw new Error('boom');
85+
});
86+
87+
test('fails with config object (match only)', {
88+
expectFailure: { match: /boom/ }
89+
}, () => {
90+
throw new Error('boom');
91+
});
92+
93+
// 5. Edge Case: Empty Object (Should throw ERR_INVALID_ARG_VALUE during creation)
94+
try {
95+
test('invalid empty object', { expectFailure: {} }, () => {});
96+
} catch (e) {
97+
console.log(`CAUGHT_INVALID_ARG: ${e.code}`);
98+
}
99+
100+
// 6. Primitives and Truthiness
101+
test('fails with boolean true', { expectFailure: true }, () => {
102+
throw new Error('any error');
103+
});
104+
105+
// 7. Unexpected Pass (Enhanced)
106+
test('unexpected pass (reason string)', { expectFailure: 'should fail' }, () => {
107+
// Pass
108+
});
109+
110+
test('unexpected pass (matcher)', { expectFailure: /boom/ }, () => {
111+
// Pass
112+
});
113+
36114
} else {
37115
const child = spawn(process.execPath, ['--test-reporter', 'tap', __filename], {
38116
env: { ...process.env, CHILD_PROCESS: 'true' },
@@ -47,10 +125,30 @@ if (process.env.CHILD_PROCESS === 'true') {
47125
// We expect exit code 1 because 'unexpected pass' and 'wrong error' should fail the test run
48126
assert.strictEqual(code, 1);
49127

50-
// Check outputs
51-
assert.match(stdout, /# EXPECTED FAILURE reason string/);
52-
assert.match(stdout, /# EXPECTED FAILURE reason object/);
53-
assert.match(stdout, /not ok \d+ - fail with validation error \(wrong error\)/);
54-
assert.match(stdout, /not ok \d+ - unexpected pass/);
128+
assert.match(stdout, /ok \d+ - fail with message string # EXPECTED FAILURE reason string/);
129+
assert.match(stdout, /ok \d+ - fail with label object # EXPECTED FAILURE reason object/);
130+
assert.match(stdout, /ok \d+ - fail with match regex # EXPECTED FAILURE/);
131+
assert.match(stdout, /ok \d+ - fail with match object # EXPECTED FAILURE/);
132+
assert.match(stdout, /ok \d+ - fail with match class # EXPECTED FAILURE/);
133+
assert.match(stdout, /not ok \d+ - fail with match error \(wrong error\) # EXPECTED FAILURE/);
134+
assert.match(stdout, /not ok \d+ - unexpected pass # EXPECTED FAILURE/);
135+
assert.match(stdout, /ok \d+ - fail with empty string # EXPECTED FAILURE/);
136+
137+
// New tests verification
138+
assert.match(stdout, /ok \d+ - fails with regex matcher # EXPECTED FAILURE/);
139+
assert.match(stdout, /not ok \d+ - fails with regex matcher \(mismatch\) # EXPECTED FAILURE/);
140+
assert.match(stdout, /ok \d+ - fails with class matcher # EXPECTED FAILURE/);
141+
assert.match(stdout, /not ok \d+ - fails with class matcher \(mismatch\) # EXPECTED FAILURE/);
142+
assert.match(stdout, /ok \d+ - fails with object matcher # EXPECTED FAILURE/);
143+
assert.match(stdout, /not ok \d+ - fails with object matcher \(mismatch\) # EXPECTED FAILURE/);
144+
assert.match(stdout, /ok \d+ - fails with config object \(label \+ match\) # EXPECTED FAILURE Bug \\#124/);
145+
assert.match(stdout, /ok \d+ - fails with config object \(label only\) # EXPECTED FAILURE Bug \\#125/);
146+
assert.match(stdout, /ok \d+ - fails with config object \(match only\) # EXPECTED FAILURE/);
147+
assert.match(stdout, /ok \d+ - fails with boolean true # EXPECTED FAILURE/);
148+
assert.match(stdout, /not ok \d+ - unexpected pass \(reason string\) # EXPECTED FAILURE should fail/);
149+
assert.match(stdout, /not ok \d+ - unexpected pass \(matcher\) # EXPECTED FAILURE/);
150+
151+
// Empty object error
152+
assert.match(stdout, /CAUGHT_INVALID_ARG: ERR_INVALID_ARG_VALUE/);
55153
}));
56154
}

0 commit comments

Comments
 (0)