-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 982b46a
Showing
8 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import type { Nuxt } from '@nuxt/schema' | ||
import type { InlineConfig as VitestConfig } from 'vitest' | ||
import { InlineConfig, mergeConfig, defineConfig } from 'vite' | ||
import modules from './module' | ||
|
||
interface GetVitestConfigOptions { | ||
nuxt: Nuxt | ||
viteConfig: InlineConfig | ||
} | ||
|
||
// https://github.com/nuxt/framework/issues/6496 | ||
async function getNuxtAndViteConfig(rootDir = process.cwd()) { | ||
const { loadNuxt, buildNuxt } = await import('@nuxt/kit') | ||
const nuxt = await loadNuxt({ | ||
cwd: rootDir, | ||
dev: false, | ||
ready: false, | ||
overrides: { | ||
ssr: false, | ||
app: { | ||
rootId: 'nuxt-test', | ||
}, | ||
}, | ||
}) | ||
nuxt.options.modules.push(modules) | ||
await nuxt.ready() | ||
|
||
return new Promise<GetVitestConfigOptions>((resolve, reject) => { | ||
nuxt.hook('vite:extendConfig', viteConfig => { | ||
resolve({ nuxt, viteConfig }) | ||
throw new Error('_stop_') | ||
}) | ||
buildNuxt(nuxt).catch(err => { | ||
if (!err.toString().includes('_stop_')) { | ||
reject(err) | ||
} | ||
}) | ||
}).finally(() => nuxt.close()) | ||
} | ||
|
||
export async function getVitestConfig( | ||
options?: GetVitestConfigOptions | ||
): Promise<InlineConfig & { test: VitestConfig }> { | ||
if (!options) options = await getNuxtAndViteConfig() | ||
|
||
return { | ||
...options.viteConfig, | ||
test: { | ||
...options.viteConfig.test, | ||
dir: options.nuxt.options.rootDir, | ||
environment: 'nuxt', | ||
deps: { | ||
...options.viteConfig.test?.deps, | ||
inline: | ||
options.viteConfig.test?.deps?.inline === true | ||
? true | ||
: [ | ||
// vite-node defaults | ||
/\/(nuxt|nuxt3)\//, | ||
/^#/, | ||
// additional deps | ||
'vue', | ||
'vitest-environment-nuxt', | ||
...(options.nuxt.options.build.transpile.filter( | ||
r => typeof r === 'string' || r instanceof RegExp | ||
) as Array<string | RegExp>), | ||
...(options.viteConfig.test?.deps?.inline || []), | ||
], | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
export function defineConfigWithNuxtEnv(config: InlineConfig = {}) { | ||
return defineConfig(async () => { | ||
return mergeConfig(await getVitestConfig(), config) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type { Environment } from 'vitest' | ||
import { Window, GlobalWindow } from 'happy-dom' | ||
import { createFetch } from 'ofetch' | ||
import { createApp, toNodeListener } from 'h3' | ||
import type { App } from 'h3' | ||
import { populateGlobal } from 'vitest/environments' | ||
import { | ||
createCall, | ||
createFetch as createLocalFetch, | ||
} from 'unenv/runtime/fetch/index' | ||
|
||
export default <Environment>{ | ||
name: 'nuxt', | ||
async setup() { | ||
const win = new (GlobalWindow || Window)() as any as Window & { | ||
__app: App | ||
__registry: Set<string> | ||
__NUXT__: any | ||
$fetch: any | ||
fetch: any | ||
IntersectionObserver: any | ||
} | ||
|
||
win.__NUXT__ = { | ||
serverRendered: false, | ||
config: { | ||
public: {}, | ||
app: { baseURL: '/' }, | ||
}, | ||
data: {}, | ||
state: {}, | ||
} | ||
|
||
const app = win.document.createElement('div') | ||
// this is a workaround for a happy-dom bug with ids beginning with _ | ||
app.id = 'nuxt-test' | ||
win.document.body.appendChild(app) | ||
|
||
win.IntersectionObserver = | ||
win.IntersectionObserver || | ||
class IntersectionObserver { | ||
observe() {} | ||
} | ||
|
||
const h3App = createApp() | ||
|
||
// @ts-expect-error TODO: fix in h3 | ||
const localCall = createCall(toNodeListener(h3App)) | ||
const localFetch = createLocalFetch(localCall, globalThis.fetch) | ||
|
||
const registry = new Set<string>() | ||
|
||
win.fetch = (init: string, options?: any) => { | ||
if (typeof init === 'string' && registry.has(init)) { | ||
init = '/_' + init | ||
} | ||
return localFetch(init, options) | ||
} | ||
|
||
win.$fetch = createFetch({ fetch: win.fetch, Headers: win.Headers as any }) | ||
|
||
win.__registry = registry | ||
win.__app = h3App | ||
|
||
const { keys, originals } = populateGlobal(global, win, { | ||
bindFunctions: true, | ||
}) | ||
|
||
await import('#app/entry') | ||
|
||
return { | ||
// called after all tests with this env have been run | ||
teardown() { | ||
win.happyDOM.cancelAsync() | ||
// @ts-expect-error | ||
keys.forEach(key => delete global[key]) | ||
// @ts-expect-error | ||
originals.forEach((v, k) => (global[k] = v)) | ||
}, | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { defineNuxtModule, installModule } from '@nuxt/kit' | ||
import autoImportMock from './modules/auto-import-mock' | ||
|
||
export default defineNuxtModule({ | ||
meta: { | ||
name: 'vitest-env', | ||
}, | ||
async setup() { | ||
await installModule(autoImportMock) | ||
}, | ||
}) |
127 changes: 127 additions & 0 deletions
127
src/vitest-environment-nuxt/modules/auto-import-mock.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-env: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(), | ||
} | ||
}, | ||
}) | ||
}, | ||
}) |
33 changes: 33 additions & 0 deletions
33
src/vitest-environment-nuxt/runtime/components/RouterLink.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { defineComponent, useRouter } from '#imports' | ||
|
||
export const RouterLink = defineComponent({ | ||
functional: true, | ||
props: { | ||
to: String, | ||
custom: Boolean, | ||
replace: Boolean, | ||
// Not implemented | ||
activeClass: String, | ||
exactActiveClass: String, | ||
ariaCurrentValue: String, | ||
}, | ||
setup: (props, { slots }) => { | ||
const navigate = () => {} | ||
return () => { | ||
const route = props.to ? useRouter().resolve(props.to) : {} | ||
return props.custom | ||
? slots.default?.({ href: props.to, navigate, route }) | ||
: h( | ||
'a', | ||
{ | ||
href: props.to, | ||
onClick: (e: MouseEvent) => { | ||
e.preventDefault() | ||
return navigate() | ||
}, | ||
}, | ||
slots | ||
) | ||
} | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { defineEventHandler } from 'h3' | ||
import type { EventHandler } from 'h3' | ||
|
||
export function registerEndpoint(url: string, handler: EventHandler) { | ||
// @ts-expect-error private property | ||
if (!window.__app) return | ||
// @ts-expect-error private property | ||
window.__app.use('/_' + url, defineEventHandler(handler)) | ||
// @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') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { mount, VueWrapper } from '@vue/test-utils' | ||
import { h, DefineComponent, Suspense, nextTick } from 'vue' | ||
|
||
import { RouterLink } from './components/RouterLink' | ||
|
||
// @ts-expect-error virtual file | ||
import NuxtRoot from '#build/root-component.mjs' | ||
|
||
export async function mountSuspended< | ||
T extends DefineComponent<any, any, any, any> | ||
>(component: T) { | ||
return new Promise<VueWrapper<InstanceType<T>>>(resolve => { | ||
const vm = mount( | ||
{ | ||
setup: NuxtRoot.setup, | ||
render: () => | ||
h( | ||
Suspense, | ||
{ onResolve: () => nextTick().then(() => resolve(vm as any)) }, | ||
{ default: () => h(component) } | ||
), | ||
}, | ||
{ | ||
global: { | ||
components: { | ||
RouterLink, | ||
}, | ||
}, | ||
} | ||
) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { registerEndpoint, mockNuxtImport } from './runtime/mock' | ||
export { mountSuspended } from './runtime/mount' |