@nuxt/test-utils
Version:
Test utilities for Nuxt
921 lines (911 loc) • 32.8 kB
JavaScript
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 };