@nuxt/test-utils
Version:
Test utilities for Nuxt
1,048 lines (1,029 loc) • 33.3 kB
JavaScript
import { resolveIgnorePatterns, logger, useNuxt, addDevServerHandler, defineNuxtModule, createResolver, resolvePath } from '@nuxt/kit';
import { extname, join, dirname, relative, resolve } from 'pathe';
import { isCI, hasTTY, provider } from 'std-env';
import { walk } from 'estree-walker';
import MagicString from 'magic-string';
import { createUnplugin } from 'unplugin';
import { l as loadKit } from './shared/test-utils.BIY9XRkB.mjs';
import { readFileSync, existsSync, promises } from 'node:fs';
import process$1 from 'node:process';
import { intro, multiselect, isCancel, cancel, select, confirm, outro } from '@clack/prompts';
import { colors } from 'consola/utils';
import { detectPackageManager, addDependency } from 'nypm';
import { h } from 'vue';
import { debounce } from 'perfect-debounce';
import { fork } from 'node:child_process';
import { c as createVitestTestSummary, l as listenCliMessages, s as sendMessageToCli } from './shared/test-utils.DDUpsMYL.mjs';
import { distDir } from '#dirs';
import 'destr';
import 'scule';
import 'node:url';
import 'exsolve';
const PLUGIN_NAME$1 = "nuxt:vitest:mock-transform";
const HELPER_MOCK_IMPORT = "mockNuxtImport";
const HELPER_MOCK_COMPONENT = "mockComponent";
const HELPER_MOCK_HOIST = "__NUXT_VITEST_MOCKS";
const HELPERS_NAME = [HELPER_MOCK_IMPORT, HELPER_MOCK_COMPONENT];
const createMockPlugin = (ctx) => createUnplugin(() => {
function 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, {
// @ts-expect-error compatibility with rollup v3
sourceType: "module",
ecmaVersion: "latest",
ranges: true
});
} catch {
return;
}
let insertionPoint = 0;
let hasViImport = false;
const s = new MagicString(code);
const mocksImport = [];
const mocksComponent = [];
const importPathsList = /* @__PURE__ */ new Set();
walk(ast, {
enter: (node, parent) => {
if (isImportDeclaration(node)) {
if (node.source.value === "vitest" && !hasViImport) {
const viImport = node.specifiers.find(
(i) => isImportSpecifier(i) && i.imported.type === "Identifier" && i.imported.name === "vi"
);
if (viImport) {
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(
new Error(
`${HELPER_MOCK_IMPORT}() should have exactly 2 arguments`
),
startOf(node)
);
}
const importTarget = node.arguments[0];
const name = isLiteral(importTarget) ? importTarget.value : isIdentifier(importTarget) ? importTarget.name : void 0;
if (typeof name !== "string") {
return this.error(
new Error(
`The first argument of ${HELPER_MOCK_IMPORT}() must be a string literal or mocked target`
),
startOf(importTarget)
);
}
const importItem = ctx.imports.find((_) => name === (_.as || _.name));
if (!importItem) {
return this.error(`Cannot find import "${name}" to mock`);
}
s.overwrite(
isExpressionStatement(parent) ? startOf(parent) : startOf(node.arguments[0]),
isExpressionStatement(parent) ? endOf(parent) : endOf(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_MOCK_COMPONENT) {
if (node.arguments.length !== 2) {
return this.error(
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(
new Error(
`The first argument of ${HELPER_MOCK_COMPONENT}() must be a string literal`
),
startOf(componentName)
);
}
const pathOrName = componentName.value;
const component = ctx.components.find(
(_) => _.pascalName === pathOrName || _.kebabName === pathOrName
);
const path = component?.filePath || pathOrName;
s.overwrite(
isExpressionStatement(parent) ? startOf(parent) : startOf(node.arguments[1]),
isExpressionStatement(parent) ? endOf(parent) : endOf(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 = [];
if (mocksImport.length) {
const mockImportMap = /* @__PURE__ */ new Map();
for (const mock of mocksImport) {
if (!mockImportMap.has(mock.import.from)) {
mockImportMap.set(mock.import.from, []);
}
mockImportMap.get(mock.import.from).push(mock);
}
mockLines.push(
...Array.from(mockImportMap.entries()).flatMap(
([from, mocks]) => {
importPathsList.add(from);
const lines = [
`vi.mock(${JSON.stringify(from)}, async (importOriginal) => {`,
` const mocks = globalThis.${HELPER_MOCK_HOIST}`,
` if (!mocks[${JSON.stringify(from)}]) {`,
` mocks[${JSON.stringify(from)}] = { ...await importOriginal(${JSON.stringify(from)}) }`,
` }`
];
for (const mock of mocks) {
if (mock.import.name === "default") {
lines.push(
` mocks[${JSON.stringify(from)}]["default"] = await (${mock.factory})();`
);
} else {
lines.push(
` mocks[${JSON.stringify(from)}][${JSON.stringify(mock.name)}] = await (${mock.factory})();`
);
}
}
lines.push(` return mocks[${JSON.stringify(from)}] `);
lines.push(`});`);
return lines;
}
)
);
}
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)}, {})
}
});
`);
if (!hasViImport) s.prepend(`import {vi} from "vitest";
`);
s.appendLeft(insertionPoint, "\n" + mockLines.join("\n") + "\n");
importPathsList.forEach((p) => {
s.append(`
import ${JSON.stringify(p)};`);
});
return {
code: s.toString(),
map: s.generateMap()
};
}
return {
name: PLUGIN_NAME$1,
enforce: "post",
vite: {
transform,
// Place Vitest's mock plugin after all Nuxt plugins
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;
}
async function setupImportMocking(nuxt) {
const { addVitePlugin } = await loadKit(nuxt.options.rootDir);
const ctx = {
components: [],
imports: []
};
let importsCtx;
nuxt.hook("imports:context", async (ctx2) => {
importsCtx = ctx2;
});
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) => 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}`);
}
}
addVitePlugin(createMockPlugin(ctx).vite());
}
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() {
const entryContents = readFileSync(options.entry, "utf-8");
return entryContents.replace("#build/root-component.mjs", options.rootStubPath);
}
}
};
};
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({
`;
config += ` test: {
projects: [
`;
if (answers.testingScope.includes("unit")) {
config += ` {
test: {
name: 'unit',
include: ['test/unit/*.{test,spec}.ts'],
environment: 'node',
},
},
`;
}
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 ? "" : `
domEnvironment: '${answers.domEnvironment || "happy-dom"}',`}
},
},${answers.browserMode ? `
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},` : ""}
},
}),
`;
if (answers.testingScope.includes("e2e") && answers.e2eRunner === "vitest") {
config += ` {
test: {
name: 'e2e',
include: ['test/e2e/*.{test,spec}.ts'],
environment: 'node',
},
},
`;
}
config += ` ],
`;
if (answers.coverage) {
config += ` coverage: {
enabled: true,
provider: 'v8',
},
`;
}
config += ` },
`;
config += `})
`;
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("\u{1F9EA} 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("\u2728 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");
const unitTest = `import { describe, expect, it } from 'vitest'
describe('example unit test', () => {
it('should pass', () => {
expect(1 + 1).toBe(2)
})
})
`;
await promises.writeFile(unitTestPath, unitTest, "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");
const componentTest = `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!')
})
})
`;
await promises.writeFile(componentTestPath, componentTest, "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");
const e2eTest = `import { expect, test } from '@nuxt/test-utils/playwright'
test('example e2e test', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page).toHaveTitle(/Nuxt/)
})
`;
await promises.writeFile(e2eTestPath, e2eTest, "utf-8");
logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eTestPath))}`);
} else {
const e2eTestPath = join(rootDir, "test/e2e/example.test.ts");
const e2eTest = `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')
})
})
`;
await promises.writeFile(e2eTestPath, e2eTest, "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");
}
}
async function setupDevTools(vitestWrapper, nuxt = useNuxt()) {
const iframeSrc = "/__test_utils_vitest__/";
const updateTabs = debounce(() => {
nuxt.callHook("devtools:customTabs:refresh");
}, 100);
nuxt.hook("devtools:customTabs", (tabs) => {
const tab = createVitestCustomTab(vitestWrapper, { iframeSrc });
const index = tabs.findIndex(({ name }) => tab.name === name);
if (index === -1) {
tabs.push(tab);
} else {
tabs.splice(index, 1, tab);
}
});
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
};
const tab = {
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
};
return tab;
}
function iframeContentHtml(uiUrl) {
return [
"<html><head><script>",
`(${function redirect(uiUrl2, provider2) {
if (typeof window === "undefined") return;
if (!uiUrl2) return;
if (provider2 === "stackblitz") {
const url = new URL(window.location.href);
const newUrl = new URL(uiUrl2);
newUrl.host = url.host.replace(/--\d+--/, `--${newUrl.port}--`);
newUrl.protocol = url.protocol;
newUrl.port = url.port;
uiUrl2 = newUrl.toString();
}
window.location.replace(uiUrl2);
}})(${JSON.stringify(uiUrl)}, ${JSON.stringify(provider)})`,
"<\/script></head></html>"
].join("\n");
}
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((resolve2) => {
vitest.once("exit", () => resolve2());
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 };
}
};
}
const version = "3.23.0";
const pkg = {
version: version};
const module$1 = defineNuxtModule({
meta: {
name: "@nuxt/test-utils",
configKey: "testUtils",
version: pkg.version
},
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);
}
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" });
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 vitestWrapper2 = createVitestWrapper(options, nuxt);
nuxt.hook("devtools:before", async () => {
await setupDevTools(vitestWrapper2, nuxt);
});
if (options.startOnBoot) {
vitestWrapper2.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;
}
export { module$1 as default };