Skip to content

Commit 346ec8f

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 62a3cea commit 346ec8f

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`
@@ -1696,9 +1706,12 @@ changes:
16961706
thread. If `false`, only one test runs at a time.
16971707
If unspecified, subtests inherit this value from their parent.
16981708
**Default:** `false`.
1699-
* `expectFailure` {boolean|string} If truthy, the test is expected to fail.
1700-
If a string is provided, that string is displayed in the test results as the
1701-
reason why the test is expected to fail. **Default:** `false`.
1709+
* `expectFailure` {boolean|string|Object} If truthy, the test is expected to
1710+
fail. If a string is provided, that string is displayed in the test results
1711+
as the reason why the test is expected to fail. If an object is provided,
1712+
it can contain a `label` property (string) for the failure reason and a
1713+
`match` property (RegExp, Function, Object, or Error) to validate the error
1714+
thrown. **Default:** `false`.
17021715
* `only` {boolean} If truthy, and the test context is configured to run
17031716
`only` tests, then this test will be run. Otherwise, the test is skipped.
17041717
**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,
@@ -488,6 +490,39 @@ class SuiteContext {
488490
}
489491
}
490492

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

958987
if (this.expectFailure) {
959988
if (typeof this.expectFailure === 'object' &&
960-
this.expectFailure.with !== undefined) {
961-
const { with: validation } = this.expectFailure;
989+
this.expectFailure.match !== undefined) {
990+
const { match: validation } = this.expectFailure;
962991
try {
963992
const { throws } = assert;
964-
throws(() => { throw err; }, validation);
993+
const errorToCheck = (err?.code === 'ERR_TEST_FAILURE' &&
994+
err?.failureType === kTestCodeFailure &&
995+
err.cause) ?
996+
err.cause : err;
997+
throws(() => { throw errorToCheck; }, validation);
965998
} catch (e) {
966999
this.passed = false;
9671000
this.error = new ERR_TEST_FAILURE(
@@ -1397,7 +1430,7 @@ class Test extends AsyncResource {
13971430
directive = this.reporter.getTodo(this.message);
13981431
} else if (this.expectFailure) {
13991432
const message = typeof this.expectFailure === 'object' ?
1400-
this.expectFailure.message :
1433+
this.expectFailure.label :
14011434
this.expectFailure;
14021435
directive = this.reporter.getXFail(message);
14031436
}

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)