UNPKG

@nuxt/test-utils

Version:
921 lines (911 loc) 32.8 kB
import { r as loadKit } from "./utils-whMnUpQ0.mjs"; import { i as sendMessageToCli, n as listenCliMessages, t as createVitestTestSummary } from "./interface-BsWSTNwj.mjs"; import { hasTTY, isCI, provider } from "std-env"; import { dirname, extname, join, relative, resolve } from "pathe"; import { existsSync, promises, readFileSync } from "node:fs"; import { distDir } from "#dirs"; import process$1 from "node:process"; import { addDevServerHandler, createResolver, defineNuxtModule, logger, resolveIgnorePatterns, resolvePath, useNuxt } from "@nuxt/kit"; import { addCustomTab, onDevToolsInitialized, refreshCustomTabs } from "@nuxt/devtools-kit"; import { walk } from "estree-walker"; import MagicString from "magic-string"; import { createUnplugin } from "unplugin"; import { cancel, confirm, intro, isCancel, multiselect, outro, select } from "@clack/prompts"; import { colors } from "consola/utils"; import { addDependency, detectPackageManager } from "nypm"; import { h } from "vue"; import { debounce } from "perfect-debounce"; import { fork } from "node:child_process"; //#region src/module/plugins/mock.ts const PLUGIN_NAME$1 = "nuxt:vitest:mock-transform"; const HELPER_MOCK_IMPORT = "mockNuxtImport"; const HELPER_UNMOCK_IMPORT = "unmockNuxtImport"; const HELPER_MOCK_COMPONENT = "mockComponent"; const HELPER_MOCK_HOIST = "__NUXT_VITEST_MOCKS"; const HELPER_MOCK_HOIST_ORIGINAL = "__NUXT_VITEST_MOCKS_ORIGINAL"; const HELPER_MOCK_HOIST_PREVIOUS = "__NUXT_VITEST_MOCKS_PREVIOUS"; const HELPERS_NAME = [ HELPER_MOCK_IMPORT, HELPER_UNMOCK_IMPORT, HELPER_MOCK_COMPONENT ]; const createMockPlugin = (ctx) => createUnplugin(() => { return { name: PLUGIN_NAME$1, enforce: "post", vite: { transform(code, id) { if (!HELPERS_NAME.some((n) => code.includes(n))) return; if (id.includes("/node_modules/")) return; let ast; try { ast = this.parse(code, { sourceType: "module", ecmaVersion: "latest", ranges: true }); } catch { return; } let insertionPoint = 0; let hasViImport = false; const s = new MagicString(code); const mocksImport = []; const unmocksFrom = /* @__PURE__ */ new Set(); const mocksComponent = []; const importPathsList = /* @__PURE__ */ new Set(); walk(ast, { enter: (node, parent) => { const removeCallExpression = (start, end = start) => s.overwrite(isExpressionStatement(parent) ? startOf(parent) : startOf(start), isExpressionStatement(parent) ? endOf(parent) : endOf(end), ""); const parseMockImportTarget = (importTarget, helperName) => { const name = isLiteral(importTarget) ? importTarget.value : isIdentifier(importTarget) ? importTarget.name : void 0; if (typeof name !== "string") return this.error(/* @__PURE__ */ new Error(`The first argument of ${helperName}() must be a string literal or mocked target`), startOf(importTarget)); return { name, importItem: ctx.imports.find((_) => name === (_.as || _.name)) }; }; if (isImportDeclaration(node)) { if (node.source.value === "vitest" && !hasViImport) { if (node.specifiers.find((i) => isImportSpecifier(i) && i.imported.type === "Identifier" && i.imported.name === "vi")) { insertionPoint = endOf(node); hasViImport = true; } return; } } if (!isCallExpression(node)) return; if (isIdentifier(node.callee) && node.callee.name === HELPER_MOCK_IMPORT) { if (node.arguments.length !== 2) return this.error(/* @__PURE__ */ new Error(`${HELPER_MOCK_IMPORT}() should have exactly 2 arguments`), startOf(node)); const { name, importItem } = parseMockImportTarget(node.arguments[0], HELPER_MOCK_IMPORT); if (!importItem) return this.error(`Cannot find import "${name}" to mock`); removeCallExpression(node.arguments[0], node.arguments[1]); mocksImport.push({ name, import: importItem, factory: code.slice(startOf(node.arguments[1]), endOf(node.arguments[1])) }); } if (isIdentifier(node.callee) && node.callee.name === HELPER_UNMOCK_IMPORT) { if (node.arguments.length !== 1) return this.error(/* @__PURE__ */ new Error(`${HELPER_UNMOCK_IMPORT}() should have exactly 1 argument`), startOf(node)); const { name, importItem } = parseMockImportTarget(node.arguments[0], HELPER_UNMOCK_IMPORT); if (!importItem) return this.error(`Cannot find import "${name}" to unmock`); removeCallExpression(node.arguments[0]); unmocksFrom.add(importItem.from); mocksImport.push({ name, import: importItem, factory: void 0 }); } if (isIdentifier(node.callee) && node.callee.name === HELPER_MOCK_COMPONENT) { if (node.arguments.length !== 2) return this.error(/* @__PURE__ */ new Error(`${HELPER_MOCK_COMPONENT}() should have exactly 2 arguments`), startOf(node)); const componentName = node.arguments[0]; if (!isLiteral(componentName) || typeof componentName.value !== "string") return this.error(/* @__PURE__ */ new Error(`The first argument of ${HELPER_MOCK_COMPONENT}() must be a string literal`), startOf(componentName)); const pathOrName = componentName.value; const path = ctx.components.find((_) => _.pascalName === pathOrName || _.kebabName === pathOrName)?.filePath || pathOrName; removeCallExpression(node.arguments[1]); mocksComponent.push({ path, factory: code.slice(startOf(node.arguments[1]), endOf(node.arguments[1])) }); } } }); if (mocksImport.length === 0 && mocksComponent.length === 0) return; const mockLines = []; for (const from of unmocksFrom) mockLines.push(`vi.unmock(${JSON.stringify(from)});`); for (const [from, mocks] of mapGroupBy(mocksImport, (mock) => mock.import.from)) { importPathsList.add(from); const quotedFrom = JSON.stringify(from); const mockModuleEntry = `globalThis.${HELPER_MOCK_HOIST}[${quotedFrom}]`; mockLines.push(`vi.mock(${quotedFrom}, async (importOriginal) => {`, ` if (!${mockModuleEntry} || ${unmocksFrom.has(from)}) {`, ` const original = await importOriginal()`, ` const previous = (${mockModuleEntry} ?? {}).${HELPER_MOCK_HOIST_PREVIOUS} ?? {}`, ` ${mockModuleEntry} = { ...original, ...previous }`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_ORIGINAL} = { ...original }`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS} = { ...previous }`, ` }`); for (const mock of mocks) { const quotedName = JSON.stringify(mock.import.name); const original = `${mockModuleEntry}.${HELPER_MOCK_HOIST_ORIGINAL}[${quotedName}]`; if (mock.factory === void 0) mockLines.push(` ${mockModuleEntry}[${quotedName}] = ${original}`, ` delete ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS}[${quotedName}]`); else mockLines.push(` ${mockModuleEntry}[${quotedName}] = await (${mock.factory})(${original})`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS}[${quotedName}] = ${mockModuleEntry}[${quotedName}]`); } mockLines.push(` return ${mockModuleEntry}`); mockLines.push(`});`); } if (mocksComponent.length) mockLines.push(...mocksComponent.flatMap((mock) => { return [ `vi.mock(${JSON.stringify(mock.path)}, async () => {`, ` const factory = (${mock.factory});`, ` const result = typeof factory === 'function' ? await factory() : await factory`, ` return 'default' in result ? result : { default: result }`, "});" ]; })); if (!mockLines.length) return; s.appendLeft(insertionPoint, [ ``, `vi.hoisted(() => {`, ` if(!globalThis.${HELPER_MOCK_HOIST}){`, ` vi.stubGlobal(${JSON.stringify(HELPER_MOCK_HOIST)}, {})`, ` }`, `});`, `` ].join("\n")); if (!hasViImport) s.prepend(`import {vi} from "vitest";\n`); s.appendLeft(insertionPoint, "\n" + mockLines.join("\n") + "\n"); importPathsList.forEach((p) => { s.append(`\n import ${JSON.stringify(p)};`); }); return { code: s.toString(), map: s.generateMap({ hires: true }) }; }, async configResolved(config) { const plugins = config.plugins || []; const vitestPlugins = plugins.filter((p) => (p.name === "vite:mocks" || p.name.startsWith("vitest:")) && (p.enforce || "order" in p && p.order) === "post"); const lastNuxt = findLastIndex(plugins, (i) => !!i?.name?.startsWith("nuxt:")); if (lastNuxt === -1) return; for (const plugin of vitestPlugins) { const index = plugins.indexOf(plugin); if (index < lastNuxt) { plugins.splice(index, 1); plugins.splice(lastNuxt, 0, plugin); } } } } }; }); function findLastIndex(arr, predicate) { for (let i = arr.length - 1; i >= 0; i--) if (predicate(arr[i])) return i; return -1; } function isImportDeclaration(node) { return node.type === "ImportDeclaration"; } function isImportSpecifier(node) { return node.type === "ImportSpecifier"; } function isCallExpression(node) { return node.type === "CallExpression"; } function isIdentifier(node) { return node.type === "Identifier"; } function isLiteral(node) { return node.type === "Literal"; } function isExpressionStatement(node) { return node?.type === "ExpressionStatement"; } function startOf(node) { return "range" in node && node.range ? node.range[0] : "start" in node ? node.start : void 0; } function endOf(node) { return "range" in node && node.range ? node.range[1] : "end" in node ? node.end : void 0; } function mapGroupBy(items, keySelector) { const map = /* @__PURE__ */ new Map(); for (const item of items) { const key = keySelector(item); if (!map.has(key)) map.set(key, []); map.get(key).push(item); } return map; } //#endregion //#region src/module/mock.ts function isTestPluginFile(src) { return src.includes(".spec.") || src.includes(".test."); } /** * This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`, * which make it possible to mock Nuxt imports. */ async function setupImportMocking(nuxt) { const { addVitePlugin } = await loadKit(nuxt.options.rootDir); const ctx = { components: [], imports: [] }; let importsCtx; nuxt.hook("imports:context", async (ctx) => { importsCtx = ctx; }); nuxt.hook("ready", async () => { ctx.imports = await importsCtx.getImports(); }); nuxt.hook("components:extend", (_) => { ctx.components = _; }); nuxt.hook("imports:sources", (presets) => { const idx = presets.findIndex((p) => typeof p === "object" && "imports" in p && p.imports?.includes("setInterval")); if (idx !== -1) presets.splice(idx, 1); }); nuxt.options.ignore = nuxt.options.ignore.filter((i) => i !== "**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}"); if (nuxt._ignore) for (const pattern of resolveIgnorePatterns("**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}")) nuxt._ignore.add(`!${pattern}`); nuxt.hook("app:resolve", (app) => { app.plugins = app.plugins.filter((plugin) => !isTestPluginFile(plugin.src)); }); addVitePlugin(createMockPlugin(ctx).vite()); } //#endregion //#region src/module/plugins/entry.ts const PLUGIN_NAME = "nuxt:vitest:nuxt-root-stub"; const STUB_ID = "nuxt-vitest-app-entry"; const NuxtRootStubPlugin = (options) => { const extension = extname(options.entry); const escapedExt = extension.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const entryPath = join(dirname(options.entry), STUB_ID + extension); const idFilter = new RegExp(`${STUB_ID}(?:${escapedExt})?$`); return { name: PLUGIN_NAME, enforce: "pre", resolveId: { filter: { id: idFilter }, async handler(id, importer) { return importer?.endsWith("index.html") ? id : entryPath; } }, load: { filter: { id: idFilter }, async handler() { return readFileSync(options.entry, "utf-8").replace("#build/root-component.mjs", options.rootStubPath); } } }; }; //#endregion //#region src/module/install-wizard.ts function generateVitestConfig(answers) { let config = `import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import { defineVitestProject } from '@nuxt/test-utils/config'`; if (answers.browserMode) config += ` import { playwright } from '@vitest/browser-playwright'`; config += ` export default defineConfig({\n`; config += ` test: { projects: [\n`; if (answers.testingScope.includes("unit")) config += ` { test: { name: 'unit', include: ['test/unit/*.{test,spec}.ts'], environment: 'node', }, },\n`; config += ` await defineVitestProject({ test: { name: 'nuxt', include: ['test/nuxt/*.{test,spec}.ts'], environment: 'nuxt', environmentOptions: { nuxt: { rootDir: fileURLToPath(new URL('.', import.meta.url)),${answers.browserMode ? "" : `\n domEnvironment: '${answers.domEnvironment || "happy-dom"}',`} }, },${answers.browserMode ? `\n browser: {\n enabled: true,\n provider: playwright(),\n instances: [\n { browser: 'chromium' },\n ],\n },` : ""} }, }),\n`; if (answers.testingScope.includes("e2e") && answers.e2eRunner === "vitest") config += ` { test: { name: 'e2e', include: ['test/e2e/*.{test,spec}.ts'], environment: 'node', }, },\n`; config += ` ],\n`; if (answers.coverage) config += ` coverage: { enabled: true, provider: 'v8', },\n`; config += ` },\n`; config += `})\n`; return config; } function generatePlaywrightConfig() { return `import { fileURLToPath } from 'node:url' import { defineConfig, devices } from '@playwright/test' import type { ConfigOptions } from '@nuxt/test-utils/playwright' export default defineConfig<ConfigOptions>({ testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { trace: 'on-first-retry', nuxt: { rootDir: fileURLToPath(new URL('.', import.meta.url)), }, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], }) `; } function getDependencies(answers) { const dependencies = []; if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) { dependencies.push("vitest", "@vue/test-utils"); if (answers.domEnvironment) dependencies.push(answers.domEnvironment); if (answers.browserMode) dependencies.push("@vitest/browser-playwright"); } if (answers.e2eRunner === "playwright") dependencies.push("@playwright/test", "playwright-core"); else if (answers.e2eRunner === "cucumber") dependencies.push("@cucumber/cucumber"); else if (answers.e2eRunner === "jest") dependencies.push("@jest/globals"); if (answers.coverage) dependencies.push("@vitest/coverage-v8"); return dependencies; } function getPackageScripts(answers) { const scripts = {}; if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) { scripts.test = "vitest"; scripts["test:watch"] = "vitest --watch"; if (answers.coverage) scripts["test:coverage"] = "vitest --coverage"; if (answers.testingScope.includes("unit")) scripts["test:unit"] = "vitest --project unit"; scripts["test:nuxt"] = "vitest --project nuxt"; if (answers.testingScope.includes("e2e") && answers.e2eRunner === "vitest") scripts["test:e2e"] = "vitest --project e2e"; } if (answers.e2eRunner === "playwright") { scripts["test:e2e"] = "playwright test"; scripts["test:e2e:ui"] = "playwright test --ui"; } return scripts; } async function runInstallWizard(nuxt) { if (isCI || !hasTTY || nuxt.options.test) return; if (nuxt.options.workspaceDir && nuxt.options.workspaceDir !== nuxt.options.rootDir) { logger.info("Monorepo detected. Skipping setup wizard."); return; } const rootDir = nuxt.options.rootDir; const hasVitestConfig = existsSync(join(rootDir, "vitest.config.ts")) || existsSync(join(rootDir, "vitest.config.js")) || existsSync(join(rootDir, "vitest.config.mts")) || existsSync(join(rootDir, "vitest.config.mjs")); const hasPlaywrightConfig = existsSync(join(rootDir, "playwright.config.ts")) || existsSync(join(rootDir, "playwright.config.js")); if (hasVitestConfig || hasPlaywrightConfig) { logger.info("Test configuration already exists. Skipping setup wizard."); return; } intro(colors.bold(colors.cyan("🧪 Nuxt Test Utils Setup"))); const answers = {}; const testingScope = await multiselect({ message: "What kind of tests will you need?", options: [ { value: "runtime", label: "Runtime", hint: "components or composables running in a Nuxt runtime environment" }, { value: "unit", label: "Unit tests", hint: "pure functions or build-time/Node tests" }, { value: "e2e", label: "End-to-end", hint: "full application flows in browser" } ], required: true }); if (isCancel(testingScope)) { cancel("Setup cancelled."); process$1.exit(0); } answers.testingScope = testingScope; const needsVitest = answers.testingScope.includes("unit") || answers.testingScope.includes("runtime"); const needsE2E = answers.testingScope.includes("e2e"); if (answers.testingScope.includes("runtime")) { const domEnvironment = await select({ message: "Which Vitest environment would you like to use for runtime tests?", options: [ { value: "happy-dom", label: "happy-dom", hint: "recommended - faster, lighter" }, { value: "jsdom", label: "jsdom", hint: "more complete browser simulation" }, { value: "browser", label: "browser mode", hint: "real browser with Playwright" } ], initialValue: "happy-dom" }); if (isCancel(domEnvironment)) { cancel("Setup cancelled."); process$1.exit(0); } if (domEnvironment === "browser") answers.browserMode = true; else answers.domEnvironment = domEnvironment; } if (needsE2E) { const e2eRunner = await select({ message: "Which end-to-end test runner would you like to use?", options: [ { value: "playwright", label: "Playwright", hint: "recommended - modern, multi-browser" }, { value: "vitest", label: "Vitest", hint: "same runner as unit tests" }, { value: "cucumber", label: "Cucumber", hint: "behavior-driven development" }, { value: "jest", label: "Jest", hint: "legacy test runner" } ], initialValue: "playwright" }); if (isCancel(e2eRunner)) { cancel("Setup cancelled."); process$1.exit(0); } answers.e2eRunner = e2eRunner; } if (needsVitest) { const coverage = await confirm({ message: "Would you like to set up test coverage?", initialValue: false }); if (isCancel(coverage)) { cancel("Setup cancelled."); process$1.exit(0); } answers.coverage = coverage; } const exampleTests = await confirm({ message: "Create example test files?", initialValue: true }); if (isCancel(exampleTests)) { cancel("Setup cancelled."); process$1.exit(0); } answers.exampleTests = exampleTests; await performSetup(nuxt, answers); outro(colors.green("✨ Test setup complete!")); } async function performSetup(nuxt, answers) { const rootDir = nuxt.options.rootDir; const packageManager = await detectPackageManager(rootDir); logger.info("Installing dependencies..."); const dependencies = getDependencies(answers); if (dependencies.length > 0) try { await addDependency(dependencies, { cwd: rootDir, dev: true, packageManager }); } catch (error) { logger.error("Failed to install dependencies:", error); return; } if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) await createVitestConfig(nuxt, answers); if (answers.e2eRunner === "playwright") await createPlaywrightConfig(nuxt); await createTestDirectories(nuxt, answers); if (answers.exampleTests) await createExampleTests(nuxt, answers); await updatePackageScripts(nuxt, answers); await updateGitignore(nuxt, answers); } async function createVitestConfig(nuxt, answers) { const rootDir = nuxt.options.rootDir; const configPath = join(rootDir, "vitest.config.ts"); const config = generateVitestConfig(answers); await promises.writeFile(configPath, config, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), configPath))}`); } async function createPlaywrightConfig(nuxt) { const rootDir = nuxt.options.rootDir; const configPath = join(rootDir, "playwright.config.ts"); const config = generatePlaywrightConfig(); await promises.writeFile(configPath, config, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), configPath))}`); } async function createTestDirectories(nuxt, answers) { const rootDir = nuxt.options.rootDir; if (answers.testingScope.includes("unit")) { const unitDir = join(rootDir, "test/unit"); await promises.mkdir(unitDir, { recursive: true }); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), unitDir))}`); } if (answers.testingScope.includes("runtime")) { const nuxtDir = join(rootDir, "test/nuxt"); await promises.mkdir(nuxtDir, { recursive: true }); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), nuxtDir))}`); } if (answers.testingScope.includes("e2e")) { const e2eDir = answers.e2eRunner === "playwright" ? join(rootDir, "tests") : join(rootDir, "test/e2e"); await promises.mkdir(e2eDir, { recursive: true }); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eDir))}`); } } async function createExampleTests(nuxt, answers) { const rootDir = nuxt.options.rootDir; if (answers.testingScope.includes("unit")) { const unitTestPath = join(rootDir, "test/unit/example.test.ts"); await promises.writeFile(unitTestPath, `import { describe, expect, it } from 'vitest' describe('example unit test', () => { it('should pass', () => { expect(1 + 1).toBe(2) }) }) `, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), unitTestPath))}`); } if (answers.testingScope.includes("runtime")) { const componentTestPath = join(rootDir, "test/nuxt/component.test.ts"); await promises.writeFile(componentTestPath, `import { describe, expect, it } from 'vitest' import { mountSuspended } from '@nuxt/test-utils/runtime' import { defineComponent, h } from 'vue' describe('component test example', () => { it('can mount components', async () => { const TestComponent = defineComponent({ setup() { return () => h('div', 'Hello Nuxt!') }, }) const component = await mountSuspended(TestComponent) expect(component.text()).toBe('Hello Nuxt!') }) }) `, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), componentTestPath))}`); } if (answers.testingScope.includes("e2e")) { if (answers.e2eRunner === "playwright") { const e2eTestPath = join(rootDir, "tests/example.spec.ts"); await promises.writeFile(e2eTestPath, `import { expect, test } from '@nuxt/test-utils/playwright' test('example e2e test', async ({ page, goto }) => { await goto('/', { waitUntil: 'hydration' }) await expect(page).toHaveTitle(/Nuxt/) }) `, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eTestPath))}`); } else { const e2eTestPath = join(rootDir, "test/e2e/example.test.ts"); await promises.writeFile(e2eTestPath, `import { describe, expect, it } from 'vitest' import { $fetch, setup } from '@nuxt/test-utils/e2e' describe('example e2e test', async () => { await setup() it('renders the index page', async () => { const html = await $fetch('/') expect(html).toContain('Nuxt') }) }) `, "utf-8"); logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eTestPath))}`); } } } async function updatePackageScripts(nuxt, answers) { const rootDir = nuxt.options.rootDir; const packageJsonPath = join(rootDir, "package.json"); const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf-8")); packageJson.scripts = packageJson.scripts || {}; const newScripts = getPackageScripts(answers); Object.assign(packageJson.scripts, newScripts); await promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8"); logger.success("Updated package.json scripts"); } async function updateGitignore(nuxt, answers) { const rootDir = nuxt.options.rootDir; const gitignorePath = join(rootDir, ".gitignore"); let gitignore = ""; if (existsSync(gitignorePath)) gitignore = await promises.readFile(gitignorePath, "utf-8"); const lines = []; if (answers.coverage && !gitignore.includes("coverage")) lines.push("# Test coverage", "coverage/", ""); if (answers.e2eRunner === "playwright") { if (!gitignore.includes("playwright-report")) lines.push("# Playwright", "playwright-report/", "test-results/", ""); } if (lines.length > 0) { gitignore += "\n" + lines.join("\n"); await promises.writeFile(gitignorePath, gitignore, "utf-8"); logger.success("Updated .gitignore"); } } //#endregion //#region src/devtools.ts async function setupDevTools(vitestWrapper, nuxt = useNuxt()) { const iframeSrc = "/__test_utils_vitest__/"; const updateTabs = debounce(() => { refreshCustomTabs(nuxt); }, 100); addCustomTab(() => createVitestCustomTab(vitestWrapper, { iframeSrc }), nuxt); addDevServerHandler({ route: iframeSrc, handler: Object.assign(() => iframeContentHtml(vitestWrapper.uiUrl), { __is_handler__: true }) }); vitestWrapper.ons({ started() { updateTabs(); }, updated() { updateTabs(); }, finished() { updateTabs(); }, exited() { updateTabs(); } }); } function createVitestCustomTab(vitest, { iframeSrc }) { const launchView = { type: "launch", description: "Start tests along with Nuxt", actions: [{ get label() { switch (vitest.status) { case "starting": return "Starting..."; case "running": return "Running Vitest"; case "stopped": return "Start Vitest"; case "finished": return "Start Vitest"; } }, get pending() { return vitest.status === "starting" || vitest.status === "running"; }, handle: () => { vitest.start(); } }] }; const uiView = { type: "iframe", persistent: false, src: iframeSrc }; return { title: "Vitest", name: "vitest", icon: "logos-vitest", get view() { if (vitest.status === "stopped" || vitest.status === "starting" || !vitest.uiUrl) return launchView; else return uiView; }, extraTabVNode: vitest.testSummary.totalCount ? h("div", { style: { color: vitest.testSummary.failedCount ? "orange" : "green" } }, [ h("span", {}, vitest.testSummary.passedCount), h("span", { style: { opacity: "0.5", fontSize: "0.9em" } }, "/"), h("span", { style: { opacity: "0.8", fontSize: "0.9em" } }, vitest.testSummary.totalCount) ]) : void 0 }; } function iframeContentHtml(uiUrl) { return [ "<html><head><script>", `(${function redirect(uiUrl, provider) { if (typeof window === "undefined") return; if (!uiUrl) return; if (provider === "stackblitz") { const url = new URL(window.location.href); const newUrl = new URL(uiUrl); newUrl.host = url.host.replace(/--\d+--/, `--${newUrl.port}--`); newUrl.protocol = url.protocol; newUrl.port = url.port; uiUrl = newUrl.toString(); } window.location.replace(uiUrl); }})(${JSON.stringify(uiUrl)}, ${JSON.stringify(provider)})`, "<\/script></head></html>" ].join("\n"); } //#endregion //#region src/vitest-wrapper/host.ts function vitestWrapper(options) { const { cwd, ...startOptions } = options; let _status = "stopped"; let _uiUrl; let _process; let _testSummary = createVitestTestSummary(); const _handlers = { started: [({ uiUrl }) => { _uiUrl = uiUrl; _status = "running"; _testSummary = createVitestTestSummary(); }], updated: [(summary) => { _testSummary = summary; }], finished: [(summary) => { _status = "finished"; _testSummary = summary; }], exited: [clear] }; function clear() { _status = "stopped"; _uiUrl = void 0; _process = void 0; _testSummary = createVitestTestSummary(); } function on(name, handler) { _handlers[name] ??= []; _handlers[name]?.push(handler); } function ons(handlers) { for (const [name, handler] of Object.entries(handlers)) if (typeof handler === "function") on(name, handler); } async function stop() { const vitest = _process; if (!vitest || vitest.exitCode !== null) return; return new Promise((resolve) => { vitest.once("exit", () => resolve()); sendMessageToCli(vitest, "stop", { force: true }); }); } async function start() { if (_process) return false; const vitest = fork(resolve(distDir, "./vitest-wrapper/cli.mjs"), { cwd, env: { ...process.env, NODE_ENV: "test", MODE: "test" }, stdio: startOptions.logToConsole ? void 0 : [ "ignore", "ignore", "inherit", "ipc" ] }); _status = "starting"; _process = vitest; vitest.once("exit", () => { _handlers.exited.forEach((fn) => fn({ exitCode: vitest.exitCode ?? 0 })); }); listenCliMessages(vitest, ({ type, payload }) => { _handlers[type].forEach((fn) => fn(payload)); }); sendMessageToCli(vitest, "start", startOptions); return true; } return { on, ons, stop, start, get uiUrl() { return _uiUrl; }, get options() { return options; }, get status() { return _status; }, get testSummary() { return { ..._testSummary }; } }; } //#endregion //#region src/module.ts var module_default = defineNuxtModule({ meta: { name: "@nuxt/test-utils", configKey: "testUtils", version: "4.3.2" }, defaults: { startOnBoot: false, logToConsole: false }, async onInstall(nuxt) { await runInstallWizard(nuxt); }, async setup(options, nuxt) { if (nuxt.options.test || nuxt.options.dev) await setupImportMocking(nuxt); if (nuxt.options.test && !nuxt.options.dev) nuxt.hook("app:templates", (app) => { const template = app.templates.find((t) => t.filename === "paths.mjs"); if (!template?.getContents) return; const original = template.getContents; const inlineAppConfig = JSON.stringify(nuxt.options.app); template.getContents = async (data) => { return (await original(data)).replace(/^import \{ useRuntimeConfig \} from ['"]nitropack\/runtime['"]\n?/m, "").replace(/const getAppConfig = \(\) => useRuntimeConfig\(\)\.app/, () => `const getAppConfig = () => (${inlineAppConfig})`); }; }); const { addVitePlugin } = await loadKit(nuxt.options.rootDir); const resolver = createResolver(import.meta.url); if (nuxt.options.test || nuxt.options.dev) addVitePlugin(NuxtRootStubPlugin({ entry: await resolvePath("#app/entry", { alias: nuxt.options.alias }), rootStubPath: await resolvePath(resolver.resolve("./runtime/nuxt-root")) })); if (!nuxt.options.test && !nuxt.options.dev) { nuxt.options.vite.define ||= {}; nuxt.options.vite.define["import.meta.vitest"] = "undefined"; } nuxt.hook("prepare:types", (ctx) => { ctx.references.push({ types: "vitest/import-meta" }); for (const tsConfig of [ ctx.tsConfig, ctx.nodeTsConfig, ctx.sharedTsConfig ]) { if (!tsConfig) continue; tsConfig.compilerOptions ||= {}; tsConfig.compilerOptions.allowImportingTsExtensions = true; } if (ctx.nodeTsConfig) { ctx.nodeTsConfig.include ||= []; ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.rootDir, "vitest.config.*"))); if (nuxt.options.workspaceDir !== nuxt.options.rootDir) ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.workspaceDir, "vitest.config.*"))); } }); if (!nuxt.options.dev) return; if (process.env.TEST || process.env.VITE_TEST) return; const vitestWrapper = createVitestWrapper(options, nuxt); onDevToolsInitialized(async () => { await setupDevTools(vitestWrapper, nuxt); }, nuxt); if (options.startOnBoot) vitestWrapper.start(); } }); function createVitestWrapper(options, nuxt = useNuxt()) { const watchMode = !isCI; const wrapper = vitestWrapper({ cwd: nuxt.options.rootDir, apiPorts: [15555], logToConsole: options.logToConsole ?? false, watchMode }); wrapper.ons({ started({ uiUrl }) { if (watchMode) logger.info(`Vitest UI starting on ${uiUrl}`); }, exited({ exitCode }) { if (watchMode) logger.info(`Vitest exited with code ${exitCode}`); else nuxt.close().finally(() => process.exit(exitCode)); } }); nuxt.hooks.addHooks({ close: () => wrapper.stop(), restart: () => wrapper.stop() }); return wrapper; } //#endregion export { module_default as default };