Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lib/loader-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,24 @@ function getStubsInfo (state, moduleUrl) {
* @returns {string}
*/
function transformModuleSource ([moduleKey, mockingInfo]) {
const mockingInfoSource = `globalThis[Symbol.for('__quibbleUserState')].quibbledModules.get(${JSON.stringify(
moduleKey
)})`

// Export names don't have to be identifiers (Node 24+ gives CommonJS modules
// a 'module.exports' one), so bind each stub to a local and export that
// under its name as a string
return `
${mockingInfo.namedExports
.map(
(name) =>
`export let ${name} = globalThis[Symbol.for('__quibbleUserState')].quibbledModules.get(${JSON.stringify(
moduleKey
)}).namedExportStubs["${name}"]`
(name, i) =>
`let __quibbleExport${i} = ${mockingInfoSource}.namedExportStubs[${JSON.stringify(name)}];
export { __quibbleExport${i} as ${JSON.stringify(name)} };`
)
.join(';\n')};
.join('\n')}
${
mockingInfo.hasDefaultExport
? `export default globalThis[Symbol.for('__quibbleUserState')].quibbledModules.get(${JSON.stringify(
moduleKey
)}).defaultExportStub;`
? `export default ${mockingInfoSource}.defaultExportStub;`
: ''
}
`
Expand Down
26 changes: 26 additions & 0 deletions test/esm-lib/quibble-esm.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ export default {
const result = await import('../esm-fixtures/a-module.mjs')
assert.equal(result.default, 'default-export-replacement')
},
'mock named exports whose names are not identifiers': async function () {
await quibble.esm('../esm-fixtures/a-module.mjs', {
'module.exports': 'module-exports-replacement',
'kebab-case': 'kebab-case-replacement',
'has "quotes" and \\backslashes\\': 'quotes-replacement'
}, 'default-export-replacement')

const result = await import('../esm-fixtures/a-module.mjs')
assert.equal(result['module.exports'], 'module-exports-replacement')
assert.equal(result['kebab-case'], 'kebab-case-replacement')
assert.equal(result['has "quotes" and \\backslashes\\'], 'quotes-replacement')
assert.equal(result.default, 'default-export-replacement')
},
'mock a CommonJS module with the named exports Node gives it': async function () {
// Node 24+ gives CommonJS modules a 'module.exports' named export, which
// testdouble.js's td.replaceEsm() passes along when imitating a module
const { default: _, ...namedExports } = await import('lodash/isPlainObject.js')
const namedExportStubs = Object.fromEntries(Object.keys(namedExports).map(name => [name, 'replacement']))
await quibble.esm('lodash/isPlainObject.js', namedExportStubs, () => 'default-export-replacement')

const result = await import('lodash/isPlainObject.js')
assert.equal(result.default(), 'default-export-replacement')
for (const name of Object.keys(namedExports)) {
assert.equal(result[name], 'replacement')
}
},
'mock a module after it is used unmocked': async function () {
const result1 = await import('../esm-fixtures/a-module.mjs')
assert.deepEqual({ ...result1 }, {
Expand Down
Loading