Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

export partial and strict #140

Merged
merged 10 commits into from
Sep 5, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# changelog

* 2.0.0 _Sep.06.2022_
* [export both 'partial' and 'strict'](https://github.com/iambumblehead/esmock/pull/140) variants of esmock
* 1.9.8 _Aug.28.2022_
* [use latest node v18](https://github.com/iambumblehead/esmock/pull/130) for ci-tests, a bug in the ava package prevented this
* [use latest resolvewithplus](https://github.com/iambumblehead/esmock/pull/130) and remove many lines of code needed for the older variant
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ test('should suppport partial mocks', async () => {
message: 'path.basename is not a function'
})

// use esmock.px to create a "partial mock"
const pathWrapPartial = await esmock.px('../src/pathWrap.js', {
// use esmock.partial to create a "partial mock"
const pathWrapPartial = await esmock.partial('../src/pathWrap.js', {
path: { dirname: () => '/home/' }
})

Expand Down
39 changes: 34 additions & 5 deletions src/esmock.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ declare namespace esmock {
/**
* Mocks imports for the module specified by {@link modulePath}.
*
* The provided mocks replace the imported modules _partially_, allowing some exports to
* be overridden while the rest are provided by the real module.
* This "partial" variant gives mock definitions that are merged with the
* original module definitions.
*
* @param modulePath The module whose imports will be mocked.
* @param parent A URL to resolve specifiers relative to; typically `import.meta.url`.
Expand All @@ -44,9 +44,37 @@ declare namespace esmock {
* @param opt
* @returns The result of importing {@link modulePath}, similar to `import(modulePath)`.
*/
function px(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function px(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function partial(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function partial(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
export namespace partial {
function p(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function p(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
}

/**
* Mocks imports for the module specified by {@link modulePath}.
*
* This "strict" variant gives un-modified mock definitions that are not
* merged with original module definitions.
*
* @param modulePath The module whose imports will be mocked.
* @param parent A URL to resolve specifiers relative to; typically `import.meta.url`.
* If not specified, it will be inferred via the stack, which may not work
* if source maps are in use.
* @param mockDefs A mapping of import specifiers to mocked module objects; these mocks will
* only be used for imports resolved in the module specified by {@link modulePath}.
* @param globalDefs A mapping of import specifiers to mocked module objects; these mocks will
* apply to imports within the module specified by {@link modulePath}, as well
* as any transitively imported modules.
* @param opt
* @returns The result of importing {@link modulePath}, similar to `import(modulePath)`.
*/
function strict(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function strict(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
export namespace strict {
function p(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function p(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
}
/**
* Mocks dynamic imports for the module specified by {@link modulePath}.
*
Expand All @@ -65,7 +93,7 @@ declare namespace esmock {
* @returns The result of importing {@link modulePath}, similar to `import(modulePath)`.
*/
function p(modulePath: string, parent: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function p(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;
function p(modulePath: string, mockDefs?: Record<string, any>, globalDefs?: Record<string, any>, opt?: esmock.Options): any;

/**
* Unregisters a dynamic mock created by {@link esmock.p}.
Expand All @@ -76,3 +104,4 @@ declare namespace esmock {
}

export default esmock;
export { esmock as partial, esmock as strict };
iambumblehead marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 13 additions & 21 deletions src/esmock.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import esmockIsLoader from './esmockIsLoader.js'
import esmockArgs from './esmockArgs.js'

import {
esmockModuleMock,
Expand All @@ -10,23 +11,8 @@ import {
esmockCache
} from './esmockCache.js'

// this function normalizes different "overloaded" args signatures, returning
// one predictable args list. ex,
// [modulepath, mockdefs, globaldefs, opts]
// -> [modulepath, mockdefs, globaldefs, opts]
// [modulepath, parent, mockdefs, globaldefs, opts]
// -> [modulepath, mockdefs, globaldefs, { ...opts, parent }]
const argsnormal = (args, argsextra, parent) => {
parent = typeof args[1] === 'string' && args[1]
args = parent ? [args[0], ...args.slice(2)] : args
args[3] = { parent, ...args[3], ...(argsextra && argsextra[0]) }
args[4] = (argsextra && argsextra[1]) || args[4]

return args
}

const esmock = async (...args) => {
const [modulePath, mockDefs, globalDefs, opt = {}, err] = argsnormal(args)
const [modulePath, mockDefs, globalDefs, opt = {}, err] = esmockArgs(args)
const calleePath = (opt.parent || (err || new Error).stack.split('\n')[2])
.replace(/^.*file:\/\//, '') // rm every before filepath
.replace(/:[\d]*:[\d]*.*$/, '') // rm line and row number
Expand All @@ -47,11 +33,17 @@ const esmock = async (...args) => {
return esmockModuleImportedSanitize(importedModule, modulePathKey)
}

esmock.px = async (...args) => (
esmock(...argsnormal(args, [{ partial: true }, new Error])))
const strict = async (...args) => esmock(
...esmockArgs(args, { partial: false }, new Error))
strict.p = async (...args) => esmock(
...esmockArgs(args, { partial: false, purge: false }, new Error))

const partial = async (...args) => esmock(
...esmockArgs(args, { partial: true }, new Error))
partial.p = async (...args) => esmock(
...esmockArgs(args, { partial: true, purge: false }, new Error))

esmock.p = async (...args) => (
esmock(...argsnormal(args, [{ purge: false }, new Error])))
Object.assign(esmock, strict, { strict, partial })

esmock.purge = mockModule => {
if (mockModule && /object|function/.test(typeof mockModule)
Expand All @@ -61,4 +53,4 @@ esmock.purge = mockModule => {

esmock.esmockCache = esmockCache

export default esmock
export {esmock as default, partial, strict}
14 changes: 14 additions & 0 deletions src/esmockArgs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// this function normalizes "overloaded" args signatures, returning
// one predictable args list. ex,
// esmockArgs([modulepath, mockdefs, globaldefs, opts])
// -> [modulepath, mockdefs, globaldefs, opts]
// esmockArgs([modulepath, parent, mockdefs, globaldefs, opts])
// -> [modulepath, mockdefs, globaldefs, { ...opts, parent }]
export default (args, optsextra, err, parent) => {
parent = typeof args[1] === 'string' && args[1]
args = parent ? [args[0], ...args.slice(2)] : args
args[3] = { parent, ...args[3], ...optsextra }
args[4] = err || args[4]

return args
}
5 changes: 2 additions & 3 deletions src/esmockLoader.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import process from 'process'
import esmock from './esmock.js'
export * from './esmock.js'
export {default} from './esmock.js'
import urlDummy from './esmockDummy.js'

export default esmock

const [major, minor] = process.versions.node.split('.').map(it => +it)
const isLT1612 = major < 16 || (major === 16 && minor < 12)

Expand Down
5 changes: 2 additions & 3 deletions tests/package.json.esmock.export.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import esmock from "../src/esmock.js";

export default esmock;
export * from '../src/esmock.js'
export {default} from '../src/esmock.js'
3 changes: 2 additions & 1 deletion tests/package.json.esmock.export.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {default, load, resolve, getSource} from '../src/esmockLoader.js'
export * from '../src/esmockLoader.js'
export {default} from '../src/esmockLoader.js'

// this file is used in tandem with two other things,
//
Expand Down
35 changes: 18 additions & 17 deletions tests/tests-ava/spec/esmock.ava.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sinon from 'sinon'
test('should not error when handling non-extensible object', async t => {
// if esmock tries to simulate babel and define default.default
// runtime error may occur if non-extensible is defined there
await esmock.px('../../local/importsNonDefaultClass.js', {
await esmock.partial('../../local/importsNonDefaultClass.js', {
'../../local/exportsNonDefaultClass.js': {
getNotifier: {
default: class getNotifier {
Expand All @@ -18,14 +18,15 @@ test('should not error when handling non-extensible object', async t => {
// this error can also occur when an esmocked module is used to
// mock antother module, where esmock defined default.default on the first
// module and tried to define again from the outer module
const mockedIndex = await esmock.px('../../local/importsNonDefaultClass.js', {
'../../local/exportsNonDefaultClass.js': await esmock.px(
'../../local/exportsNonDefaultClass.js', {
'../../local/pathWrap.js': {
basename: () => 'mocked basename'
}
})
})
const mockedIndex = await esmock.partial(
'../../local/importsNonDefaultClass.js', {
'../../local/exportsNonDefaultClass.js': await esmock.partial(
'../../local/exportsNonDefaultClass.js', {
'../../local/pathWrap.js': {
basename: () => 'mocked basename'
}
})
})

t.is(await mockedIndex.callNotifier(), 'mocked basename')
})
Expand All @@ -42,7 +43,7 @@ test('should return un-mocked file', async t => {
})

test('should mock a local file', async t => {
const main = await esmock.px('../../local/main.js', {
const main = await esmock.partial('../../local/main.js', {
'../../local/mainUtil.js': {
createString: () => 'test string'
}
Expand Down Expand Up @@ -165,7 +166,7 @@ test('should return un-mocked file (again)', async t => {
})

test('should mock local file', async t => {
const mainUtil = await esmock.px('../../local/mainUtil.js', {
const mainUtil = await esmock.partial('../../local/mainUtil.js', {
'../../local/mainUtilNamedExports.js': {
mainUtilNamedExportOne: () => 'foobar'
}
Expand All @@ -181,7 +182,7 @@ test('should mock local file', async t => {
})

test('should mock module and local file at the same time', async t => {
const mainUtil = await esmock.px('../../local/mainUtil.js', {
const mainUtil = await esmock.partial('../../local/mainUtil.js', {
'form-urlencoded': o => JSON.stringify(o),
'../../local/mainUtilNamedExports.js': {
mainUtilNamedExportOne: () => 'foobar'
Expand All @@ -196,7 +197,7 @@ test('should mock module and local file at the same time', async t => {
})

test('__esModule definition, inconsequential', async t => {
const mainUtil = await esmock.px('../../local/mainUtil.js', {
const mainUtil = await esmock.partial('../../local/mainUtil.js', {
'babelGeneratedDoubleDefault': o => o,
'../../local/mainUtilNamedExports.js': {
mainUtilNamedExportOne: () => 'foobar',
Expand All @@ -208,7 +209,7 @@ test('__esModule definition, inconsequential', async t => {
})

test('should work well with sinon', async t => {
const mainUtil = await esmock.px('../../local/mainUtil.js', {
const mainUtil = await esmock.partial('../../local/mainUtil.js', {
'../../local/mainUtilNamedExports.js': {
mainUtilNamedExportOne: sinon.stub().returns('foobar')
}
Expand Down Expand Up @@ -261,7 +262,7 @@ test('should mock core module', async t => {
})

test('should apply third parameter "global" definitions', async t => {
const main = await esmock.px('../../local/main.js', {
const main = await esmock.partial('../../local/main.js', {
'../../local/mainUtil.js': {
exportedFunction: () => 'foobar'
}
Expand Down Expand Up @@ -333,7 +334,7 @@ test('should have small querystring in stacktrace filename', async t => {
test('should have small querystring in stacktrace filename, deep', async t => {
const {
causeRuntimeErrorFromImportedFile
} = await esmock.px('../../local/main.js', {}, {
} = await esmock.partial('../../local/main.js', {}, {
'../../local/mainUtil.js': {
causeRuntimeError: () => {
t.nonexistantmethod()
Expand All @@ -354,7 +355,7 @@ test('should have small querystring in stacktrace filename, deep', async t => {

test('should have small querystring in stacktrace filename, deep2', async t => {
const causeDeepErrorParent =
await esmock.px('../../local/causeDeepErrorParent.js', {}, {
await esmock.partial('../../local/causeDeepErrorParent.js', {}, {
'../../local/causeDeepErrorGrandChild.js': {
what: 'now'
}
Expand Down
2 changes: 1 addition & 1 deletion tests/tests-no-loader/esmock.loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import esmock from 'esmock'

test('should throw error if !esmockloader', async () => {
const main = await esmock.px('../local/main.js', {
const main = await esmock.partial('../local/main.js', {
'../local/mainUtil.js': {
createString: () => 'test string'
}
Expand Down
43 changes: 43 additions & 0 deletions tests/tests-node/esmock.node.importing.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import esmock, { partial, strict } from 'esmock'

const isPassingPartial = async esmockPartial => {
const main = await esmockPartial('../local/main.js', {
'../local/mainUtil.js': {
createString: () => 'test string'
}
})

assert.strictEqual(typeof main, 'function')
assert.strictEqual(main(), 'main string, test string')
}

const isPassingStrict = async esmockStrict => {
await assert.rejects(() => esmockStrict('../local/main.js', {
'../local/mainUtil.js': {
createString: () => 'test string'
}
}), {
// eslint-disable-next-line max-len
message: "The requested module ':module' does not provide an export named ':named'"
.replace(/:module/, './mainUtil.js')
.replace(/:named/, 'causeRuntimeError')
})
}

test('should export esmock partial', async () => {
await isPassingPartial(partial)
await isPassingPartial(partial.p)
await isPassingPartial(esmock.partial)
await isPassingPartial(esmock.partial.p)
})

test('should export esmock strict', async () => {
await isPassingStrict(strict)
await isPassingStrict(strict.p)
await isPassingStrict(esmock.strict)
await isPassingStrict(esmock.strict.p)
await isPassingStrict(esmock)
await isPassingStrict(esmock.p)
})
2 changes: 1 addition & 1 deletion tests/tests-node/esmock.node.only.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test'
import assert from 'assert'
import esmock from '../../src/esmock.js'
import esmock from 'esmock'

// this error can occur when sources do not define 'esmockloader'
// on 'global' but use a process linked variable instead
Expand Down
Loading