Skip to content

Commit

Permalink
Merge pull request #127 from iambumblehead/mock-modules-that-arent-found
Browse files Browse the repository at this point in the history
mock modules that aren't found, test failing
  • Loading branch information
iambumblehead committed Aug 25, 2022
2 parents 5d9dce2 + 9d9383d commit 968b357
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Expand Up @@ -15,7 +15,7 @@ jobs:
timeout-minutes: 5
strategy:
matrix:
node-version: [18.x]
node-version: [18.6]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Expand Up @@ -15,7 +15,7 @@ jobs:
timeout-minutes: 6
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [14.x, 16.x, 18.6]
os: [ubuntu-latest, windows-latest]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,7 @@
# changelog

* 1.9.7 _Aug.25.2022_
* support mocking specifiers that [aren't found in filesystem](https://github.com/iambumblehead/esmock/issues/126)
* 1.9.6 _Aug.24.2022_
* support parent url to facilitate sourcemap usage, [113](https://github.com/iambumblehead/esmock/issues/113)
* support import subpaths, eg `import: { '#sub': './path.js' }`
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "esmock",
"type": "module",
"version": "1.9.6",
"version": "1.9.7",
"license": "ISC",
"readmeFilename": "README.md",
"description": "provides native ESM import mocking for unit tests",
Expand Down
3 changes: 3 additions & 0 deletions src/esmockDummy.js
@@ -0,0 +1,3 @@
// ex, file:///path/to/esmockDummy.js,
// file:///c:/path/to/esmockDummy.js
export default import.meta.url
69 changes: 39 additions & 30 deletions src/esmockLoader.js
@@ -1,20 +1,12 @@
import process from 'process'
import path from 'path'
import url from 'url'

import esmock from './esmock.js'
import esmockIsLoader from './esmockIsLoader.js'
import urlDummy from './esmockDummy.js'

global.esmockloader = esmockIsLoader

export default esmock

// ex, file:///path/to/esmock,
// file:///c:/path/to/esmock
const urlDummy = 'file:///' + path
.join(path.dirname(url.fileURLToPath(import.meta.url)), 'esmock.js')
.replace(/^\//, '')

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

Expand All @@ -25,6 +17,22 @@ const exportNamesRe = /.*exportNames=(.*)/
const esmockKeyRe = /esmockKey=\d*/
const withHashRe = /.*#-#/
const isesmRe = /isesm=true/
const notfoundRe = /notfound=([^&]*)/

// new versions of node: when multiple loaders are used and context
// is passed to nextResolve, the process crashes in a recursive call
// see: /esmock/issues/#48
//
// old versions of node: if context.parentURL is defined, and context
// is not passed to nextResolve, the tests fail
//
// later versions of node v16 include 'node-addons'
const nextResolveCall = async (nextResolve, specifier, context) => (
context.parentURL &&
(context.conditions.slice(-1)[0] === 'node-addons'
|| context.importAssertions || isLT1612)
? await nextResolve(specifier, context)
: await nextResolve(specifier))

const resolve = async (specifier, context, nextResolve) => {
const { parentURL } = context
Expand All @@ -33,32 +41,31 @@ const resolve = async (specifier, context, nextResolve) => {
const esmockKeyLong = esmockKeyParamSmall
? global.esmockKeyGet(esmockKeyParamSmall.split('=')[1])
: parentURL
const [ esmockKeyParam ] =
(esmockKeyLong && esmockKeyLong.match(esmockKeyRe) || [])

// new versions of node: when multiple loaders are used and context
// is passed to nextResolve, the process crashes in a recursive call
// see: /esmock/issues/#48
//
// old versions of node: if context.parentURL is defined, and context
// is not passed to nextResolve, the tests fail
//
// later versions of node v16 include 'node-addons'
const resolved = context.parentURL && (
context.conditions.slice(-1)[0] === 'node-addons'
|| context.importAssertions || isLT1612)
? await nextResolve(specifier, context)
: await nextResolve(specifier)

if (!esmockKeyParam)
return resolved
if (!esmockKeyRe.test(esmockKeyLong))
return nextResolveCall(nextResolve, specifier, context)

const [ esmockKeyParam ] = String(esmockKeyLong).match(esmockKeyRe)
const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe)
const moduleGlobals = keyUrl && keyUrl.replace(esmockGlobalsAndBeforeRe, '')
// do not call 'nextResolve' for notfound modules
if (esmockKeyLong.includes(`notfound=${specifier}`)) {
const moduleKeyRe = new RegExp( // eslint-disable-line prefer-destructuring
'.*file:///' + specifier + '(\\?' + esmockKeyParam + '(?:(?!#-#).)*).*')
const moduleKey = ( // eslint-disable-line prefer-destructuring
moduleGlobals.match(moduleKeyRe) || keys.match(moduleKeyRe) || [])[1]
if (moduleKey) {
return {
shortCircuit: true,
url: urlDummy + moduleKey
}
}
}

const resolved = await nextResolveCall(nextResolve, specifier, context)
const resolvedurl = decodeURI(resolved.url)
const moduleKeyRe = new RegExp(
'.*(' + resolvedurl + '\\?' + esmockKeyParam + '(?:(?!#-#).)*).*')

const [ keyUrl, keys ] = esmockKeyLong.split(esmockModuleKeysRe)
const moduleGlobals = keyUrl.replace(esmockGlobalsAndBeforeRe, '')
const moduleKeyChild = moduleKeyRe.test(keys)
&& keys.replace(moduleKeyRe, '$1')
const moduleKeyGlobal = moduleKeyRe.test(moduleGlobals)
Expand All @@ -85,6 +92,8 @@ const load = async (url, context, nextLoad) => {
url = url.replace(esmockGlobalsAndAfterRe, '')
if (url.startsWith(urlDummy)) {
url = url.replace(withHashRe, '')
if (notfoundRe.test(url))
url = url.replace(urlDummy, `file:///${(url.match(notfoundRe) || [])[1]}`)
}

const exportedNames = exportNamesRe.test(url) &&
Expand Down
16 changes: 9 additions & 7 deletions src/esmockModule.js
Expand Up @@ -12,7 +12,7 @@ import {

const isObj = o => typeof o === 'object' && o
const isDefaultDefined = o => isObj(o) && 'default' in o

const isDirPathRe = /^\.?\.?([a-zA-Z]:)?(\/|\\)/
const FILE_PROTOCOL = 'file:///'

// https://url.spec.whatwg.org/, eg, file:///C:/demo file:///root/linux/path
Expand All @@ -21,9 +21,9 @@ const pathAddProtocol = (pathFull, protocol) => {
protocol = /^node:/.test(pathFull)
? ''
: !resolvewith.iscoremodule(pathFull) ? FILE_PROTOCOL : 'node:'
if (protocol.includes(FILE_PROTOCOL))
if (protocol.includes(FILE_PROTOCOL) && isDirPathRe.test(pathFull))
pathFull = fs.realpathSync.native(pathFull)
if (process.platform === 'win32')
if (process.platform === 'win32' && isDirPathRe.test(pathFull))
pathFull = pathFull.split(path.sep).join(path.posix.sep)
return `${protocol}${pathFull.replace(/^\//, '')}`
}
Expand Down Expand Up @@ -77,6 +77,7 @@ const esmockModuleIsESM = (mockPathFull, isesm) => {
return isesm

isesm = !resolvewith.iscoremodule(mockPathFull)
&& isDirPathRe.test(mockPathFull)
&& esmockModuleESMRe.test(fs.readFileSync(mockPathFull, 'utf-8'))

esmockCacheResolvedPathIsESMSet(mockPathFull, isesm)
Expand Down Expand Up @@ -122,6 +123,7 @@ const esmockModuleCreate = async (esmockKey, key, mockPathFull, mockDef, opt) =>
'esmockKey=' + esmockKey,
'esmockModuleKey=' + key,
'isesm=' + isesm,
opt.isfound ? 'found' : 'notfound=' + key,
mockExportNames ? 'exportNames=' + mockExportNames : 'exportNone'
].join('&')

Expand All @@ -139,23 +141,23 @@ const esmockModulesCreate = async (pathCallee, pathModule, esmockKey, defs, keys
return mocks

let mockedPathFull = resolvewith(keys[0], pathCallee)
if (!mockedPathFull) {
if (!mockedPathFull && opt.isPackageNotFoundError !== false) {
pathCallee = pathCallee
.replace(/^\/\//, '')
.replace(process.cwd(), '.')
.replace(process.env.HOME, '~')
throw new Error(`not a valid path: "${keys[0]}" (used by ${pathCallee})`)
}

if (process.platform === 'win32')
if (mockedPathFull && process.platform === 'win32')
mockedPathFull = mockedPathFull.split(path.sep).join(path.posix.sep)

mocks.push(await esmockModuleCreate(
esmockKey,
keys[0],
mockedPathFull,
mockedPathFull || keys[0],
defs[keys[0]],
opt
Object.assign({ isfound: Boolean(mockedPathFull) }, opt)
))

return esmockModulesCreate(
Expand Down
3 changes: 3 additions & 0 deletions tests/local/notinstalledVueComponent.js
@@ -0,0 +1,3 @@
import {h} from 'vue'

export default () => h('svg', { /* some properties */ })
2 changes: 1 addition & 1 deletion tests/tests-ava/spec/esmock.ava.spec.js
Expand Up @@ -2,7 +2,7 @@ import test from 'ava'
import esmock from 'esmock'
import sinon from 'sinon'

test('should not error when handling non-extnsible object', async t => {
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', {
Expand Down
25 changes: 25 additions & 0 deletions tests/tests-node/esmock.node.test.js
Expand Up @@ -4,6 +4,30 @@ import assert from 'node:assert/strict'
import esmock from '../../src/esmock.js'
import sinon from 'sinon'

test('should mock package, even when package is not installed', async () => {
const component = await esmock(`../local/notinstalledVueComponent.js`, {
vue: {
h: (...args) => args
}
}, {}, {
isPackageNotFoundError: false
})

assert.strictEqual(component()[0], 'svg')
})

test('should mock package, even when package is not installed', async () => {
const component = await esmock(`../local/notinstalledVueComponent.js`, {}, {
vue: {
h: (...args) => args
}
}, {
isPackageNotFoundError: false
})

assert.strictEqual(component()[0], 'svg')
})

test('should mock a subpath', async () => {
const localpackagepath = path.resolve('../local/')
const { subpathfunctionWrap } = await esmock(
Expand Down Expand Up @@ -409,3 +433,4 @@ test('should strict mock by default, partial mock optional', async () => {

assert.deepEqual(pathWrapPartial.basename('/dog.png'), 'dog.png')
})

0 comments on commit 968b357

Please sign in to comment.