[READ-ONLY] Mirror of https://github.com/danielroe/nuxt-vitest. An vitest environment with support for testing code that needs a Nuxt runtime environment
nuxt nuxt-module testing unit-testing vitest
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: `mockNuxtImport` utility (#7)

authored by

Anthony Fu and committed by
GitHub
(Dec 21, 2022, 3:54 PM UTC) 0b82d14d fdd0db8b

+208 -4
+24
README.md
··· 42 42 43 43 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). 44 44 45 + ## 🛠️ Helpers 46 + 47 + `vitest-environment-nuxt` provides a number of helpers to make testing Nuxt apps easier. 48 + 49 + ### `mountSuspended` 50 + 51 + // TODO: 52 + 53 + ### `mockNuxtImport` 54 + 55 + `mockNuxtImport` allows you to mock Nuxt's auto import functionality. For example, to mock `useStorage`, you can do so like this: 56 + 57 + ```ts 58 + import { mockNuxtImport } from 'vitest-environment-nuxt/utils' 59 + 60 + mockNuxtImport('useStorage', () => { 61 + return () => { 62 + return { value: 'mocked storage' } 63 + } 64 + }) 65 + 66 + // your tests here 67 + ``` 68 + 45 69 ## 💻 Development 46 70 47 71 - Clone this repository
+2
package.json
··· 58 58 "h3": "^1.0.2", 59 59 "happy-dom": "^8.1.0", 60 60 "ofetch": "^1.0.0", 61 + "magic-string": "^0.27.0", 62 + "estree-walker": "^3.0.1", 61 63 "unenv": "^1.0.0" 62 64 }, 63 65 "devDependencies": {
+5 -2
pnpm-lock.yaml
··· 16 16 eslint: latest 17 17 eslint-config-prettier: latest 18 18 eslint-plugin-prettier: latest 19 + estree-walker: ^3.0.1 19 20 h3: ^1.0.2 20 21 happy-dom: ^8.1.0 21 22 husky: latest 22 23 lint-staged: latest 24 + magic-string: ^0.27.0 23 25 nuxt: 3.0.0 24 26 ofetch: ^1.0.0 25 27 pinst: latest ··· 32 34 dependencies: 33 35 '@nuxt/kit': 3.0.0 34 36 '@vue/test-utils': 2.2.6_vue@3.2.45 37 + estree-walker: 3.0.1 35 38 h3: 1.0.2 36 39 happy-dom: 8.1.0 40 + magic-string: 0.27.0 37 41 ofetch: 1.0.0 38 42 unenv: 1.0.0 39 43 devDependencies: ··· 2255 2259 dev: true 2256 2260 2257 2261 /concat-map/0.0.1: 2258 - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} 2262 + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 2259 2263 dev: true 2260 2264 2261 2265 /concat-stream/2.0.0: ··· 5144 5148 engines: {node: '>=12'} 5145 5149 dependencies: 5146 5150 '@jridgewell/sourcemap-codec': 1.4.14 5147 - dev: true 5148 5151 5149 5152 /make-dir/3.1.0: 5150 5153 resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+3 -1
playground/nuxt.config.ts
··· 1 1 // https://v3.nuxtjs.org/api/configuration/nuxt.config 2 2 export default defineNuxtConfig({ 3 - 3 + modules: [ 4 + '~/modules/custom' 5 + ] 4 6 })
+5
src/config.ts
··· 1 1 import { loadNuxt, buildNuxt } from '@nuxt/kit' 2 2 import type { InlineConfig as VitestConfig } from 'vitest' 3 3 import { InlineConfig, mergeConfig, defineConfig } from 'vite' 4 + import autoImportMock from './modules/auto-import-mock' 4 5 5 6 // https://github.com/nuxt/framework/issues/6496 6 7 async function getViteConfig(rootDir = process.cwd()) { 7 8 const nuxt = await loadNuxt({ 8 9 cwd: rootDir, 9 10 dev: false, 11 + ready: false, 10 12 overrides: { 11 13 ssr: false, 12 14 app: { ··· 14 16 }, 15 17 }, 16 18 }) 19 + nuxt.options.modules.push(autoImportMock) 20 + await nuxt.ready() 21 + 17 22 return new Promise<InlineConfig>((resolve, reject) => { 18 23 nuxt.hook('vite:extendConfig', config => { 19 24 resolve(config)
+1 -1
src/utils.ts
··· 1 - export { registerEndpoint } from './runtime/mock' 1 + export { registerEndpoint, mockNuxtImport } from './runtime/mock' 2 2 export { mountSuspended } from './runtime/mount'
+7
playground/composables/auto-import-mock.ts
··· 1 + export function useAutoImportedTarget() { 2 + return 'the original' 3 + } 4 + 5 + export function useAutoImportedNonTarget() { 6 + return 'the original' 7 + }
+10
playground/modules/custom.ts
··· 1 + import { defineNuxtModule } from "@nuxt/kit" 2 + 3 + export default defineNuxtModule({ 4 + meta: { 5 + name: 'custom', 6 + }, 7 + setup(_, nuxt) { 8 + console.log('From custom module!') 9 + } 10 + })
+11
playground/tests/auto-import-mock.spec.ts
··· 1 + import { expect, it } from 'vitest' 2 + import { mockNuxtImport } from 'vitest-environment-nuxt/utils' 3 + 4 + mockNuxtImport<typeof useAutoImportedTarget>('useAutoImportedTarget', () => { 5 + return () => 'mocked!' 6 + }) 7 + 8 + it('should mock', () => { 9 + expect(useAutoImportedTarget()).toMatchInlineSnapshot('"mocked!"') 10 + expect(useAutoImportedNonTarget()).toMatchInlineSnapshot('"the original"') 11 + })
+6
playground/tests/auto-import.spec.ts
··· 1 + import { expect, it } from 'vitest' 2 + 3 + it('should not mock', () => { 4 + expect(useAutoImportedTarget()).toMatchInlineSnapshot('"the original"') 5 + expect(useAutoImportedNonTarget()).toMatchInlineSnapshot('"the original"') 6 + })
+127
src/modules/auto-import-mock.ts
··· 1 + import type { Import } from 'unimport' 2 + import { addVitePlugin, defineNuxtModule } from '@nuxt/kit' 3 + import { walk } from 'estree-walker' 4 + import type { CallExpression } from 'estree' 5 + import { AcornNode } from 'rollup' 6 + import MagicString from 'magic-string' 7 + 8 + const HELPER_NAME = 'mockNuxtImport' 9 + 10 + export interface MockInfo { 11 + name: string 12 + import: Import 13 + factory: string 14 + start: number 15 + end: number 16 + } 17 + 18 + /** 19 + * This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`, 20 + * which make it possible to mock Nuxt imports. 21 + */ 22 + export default defineNuxtModule({ 23 + meta: { 24 + name: 'vitest:auto-import-mock', 25 + }, 26 + setup(_, nuxt) { 27 + let imports: Import[] = [] 28 + 29 + nuxt.hook('imports:extend', _ => { 30 + imports = _ 31 + }) 32 + 33 + addVitePlugin({ 34 + name: 'nuxt:auto-import-mock', 35 + transform(code, id) { 36 + if (!code.includes(HELPER_NAME)) return 37 + if (id.includes('/node_modules/')) return 38 + 39 + let ast: AcornNode 40 + try { 41 + ast = this.parse(code, { 42 + sourceType: 'module', 43 + ecmaVersion: 'latest', 44 + ranges: true, 45 + }) 46 + } catch (e) { 47 + return 48 + } 49 + 50 + const mocks: MockInfo[] = [] 51 + 52 + walk(ast, { 53 + enter: node => { 54 + if (node.type !== 'CallExpression') return 55 + const call = node as CallExpression 56 + if ( 57 + call.callee.type !== 'Identifier' || 58 + call.callee.name !== HELPER_NAME 59 + ) { 60 + return 61 + } 62 + if (call.arguments.length !== 2) { 63 + return 64 + } 65 + if (call.arguments[0].type !== 'Literal') { 66 + return // TODO: warn 67 + } 68 + const name = call.arguments[0].value as string 69 + const importItem = imports.find(_ => name === (_.as || _.name)) 70 + if (!importItem) { 71 + return this.error(`Cannot find import "${name}" to mock`) 72 + } 73 + mocks.push({ 74 + name, 75 + import: importItem, 76 + factory: code.slice( 77 + call.arguments[1].range![0], 78 + call.arguments[1].range![1] 79 + ), 80 + start: call.range![0], 81 + end: call.range![1], 82 + }) 83 + }, 84 + }) 85 + 86 + if (!mocks.length) return 87 + 88 + const s = new MagicString(code) 89 + 90 + const mockMap = new Map<string, MockInfo[]>() 91 + for (const mock of mocks) { 92 + s.overwrite(mock.start, mock.end, '') 93 + if (!mockMap.has(mock.import.from)) { 94 + mockMap.set(mock.import.from, []) 95 + } 96 + mockMap.get(mock.import.from)!.push(mock) 97 + } 98 + 99 + const mockCode = [...mockMap.entries()] 100 + .map(([from, mocks]) => { 101 + const lines = [ 102 + `vi.mock(${JSON.stringify(from)}, async () => {`, 103 + ` const mod = { ...await vi.importActual(${JSON.stringify( 104 + from 105 + )}) }`, 106 + ] 107 + for (const mock of mocks) { 108 + lines.push( 109 + ` mod[${JSON.stringify(mock.name)}] = (${mock.factory})()` 110 + ) 111 + } 112 + lines.push(` return mod`) 113 + lines.push(`})`) 114 + return lines.join('\n') 115 + }) 116 + .join('\n') 117 + 118 + s.append('\nimport {vi} from "vitest";\n' + mockCode) 119 + 120 + return { 121 + code: s.toString(), 122 + map: s.generateMap(), 123 + } 124 + }, 125 + }) 126 + }, 127 + })
+7
src/runtime/mock.ts
··· 9 9 // @ts-expect-error private property 10 10 window.__registry.add(url) 11 11 } 12 + 13 + export function mockNuxtImport<T = any>( 14 + name: string, 15 + factory: () => T | Promise<T> 16 + ) { 17 + throw new Error('mockNuxtImport() is a macro and it did not get transpiled') 18 + }