Skip to content

Commit cd8b5aa

Browse files
committed
test_runner: enhance expectFailure with validation support
Update `expectFailure` to accept an object for detailed configuration. - Support `message` property for TAP output directives. - Support `with` property for error validation (RegExp or Object), similar to `assert.throws`. Tests added in `test/parallel/test-runner-xfail.js`.
1 parent 13aedaa commit cd8b5aa

File tree

4 files changed

+117
-30
lines changed

4 files changed

+117
-30
lines changed

doc/api/test.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,16 @@ it('should do the thing', { expectFailure: true }, () => {
246246
assert.strictEqual(doTheThing(), true);
247247
});
248248

249-
it('should do the thing', { expectFailure: 'doTheThing is not doing the thing because ...' }, () => {
249+
it('should do the thing', { expectFailure: 'feature not implemented' }, () => {
250+
assert.strictEqual(doTheThing(), true);
251+
});
252+
253+
it('should fail with specific error', {
254+
expectFailure: {
255+
with: /error message/,
256+
message: 'reason for failure',
257+
},
258+
}, () => {
250259
assert.strictEqual(doTheThing(), true);
251260
});
252261
```

lib/internal/test_runner/test.js

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const {
5656
once: runOnce,
5757
setOwnProperty,
5858
} = require('internal/util');
59-
const { isPromise } = require('internal/util/types');
59+
const { isDeepStrictEqual } = require('internal/util/comparisons');
60+
const { isPromise, isRegExp } = require('internal/util/types');
6061
const {
6162
validateAbortSignal,
6263
validateFunction,
@@ -637,7 +638,7 @@ class Test extends AsyncResource {
637638
this.cancelled = false;
638639
if (expectFailure === undefined || expectFailure === false) {
639640
this.expectFailure = false;
640-
} else if (typeof expectFailure === 'string') {
641+
} else if (typeof expectFailure === 'string' || typeof expectFailure === 'object') {
641642
this.expectFailure = expectFailure;
642643
} else {
643644
this.expectFailure = true;
@@ -953,7 +954,40 @@ class Test extends AsyncResource {
953954
return;
954955
}
955956

956-
this.passed = this.expectFailure;
957+
if (this.expectFailure) {
958+
if (typeof this.expectFailure === 'object' &&
959+
this.expectFailure.with !== undefined) {
960+
const { with: validation } = this.expectFailure;
961+
let match = false;
962+
963+
if (isRegExp(validation)) {
964+
match = RegExpPrototypeExec(validation, err.message) !== null;
965+
} else if (typeof validation === 'object' && validation !== null) {
966+
match = true;
967+
for (const prop in validation) {
968+
if (!isDeepStrictEqual(err[prop], validation[prop])) {
969+
match = false;
970+
break;
971+
}
972+
}
973+
} else if (validation === err) {
974+
match = true;
975+
}
976+
977+
if (!match) {
978+
this.passed = false;
979+
this.error = new ERR_TEST_FAILURE(
980+
'The test failed, but the error did not match the expected validation',
981+
kTestCodeFailure,
982+
);
983+
this.error.cause = err;
984+
return;
985+
}
986+
}
987+
this.passed = true;
988+
} else {
989+
this.passed = false;
990+
}
957991

958992
this.error = err;
959993
}
@@ -963,6 +997,20 @@ class Test extends AsyncResource {
963997
return;
964998
}
965999

1000+
if (this.skipped || this.isTodo) {
1001+
this.passed = true;
1002+
return;
1003+
}
1004+
1005+
if (this.expectFailure) {
1006+
this.passed = false;
1007+
this.error = new ERR_TEST_FAILURE(
1008+
'Test passed but was expected to fail',
1009+
kTestCodeFailure,
1010+
);
1011+
return;
1012+
}
1013+
9661014
this.passed = true;
9671015
}
9681016

@@ -1352,7 +1400,10 @@ class Test extends AsyncResource {
13521400
} else if (this.isTodo) {
13531401
directive = this.reporter.getTodo(this.message);
13541402
} else if (this.expectFailure) {
1355-
directive = this.reporter.getXFail(this.expectFailure);
1403+
const message = typeof this.expectFailure === 'object' ?
1404+
this.expectFailure.message :
1405+
this.expectFailure;
1406+
directive = this.reporter.getXFail(message);
13561407
}
13571408

13581409
if (this.reportedType) {

test/parallel/test-runner-xfail-message.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

test/parallel/test-runner-xfail.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { test } = require('node:test');
4+
const { spawn } = require('child_process');
5+
const assert = require('node:assert');
6+
7+
if (process.env.CHILD_PROCESS === 'true') {
8+
test('fail with message string', { expectFailure: 'reason string' }, () => {
9+
assert.fail('boom');
10+
});
11+
12+
test('fail with message object', { expectFailure: { message: 'reason object' } }, () => {
13+
assert.fail('boom');
14+
});
15+
16+
test('fail with validation regex', { expectFailure: { with: /boom/ } }, () => {
17+
assert.fail('boom');
18+
});
19+
20+
test('fail with validation object', { expectFailure: { with: { message: 'boom' } } }, () => {
21+
assert.fail('boom');
22+
});
23+
24+
test('fail with validation error (wrong error)', { expectFailure: { with: /bang/ } }, () => {
25+
assert.fail('boom'); // Should result in real failure because error doesn't match
26+
});
27+
28+
test('unexpected pass', { expectFailure: true }, () => {
29+
// Should result in real failure because it didn't fail
30+
});
31+
32+
} else {
33+
const child = spawn(process.execPath, ['--test-reporter', 'tap', __filename], {
34+
env: { ...process.env, CHILD_PROCESS: 'true' },
35+
stdio: 'pipe',
36+
});
37+
38+
let stdout = '';
39+
child.stdout.setEncoding('utf8');
40+
child.stdout.on('data', (chunk) => { stdout += chunk; });
41+
42+
child.on('close', common.mustCall((code) => {
43+
// We expect exit code 1 because 'unexpected pass' and 'wrong error' should fail the test run
44+
assert.strictEqual(code, 1);
45+
46+
// Check outputs
47+
assert.match(stdout, /# EXPECTED FAILURE reason string/);
48+
assert.match(stdout, /# EXPECTED FAILURE reason object/);
49+
assert.match(stdout, /not ok \d+ - fail with validation error \(wrong error\)/);
50+
assert.match(stdout, /not ok \d+ - unexpected pass/);
51+
}));
52+
}

0 commit comments

Comments
 (0)