From 60406a3eb1f85cc989d481c4d60b8ee1109797d4 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 26 Nov 2021 13:02:43 -0800 Subject: [PATCH] add test and support for await import --- README.md | 40 +++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- spec/esmock.spec.js | 33 +++++++++++++++++++++++++++ spec/local/usesInlineImport.mjs | 13 +++++++++++ src/esmock.js | 19 ++++++++++++++-- src/esmockCache.js | 6 ----- src/esmockModule.js | 8 +++---- 8 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 spec/local/usesInlineImport.mjs diff --git a/README.md b/README.md index 121258d9..aed99f6a 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,49 @@ test('should mock modules and local files at same time', async t => { }); ``` +## Examle, mocking await import( 'modulename' ) + +When `esmock` loads and returns a module it deletes mocking definitions from a cache by default. Disable this behaviour when using 'await import', so that mocked definitions can be loaded during test runtime. A function can be called later to clear the cache. + +``` javascript +export default async function usesAwaitImport (config) { + const eslint = (await import('eslint')); + + return new eslint.ESLint({ baseConfig : config }); +}; +``` + +In the test file, define an 'options' object before local and global mock definitions, `{ isPurge: false }`, +``` javascript +test('mocks inline `async import("name")`', async t => { + const writeJSConfigFile = await esmock('./local/usesAwaitImport.mjs', { + isPurge : false + }, { + eslint : { + ESLint : function (o) { + this.stringify = () => JSON.stringify(o); + + return this; + } + } + }); + + t.is((await writeJSConfigFile('config')).stringify(), JSON.stringify({ + baseConfig : 'config' + })); + + // clear the cache + esmock.purge(writeJSConfigFile); +}); +``` + +If there are not many tests or if each test completes in a separate process, skipping `esmock.purge()` is OK + ### changelog + * 1.2.0 _Nov.26.2021_ + * add support for await import * 1.1.0 _Nov.25.2021_ * add windows-latest to testing pipeline and begin windows support * removed files and functions no longer needed diff --git a/package-lock.json b/package-lock.json index 43e0b4cd..d2c5992d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "esmock", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "esmock", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "resolvewithplus": "^0.4.2" diff --git a/package.json b/package.json index 84dbd754..b6e4f6cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "esmock", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "readmeFilename": "README.md", "description": "mock esm modules for unit-tests", diff --git a/spec/esmock.spec.js b/spec/esmock.spec.js index 35ff790d..eb154418 100644 --- a/spec/esmock.spec.js +++ b/spec/esmock.spec.js @@ -254,3 +254,36 @@ test('returns spread-imported [object Module] default export', async t => { t.is(main.exportedFunction(), 'foobar'); }); + +test('mocks inline `async import("name")`', async t => { + const writeJSConfigFile = await esmock('./local/usesInlineImport.mjs', { + isPurge : false + }, { + eslint : { + ESLint : function (...o) { + this.stringify = () => JSON.stringify(...o); + + return this; + } + } + }); + + t.is( + (await writeJSConfigFile('config', 'filePath')).stringify(), + JSON.stringify({ + baseConfig : 'config', + fix : true, + useEslintrc : false, + filePath : 'filePath' + })); + + const [ , key ] = writeJSConfigFile.esmockKey.match(/esmockKey=(\d*)/); + const keyRe = new RegExp(`esmockKey=${key}[^d]`); + + const moduleKeys = Object.keys(esmock.esmockCache.mockDefs) + .filter(moduleKey => keyRe.test(moduleKey)); + + t.true(moduleKeys.every(mkey => esmock.esmockCache.mockDefs[mkey])); + esmock.purge(writeJSConfigFile); + t.true(moduleKeys.every(mkey => esmock.esmockCache.mockDefs[mkey] === null)); +}); diff --git a/spec/local/usesInlineImport.mjs b/spec/local/usesInlineImport.mjs new file mode 100644 index 00000000..12612c85 --- /dev/null +++ b/spec/local/usesInlineImport.mjs @@ -0,0 +1,13 @@ + +async function writeJSConfigFile (config, filePath) { + const eslint = (await import('eslint')); + + return new eslint.ESLint({ + baseConfig : config, + fix : true, + useEslintrc : false, + filePath + }); +}; + +export default writeJSConfigFile; diff --git a/src/esmock.js b/src/esmock.js index a4b0573f..12a8747f 100644 --- a/src/esmock.js +++ b/src/esmock.js @@ -8,20 +8,35 @@ import { esmockCache } from './esmockCache.js'; +const argHasKey = (arg, key) => ( + arg && !/number|boolean/.test(typeof arg) && key in arg); + const esmock = async (modulePath, mockDefs, globalDefs, opt) => { // this functions caller is stack item '2' const calleePath = new Error().stack.split('\n')[2] .replace(/^.*file:\/\//, '') // rm everything before filepathfe .replace(/:[\d]*:[\d]*.*$/, '') // rm line and row number .replace(/^.*:/, ''); // rm windows-style drive locations + + // remap params in case options are provided as first arg + [ opt, mockDefs, globalDefs ] = argHasKey(mockDefs, 'isPurge') + ? [ mockDefs || {}, globalDefs || {}, opt || {} ] + : [ opt || {}, mockDefs || {}, globalDefs || {} ]; + const modulePathKey = await esmockModuleMock( calleePath, modulePath, mockDefs || {}, globalDefs || {}, opt || {}); const importedModule = await import(modulePathKey); - esmockModuleImportedPurge(modulePathKey); + if (opt.isPurge !== false) + esmockModuleImportedPurge(modulePathKey); - return esmockModuleImportedSanitize(importedModule); + return esmockModuleImportedSanitize(importedModule, modulePathKey); +}; + +esmock.purge = mockModule => { + if (argHasKey(mockModule, 'esmockKey')) + esmockModuleImportedPurge(mockModule.esmockKey); }; esmock.esmockCache = esmockCache; diff --git a/src/esmockCache.js b/src/esmockCache.js index cdba6cdd..7eaac0d4 100644 --- a/src/esmockCache.js +++ b/src/esmockCache.js @@ -1,5 +1,3 @@ -import path from 'path'; - const esmockCache = { resolvedPaths : {}, isESM : {}, @@ -15,9 +13,6 @@ const esmockCacheSet = (key, mockDef) => ( const esmockCacheGet = key => ( esmockCache.mockDefs[key]); -const esmockCacheResolvePathKey = (calleePath, localPath) => ( - path.join(path.dirname(calleePath), localPath)); - const esmockCacheResolvedPathIsESMGet = mockPathFull => ( esmockCache.isESM[mockPathFull]); @@ -30,7 +25,6 @@ export { esmockCache, esmockCacheSet, esmockCacheGet, - esmockCacheResolvePathKey, esmockCacheResolvedPathIsESMGet, esmockCacheResolvedPathIsESMSet }; diff --git a/src/esmockModule.js b/src/esmockModule.js index 7b3afdf5..74f5fdca 100644 --- a/src/esmockModule.js +++ b/src/esmockModule.js @@ -73,22 +73,22 @@ const esmockModuleIsESM = (mockPathFull, isesm) => { // return the default value directly, so that the esmock caller // does not need to lookup default as in "esmockedValue.default" -const esmockModuleImportedSanitize = importedModule => { +const esmockModuleImportedSanitize = (importedModule, esmockKey) => { const importedDefault = 'default' in importedModule && importedModule.default; if (!/boolean|string|number/.test(typeof importedDefault)) { // an example of [object Module]: import * as mod from 'fs'; export mod; return Object.prototype.toString.call(importedDefault) === '[object Module]' - ? Object.assign({}, importedDefault, importedModule) - : Object.assign(importedDefault, importedModule); + ? Object.assign({}, importedDefault, importedModule, { esmockKey }) + : Object.assign(importedDefault, importedModule, { esmockKey }); } return importedModule; }; const esmockModuleImportedPurge = modulePathKey => { - const purgeKey = key => esmockCacheSet(key, null); + const purgeKey = key => key !== 'null' && esmockCacheSet(key, null); const [ url, keys ] = modulePathKey.split('#esmockModuleKeys='); String(keys).split('#').forEach(purgeKey);