Skip to content
This repository has been archived by the owner on Dec 19, 2023. It is now read-only.

Commit

Permalink
feat: mockNuxtImport utility (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Dec 21, 2022
1 parent fdd0db8 commit 0b82d14
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 4 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ When you run your tests within `vitest-environment-nuxt`, they will be running i

This means you should be take particular care not to mutate the global state in your tests (or, if you have, to reset it afterwards).

## 🛠️ Helpers

`vitest-environment-nuxt` provides a number of helpers to make testing Nuxt apps easier.

### `mountSuspended`

// TODO:

### `mockNuxtImport`

`mockNuxtImport` allows you to mock Nuxt's auto import functionality. For example, to mock `useStorage`, you can do so like this:

```ts
import { mockNuxtImport } from 'vitest-environment-nuxt/utils'

mockNuxtImport('useStorage', () => {
return () => {
return { value: 'mocked storage' }
}
})

// your tests here
```

## 💻 Development

- Clone this repository
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
"h3": "^1.0.2",
"happy-dom": "^8.1.0",
"ofetch": "^1.0.0",
"magic-string": "^0.27.0",
"estree-walker": "^3.0.1",
"unenv": "^1.0.0"
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions playground/composables/auto-import-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function useAutoImportedTarget() {
return 'the original'
}

export function useAutoImportedNonTarget() {
return 'the original'
}
10 changes: 10 additions & 0 deletions playground/modules/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineNuxtModule } from "@nuxt/kit"

export default defineNuxtModule({
meta: {
name: 'custom',
},
setup(_, nuxt) {
console.log('From custom module!')
}
})
4 changes: 3 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({

modules: [
'~/modules/custom'
]
})
11 changes: 11 additions & 0 deletions playground/tests/auto-import-mock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expect, it } from 'vitest'
import { mockNuxtImport } from 'vitest-environment-nuxt/utils'

mockNuxtImport<typeof useAutoImportedTarget>('useAutoImportedTarget', () => {
return () => 'mocked!'
})

it('should mock', () => {
expect(useAutoImportedTarget()).toMatchInlineSnapshot('"mocked!"')
expect(useAutoImportedNonTarget()).toMatchInlineSnapshot('"the original"')
})
6 changes: 6 additions & 0 deletions playground/tests/auto-import.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, it } from 'vitest'

it('should not mock', () => {
expect(useAutoImportedTarget()).toMatchInlineSnapshot('"the original"')
expect(useAutoImportedNonTarget()).toMatchInlineSnapshot('"the original"')
})
7 changes: 5 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { loadNuxt, buildNuxt } from '@nuxt/kit'
import type { InlineConfig as VitestConfig } from 'vitest'
import { InlineConfig, mergeConfig, defineConfig } from 'vite'
import autoImportMock from './modules/auto-import-mock'

// https://github.com/nuxt/framework/issues/6496
async function getViteConfig(rootDir = process.cwd()) {
const nuxt = await loadNuxt({
cwd: rootDir,
dev: false,
ready: false,
overrides: {
ssr: false,
app: {
rootId: 'nuxt-test',
},
},
})
nuxt.options.modules.push(autoImportMock)
await nuxt.ready()

return new Promise<InlineConfig>((resolve, reject) => {
nuxt.hook('vite:extendConfig', config => {
resolve(config)
Expand Down
127 changes: 127 additions & 0 deletions src/modules/auto-import-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Import } from 'unimport'
import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
import { walk } from 'estree-walker'
import type { CallExpression } from 'estree'
import { AcornNode } from 'rollup'
import MagicString from 'magic-string'

const HELPER_NAME = 'mockNuxtImport'

export interface MockInfo {
name: string
import: Import
factory: string
start: number
end: number
}

/**
* This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`,
* which make it possible to mock Nuxt imports.
*/
export default defineNuxtModule({
meta: {
name: 'vitest:auto-import-mock',
},
setup(_, nuxt) {
let imports: Import[] = []

nuxt.hook('imports:extend', _ => {
imports = _
})

addVitePlugin({
name: 'nuxt:auto-import-mock',
transform(code, id) {
if (!code.includes(HELPER_NAME)) return
if (id.includes('/node_modules/')) return

let ast: AcornNode
try {
ast = this.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
ranges: true,
})
} catch (e) {
return
}

const mocks: MockInfo[] = []

walk(ast, {
enter: node => {
if (node.type !== 'CallExpression') return
const call = node as CallExpression
if (
call.callee.type !== 'Identifier' ||
call.callee.name !== HELPER_NAME
) {
return
}
if (call.arguments.length !== 2) {
return
}
if (call.arguments[0].type !== 'Literal') {
return // TODO: warn
}
const name = call.arguments[0].value as string
const importItem = imports.find(_ => name === (_.as || _.name))
if (!importItem) {
return this.error(`Cannot find import "${name}" to mock`)
}
mocks.push({
name,
import: importItem,
factory: code.slice(
call.arguments[1].range![0],
call.arguments[1].range![1]
),
start: call.range![0],
end: call.range![1],
})
},
})

if (!mocks.length) return

const s = new MagicString(code)

const mockMap = new Map<string, MockInfo[]>()
for (const mock of mocks) {
s.overwrite(mock.start, mock.end, '')
if (!mockMap.has(mock.import.from)) {
mockMap.set(mock.import.from, [])
}
mockMap.get(mock.import.from)!.push(mock)
}

const mockCode = [...mockMap.entries()]
.map(([from, mocks]) => {
const lines = [
`vi.mock(${JSON.stringify(from)}, async () => {`,
` const mod = { ...await vi.importActual(${JSON.stringify(
from
)}) }`,
]
for (const mock of mocks) {
lines.push(
` mod[${JSON.stringify(mock.name)}] = (${mock.factory})()`
)
}
lines.push(` return mod`)
lines.push(`})`)
return lines.join('\n')
})
.join('\n')

s.append('\nimport {vi} from "vitest";\n' + mockCode)

return {
code: s.toString(),
map: s.generateMap(),
}
},
})
},
})
7 changes: 7 additions & 0 deletions src/runtime/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ export function registerEndpoint(url: string, handler: EventHandler) {
// @ts-expect-error private property
window.__registry.add(url)
}

export function mockNuxtImport<T = any>(
name: string,
factory: () => T | Promise<T>
) {
throw new Error('mockNuxtImport() is a macro and it did not get transpiled')
}
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { registerEndpoint } from './runtime/mock'
export { registerEndpoint, mockNuxtImport } from './runtime/mock'
export { mountSuspended } from './runtime/mount'

0 comments on commit 0b82d14

Please sign in to comment.