Skip to content

Commit 04cfa17

Browse files
committed
test_runner: reject mixed module mock options
Throw when options.exports is combined with the legacy defaultExport or namedExports options. This keeps the behavior explicit during the transition and avoids merge or precedence rules for mixed usage.
1 parent c8a3323 commit 04cfa17

File tree

3 files changed

+53
-19
lines changed

3 files changed

+53
-19
lines changed

doc/api/test.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,6 +2486,7 @@ changes:
24862486
* `exports` {Object} Optional mocked exports. The `default` property, if
24872487
provided, is used as the mocked module's default export. All other own
24882488
enumerable properties are used as named exports.
2489+
This option cannot be used with `defaultExport` or `namedExports`.
24892490
* If the mock is a CommonJS or builtin module, `exports.default` is used as
24902491
the value of `module.exports`.
24912492
* If `exports.default` is not provided for a CommonJS or builtin mock,
@@ -2497,13 +2498,15 @@ changes:
24972498
export. If the mock is a CommonJS or builtin module, this setting is used as
24982499
the value of `module.exports`. If this value is not provided, CJS and builtin
24992500
mocks use an empty object as the value of `module.exports`.
2501+
This option cannot be used with `options.exports`.
25002502
This option is deprecated and will be removed in a later version.
25012503
Prefer `options.exports.default`.
25022504
* `namedExports` {Object} An optional object whose keys and values are used to
25032505
create the named exports of the mock module. If the mock is a CommonJS or
25042506
builtin module, these values are copied onto `module.exports`. Therefore, if a
25052507
mock is created with both named exports and a non-object default export, the
25062508
mock will throw an exception when used as a CJS or builtin module.
2509+
This option cannot be used with `options.exports`.
25072510
This option is deprecated and will be removed in a later version.
25082511
Prefer `options.exports`.
25092512
* Returns: {MockModuleContext} An object that can be used to manipulate the mock.

lib/internal/test_runner/mock/mock.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -819,22 +819,44 @@ function normalizeModuleMockOptions(options) {
819819
const { cache = false } = options;
820820
validateBoolean(cache, 'options.cache');
821821

822+
const hasExports = 'exports' in options;
823+
const hasNamedExports = 'namedExports' in options;
824+
const hasDefaultExport = 'defaultExport' in options;
825+
822826
deprecateNamedExports(options);
823827
deprecateDefaultExport(options);
824828

825829
const moduleExports = { __proto__: null };
826830

827-
if ('exports' in options) {
831+
if (hasExports) {
828832
validateObject(options.exports, 'options.exports');
829-
copyOwnProperties(options.exports, moduleExports);
830833
}
831834

832-
if ('namedExports' in options) {
835+
if (hasNamedExports) {
833836
validateObject(options.namedExports, 'options.namedExports');
837+
}
838+
839+
if (hasExports && (hasNamedExports || hasDefaultExport)) {
840+
let reason = "cannot be used with 'options.namedExports'";
841+
842+
if (hasDefaultExport) {
843+
reason = hasNamedExports ?
844+
"cannot be used with 'options.namedExports' or 'options.defaultExport'" :
845+
"cannot be used with 'options.defaultExport'";
846+
}
847+
848+
throw new ERR_INVALID_ARG_VALUE('options.exports', options.exports, reason);
849+
}
850+
851+
if (hasExports) {
852+
copyOwnProperties(options.exports, moduleExports);
853+
}
854+
855+
if (hasNamedExports) {
834856
copyOwnProperties(options.namedExports, moduleExports);
835857
}
836858

837-
if ('defaultExport' in options) {
859+
if (hasDefaultExport) {
838860
ObjectDefineProperty(
839861
moduleExports,
840862
'default',

test/parallel/test-runner-module-mocking.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,32 @@ test('input validation', async (t) => {
4848
}, { code: 'ERR_INVALID_ARG_TYPE' });
4949
});
5050

51-
await t.test('allows exports to be used with legacy options', async (t) => {
52-
const fixturePath = fixtures.path('module-mocking', 'basic-cjs.js');
53-
const fixture = pathToFileURL(fixturePath);
54-
55-
t.mock.module(fixture, {
56-
exports: { value: 'from exports' },
57-
namedExports: { value: 'from namedExports' },
58-
defaultExport: { from: 'defaultExport' },
59-
});
51+
await t.test('throws if exports is used with namedExports', async (t) => {
52+
assert.throws(() => {
53+
t.mock.module(__filename, {
54+
exports: {},
55+
namedExports: {},
56+
});
57+
}, { code: 'ERR_INVALID_ARG_VALUE' });
58+
});
6059

61-
const cjsMock = require(fixturePath);
62-
const esmMock = await import(fixture);
60+
await t.test('throws if exports is used with defaultExport', async (t) => {
61+
assert.throws(() => {
62+
t.mock.module(__filename, {
63+
exports: {},
64+
defaultExport: {},
65+
});
66+
}, { code: 'ERR_INVALID_ARG_VALUE' });
67+
});
6368

64-
assert.strictEqual(cjsMock.value, 'from namedExports');
65-
assert.strictEqual(cjsMock.from, 'defaultExport');
66-
assert.strictEqual(esmMock.value, 'from namedExports');
67-
assert.strictEqual(esmMock.default.from, 'defaultExport');
69+
await t.test('throws if exports is used with both legacy options', async (t) => {
70+
assert.throws(() => {
71+
t.mock.module(__filename, {
72+
exports: {},
73+
namedExports: {},
74+
defaultExport: {},
75+
});
76+
}, { code: 'ERR_INVALID_ARG_VALUE' });
6877
});
6978
});
7079

0 commit comments

Comments
 (0)