@nuxt/test-utils 3.21.0 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/config.d.mts +3 -3
  3. package/dist/config.mjs +40 -21
  4. package/dist/e2e.d.mts +3 -2
  5. package/dist/e2e.mjs +17 -13
  6. package/dist/experimental.mjs +1 -1
  7. package/dist/module.mjs +721 -148
  8. package/dist/playwright.d.mts +2 -1
  9. package/dist/playwright.mjs +3 -3
  10. package/dist/runtime/shared/environment.mjs +11 -66
  11. package/dist/runtime/shared/h3-v1.d.ts +6 -0
  12. package/dist/runtime/shared/h3-v1.mjs +69 -0
  13. package/dist/runtime/shared/h3-v2.d.ts +6 -0
  14. package/dist/runtime/shared/h3-v2.mjs +35 -0
  15. package/dist/runtime/shared/h3.d.ts +3 -0
  16. package/dist/runtime/shared/h3.mjs +3 -0
  17. package/dist/runtime/shared/nuxt.d.ts +1 -1
  18. package/dist/runtime/shared/nuxt.mjs +10 -2
  19. package/dist/runtime/shared/vue-wrapper-plugin.d.ts +50 -0
  20. package/dist/runtime/shared/vue-wrapper-plugin.mjs +41 -0
  21. package/dist/runtime-utils/index.d.mts +26 -25
  22. package/dist/runtime-utils/index.mjs +185 -272
  23. package/dist/shared/{test-utils.3NR-so9-.mjs → test-utils.5cnw0YZR.mjs} +2 -2
  24. package/dist/shared/{test-utils.G1ew4kEe.mjs → test-utils.BIY9XRkB.mjs} +1 -1
  25. package/dist/shared/{test-utils.CtwoJP76.mjs → test-utils.BsmyE2FA.mjs} +10 -8
  26. package/dist/shared/{test-utils.20kU0tZa.d.mts → test-utils.C9GKP_T5.d.mts} +3 -2
  27. package/dist/shared/test-utils.DDUpsMYL.mjs +32 -0
  28. package/dist/vitest-environment.d.mts +15 -3
  29. package/dist/vitest-environment.mjs +118 -66
  30. package/dist/vitest-wrapper/cli.d.mts +2 -0
  31. package/dist/vitest-wrapper/cli.mjs +78 -0
  32. package/package.json +32 -26
package/dist/module.mjs CHANGED
@@ -1,22 +1,23 @@
1
- import { pathToFileURL } from 'node:url';
2
- import { resolveIgnorePatterns, defineNuxtModule, createResolver, resolvePath, importModule, logger } from '@nuxt/kit';
3
- import { getPort } from 'get-port-please';
4
- import { h } from 'vue';
5
- import { debounce } from 'perfect-debounce';
6
- import { isCI } from 'std-env';
7
- import { defu } from 'defu';
8
- import { extname, join, dirname, relative } from 'pathe';
9
- import { getVitestConfigFromNuxt } from './config.mjs';
1
+ import { resolveIgnorePatterns, logger, useNuxt, addDevServerHandler, defineNuxtModule, createResolver, resolvePath } from '@nuxt/kit';
2
+ import { extname, join, dirname, relative, resolve } from 'pathe';
3
+ import { isCI, hasTTY, provider } from 'std-env';
10
4
  import { walk } from 'estree-walker';
11
5
  import MagicString from 'magic-string';
12
6
  import { createUnplugin } from 'unplugin';
13
- import { l as loadKit } from './shared/test-utils.G1ew4kEe.mjs';
14
- import { readFileSync } from 'node:fs';
15
- import 'node:process';
16
- import 'vite';
17
- import 'c12';
7
+ import { l as loadKit } from './shared/test-utils.BIY9XRkB.mjs';
8
+ import { readFileSync, existsSync, promises } from 'node:fs';
9
+ import process$1 from 'node:process';
10
+ import { intro, multiselect, isCancel, cancel, select, confirm, outro } from '@clack/prompts';
11
+ import { colors } from 'consola/utils';
12
+ import { detectPackageManager, addDependency } from 'nypm';
13
+ import { h } from 'vue';
14
+ import { debounce } from 'perfect-debounce';
15
+ import { fork } from 'node:child_process';
16
+ import { c as createVitestTestSummary, l as listenCliMessages, s as sendMessageToCli } from './shared/test-utils.DDUpsMYL.mjs';
17
+ import { distDir } from '#dirs';
18
18
  import 'destr';
19
19
  import 'scule';
20
+ import 'node:url';
20
21
  import 'exsolve';
21
22
 
22
23
  const PLUGIN_NAME$1 = "nuxt:vitest:mock-transform";
@@ -81,7 +82,6 @@ const createMockPlugin = (ctx) => createUnplugin(() => {
81
82
  }
82
83
  const importItem = ctx.imports.find((_) => name === (_.as || _.name));
83
84
  if (!importItem) {
84
- console.log({ imports: ctx.imports });
85
85
  return this.error(`Cannot find import "${name}" to mock`);
86
86
  }
87
87
  s.overwrite(
@@ -297,178 +297,751 @@ async function setupImportMocking(nuxt) {
297
297
 
298
298
  const PLUGIN_NAME = "nuxt:vitest:nuxt-root-stub";
299
299
  const STUB_ID = "nuxt-vitest-app-entry";
300
- const NuxtRootStubPlugin = createUnplugin((options) => {
301
- const STUB_ID_WITH_EXT = STUB_ID + extname(options.entry);
300
+ const NuxtRootStubPlugin = (options) => {
301
+ const extension = extname(options.entry);
302
+ const escapedExt = extension.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303
+ const entryPath = join(dirname(options.entry), STUB_ID + extension);
304
+ const idFilter = new RegExp(`${STUB_ID}(?:${escapedExt})?$`);
302
305
  return {
303
306
  name: PLUGIN_NAME,
304
307
  enforce: "pre",
305
- vite: {
306
- async resolveId(id, importer) {
307
- if (id.endsWith(STUB_ID) || id.endsWith(STUB_ID_WITH_EXT)) {
308
- return importer?.endsWith("index.html") ? id : join(dirname(options.entry), STUB_ID_WITH_EXT);
308
+ resolveId: {
309
+ filter: {
310
+ id: idFilter
311
+ },
312
+ async handler(id, importer) {
313
+ return importer?.endsWith("index.html") ? id : entryPath;
314
+ }
315
+ },
316
+ load: {
317
+ filter: {
318
+ id: idFilter
319
+ },
320
+ async handler() {
321
+ const entryContents = readFileSync(options.entry, "utf-8");
322
+ return entryContents.replace("#build/root-component.mjs", options.rootStubPath);
323
+ }
324
+ }
325
+ };
326
+ };
327
+
328
+ function generateVitestConfig(answers) {
329
+ let config = `import { fileURLToPath } from 'node:url'
330
+ import { defineConfig } from 'vitest/config'
331
+ import { defineVitestProject } from '@nuxt/test-utils/config'`;
332
+ if (answers.browserMode) {
333
+ config += `
334
+ import { playwright } from '@vitest/browser-playwright'`;
335
+ }
336
+ config += `
337
+
338
+ export default defineConfig({
339
+ `;
340
+ config += ` test: {
341
+ projects: [
342
+ `;
343
+ if (answers.testingScope.includes("unit")) {
344
+ config += ` {
345
+ test: {
346
+ name: 'unit',
347
+ include: ['test/unit/*.{test,spec}.ts'],
348
+ environment: 'node',
349
+ },
350
+ },
351
+ `;
352
+ }
353
+ config += ` await defineVitestProject({
354
+ test: {
355
+ name: 'nuxt',
356
+ include: ['test/nuxt/*.{test,spec}.ts'],
357
+ environment: 'nuxt',
358
+ environmentOptions: {
359
+ nuxt: {
360
+ rootDir: fileURLToPath(new URL('.', import.meta.url)),${answers.browserMode ? "" : `
361
+ domEnvironment: '${answers.domEnvironment || "happy-dom"}',`}
362
+ },
363
+ },${answers.browserMode ? `
364
+ browser: {
365
+ enabled: true,
366
+ provider: playwright(),
367
+ instances: [
368
+ { browser: 'chromium' },
369
+ ],
370
+ },` : ""}
371
+ },
372
+ }),
373
+ `;
374
+ if (answers.testingScope.includes("e2e") && answers.e2eRunner === "vitest") {
375
+ config += ` {
376
+ test: {
377
+ name: 'e2e',
378
+ include: ['test/e2e/*.{test,spec}.ts'],
379
+ environment: 'node',
380
+ },
381
+ },
382
+ `;
383
+ }
384
+ config += ` ],
385
+ `;
386
+ if (answers.coverage) {
387
+ config += ` coverage: {
388
+ enabled: true,
389
+ provider: 'v8',
390
+ },
391
+ `;
392
+ }
393
+ config += ` },
394
+ `;
395
+ config += `})
396
+ `;
397
+ return config;
398
+ }
399
+ function generatePlaywrightConfig() {
400
+ return `import { fileURLToPath } from 'node:url'
401
+ import { defineConfig, devices } from '@playwright/test'
402
+ import type { ConfigOptions } from '@nuxt/test-utils/playwright'
403
+
404
+ export default defineConfig<ConfigOptions>({
405
+ testDir: './tests',
406
+ fullyParallel: true,
407
+ forbidOnly: !!process.env.CI,
408
+ retries: process.env.CI ? 2 : 0,
409
+ workers: process.env.CI ? 1 : undefined,
410
+ reporter: 'html',
411
+ use: {
412
+ trace: 'on-first-retry',
413
+ nuxt: {
414
+ rootDir: fileURLToPath(new URL('.', import.meta.url)),
415
+ },
416
+ },
417
+ projects: [
418
+ {
419
+ name: 'chromium',
420
+ use: { ...devices['Desktop Chrome'] },
421
+ },
422
+ ],
423
+ })
424
+ `;
425
+ }
426
+ function getDependencies(answers) {
427
+ const dependencies = [];
428
+ if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) {
429
+ dependencies.push("vitest", "@vue/test-utils");
430
+ if (answers.domEnvironment) {
431
+ dependencies.push(answers.domEnvironment);
432
+ }
433
+ if (answers.browserMode) {
434
+ dependencies.push("@vitest/browser-playwright");
435
+ }
436
+ }
437
+ if (answers.e2eRunner === "playwright") {
438
+ dependencies.push("@playwright/test", "playwright-core");
439
+ } else if (answers.e2eRunner === "cucumber") {
440
+ dependencies.push("@cucumber/cucumber");
441
+ } else if (answers.e2eRunner === "jest") {
442
+ dependencies.push("@jest/globals");
443
+ }
444
+ if (answers.coverage) {
445
+ dependencies.push("@vitest/coverage-v8");
446
+ }
447
+ return dependencies;
448
+ }
449
+ function getPackageScripts(answers) {
450
+ const scripts = {};
451
+ if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) {
452
+ scripts.test = "vitest";
453
+ scripts["test:watch"] = "vitest --watch";
454
+ if (answers.coverage) {
455
+ scripts["test:coverage"] = "vitest --coverage";
456
+ }
457
+ if (answers.testingScope.includes("unit")) {
458
+ scripts["test:unit"] = "vitest --project unit";
459
+ }
460
+ scripts["test:nuxt"] = "vitest --project nuxt";
461
+ if (answers.testingScope.includes("e2e") && answers.e2eRunner === "vitest") {
462
+ scripts["test:e2e"] = "vitest --project e2e";
463
+ }
464
+ }
465
+ if (answers.e2eRunner === "playwright") {
466
+ scripts["test:e2e"] = "playwright test";
467
+ scripts["test:e2e:ui"] = "playwright test --ui";
468
+ }
469
+ return scripts;
470
+ }
471
+ async function runInstallWizard(nuxt) {
472
+ if (isCI || !hasTTY || nuxt.options.test) {
473
+ return;
474
+ }
475
+ if (nuxt.options.workspaceDir && nuxt.options.workspaceDir !== nuxt.options.rootDir) {
476
+ logger.info("Monorepo detected. Skipping setup wizard.");
477
+ return;
478
+ }
479
+ const rootDir = nuxt.options.rootDir;
480
+ 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"));
481
+ const hasPlaywrightConfig = existsSync(join(rootDir, "playwright.config.ts")) || existsSync(join(rootDir, "playwright.config.js"));
482
+ if (hasVitestConfig || hasPlaywrightConfig) {
483
+ logger.info("Test configuration already exists. Skipping setup wizard.");
484
+ return;
485
+ }
486
+ intro(colors.bold(colors.cyan("\u{1F9EA} Nuxt Test Utils Setup")));
487
+ const answers = {};
488
+ const testingScope = await multiselect({
489
+ message: "What kind of tests will you need?",
490
+ options: [
491
+ {
492
+ value: "runtime",
493
+ label: "Runtime",
494
+ hint: "components or composables running in a Nuxt runtime environment"
495
+ },
496
+ {
497
+ value: "unit",
498
+ label: "Unit tests",
499
+ hint: "pure functions or build-time/Node tests"
500
+ },
501
+ {
502
+ value: "e2e",
503
+ label: "End-to-end",
504
+ hint: "full application flows in browser"
505
+ }
506
+ ],
507
+ required: true
508
+ });
509
+ if (isCancel(testingScope)) {
510
+ cancel("Setup cancelled.");
511
+ process$1.exit(0);
512
+ }
513
+ answers.testingScope = testingScope;
514
+ const needsVitest = answers.testingScope.includes("unit") || answers.testingScope.includes("runtime");
515
+ const needsE2E = answers.testingScope.includes("e2e");
516
+ if (answers.testingScope.includes("runtime")) {
517
+ const domEnvironment = await select({
518
+ message: "Which Vitest environment would you like to use for runtime tests?",
519
+ options: [
520
+ {
521
+ value: "happy-dom",
522
+ label: "happy-dom",
523
+ hint: "recommended - faster, lighter"
524
+ },
525
+ {
526
+ value: "jsdom",
527
+ label: "jsdom",
528
+ hint: "more complete browser simulation"
529
+ },
530
+ {
531
+ value: "browser",
532
+ label: "browser mode",
533
+ hint: "real browser with Playwright"
534
+ }
535
+ ],
536
+ initialValue: "happy-dom"
537
+ });
538
+ if (isCancel(domEnvironment)) {
539
+ cancel("Setup cancelled.");
540
+ process$1.exit(0);
541
+ }
542
+ if (domEnvironment === "browser") {
543
+ answers.browserMode = true;
544
+ } else {
545
+ answers.domEnvironment = domEnvironment;
546
+ }
547
+ }
548
+ if (needsE2E) {
549
+ const e2eRunner = await select({
550
+ message: "Which end-to-end test runner would you like to use?",
551
+ options: [
552
+ {
553
+ value: "playwright",
554
+ label: "Playwright",
555
+ hint: "recommended - modern, multi-browser"
556
+ },
557
+ {
558
+ value: "vitest",
559
+ label: "Vitest",
560
+ hint: "same runner as unit tests"
561
+ },
562
+ {
563
+ value: "cucumber",
564
+ label: "Cucumber",
565
+ hint: "behavior-driven development"
566
+ },
567
+ {
568
+ value: "jest",
569
+ label: "Jest",
570
+ hint: "legacy test runner"
309
571
  }
572
+ ],
573
+ initialValue: "playwright"
574
+ });
575
+ if (isCancel(e2eRunner)) {
576
+ cancel("Setup cancelled.");
577
+ process$1.exit(0);
578
+ }
579
+ answers.e2eRunner = e2eRunner;
580
+ }
581
+ if (needsVitest) {
582
+ const coverage = await confirm({
583
+ message: "Would you like to set up test coverage?",
584
+ initialValue: false
585
+ });
586
+ if (isCancel(coverage)) {
587
+ cancel("Setup cancelled.");
588
+ process$1.exit(0);
589
+ }
590
+ answers.coverage = coverage;
591
+ }
592
+ const exampleTests = await confirm({
593
+ message: "Create example test files?",
594
+ initialValue: true
595
+ });
596
+ if (isCancel(exampleTests)) {
597
+ cancel("Setup cancelled.");
598
+ process$1.exit(0);
599
+ }
600
+ answers.exampleTests = exampleTests;
601
+ await performSetup(nuxt, answers);
602
+ outro(colors.green("\u2728 Test setup complete!"));
603
+ }
604
+ async function performSetup(nuxt, answers) {
605
+ const rootDir = nuxt.options.rootDir;
606
+ const packageManager = await detectPackageManager(rootDir);
607
+ logger.info("Installing dependencies...");
608
+ const dependencies = getDependencies(answers);
609
+ if (dependencies.length > 0) {
610
+ try {
611
+ await addDependency(dependencies, {
612
+ cwd: rootDir,
613
+ dev: true,
614
+ packageManager
615
+ });
616
+ } catch (error) {
617
+ logger.error("Failed to install dependencies:", error);
618
+ return;
619
+ }
620
+ }
621
+ if (answers.testingScope.includes("unit") || answers.testingScope.includes("runtime")) {
622
+ await createVitestConfig(nuxt, answers);
623
+ }
624
+ if (answers.e2eRunner === "playwright") {
625
+ await createPlaywrightConfig(nuxt);
626
+ }
627
+ await createTestDirectories(nuxt, answers);
628
+ if (answers.exampleTests) {
629
+ await createExampleTests(nuxt, answers);
630
+ }
631
+ await updatePackageScripts(nuxt, answers);
632
+ await updateGitignore(nuxt, answers);
633
+ }
634
+ async function createVitestConfig(nuxt, answers) {
635
+ const rootDir = nuxt.options.rootDir;
636
+ const configPath = join(rootDir, "vitest.config.ts");
637
+ const config = generateVitestConfig(answers);
638
+ await promises.writeFile(configPath, config, "utf-8");
639
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), configPath))}`);
640
+ }
641
+ async function createPlaywrightConfig(nuxt) {
642
+ const rootDir = nuxt.options.rootDir;
643
+ const configPath = join(rootDir, "playwright.config.ts");
644
+ const config = generatePlaywrightConfig();
645
+ await promises.writeFile(configPath, config, "utf-8");
646
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), configPath))}`);
647
+ }
648
+ async function createTestDirectories(nuxt, answers) {
649
+ const rootDir = nuxt.options.rootDir;
650
+ if (answers.testingScope.includes("unit")) {
651
+ const unitDir = join(rootDir, "test/unit");
652
+ await promises.mkdir(unitDir, { recursive: true });
653
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), unitDir))}`);
654
+ }
655
+ if (answers.testingScope.includes("runtime")) {
656
+ const nuxtDir = join(rootDir, "test/nuxt");
657
+ await promises.mkdir(nuxtDir, { recursive: true });
658
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), nuxtDir))}`);
659
+ }
660
+ if (answers.testingScope.includes("e2e")) {
661
+ const e2eDir = answers.e2eRunner === "playwright" ? join(rootDir, "tests") : join(rootDir, "test/e2e");
662
+ await promises.mkdir(e2eDir, { recursive: true });
663
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eDir))}`);
664
+ }
665
+ }
666
+ async function createExampleTests(nuxt, answers) {
667
+ const rootDir = nuxt.options.rootDir;
668
+ if (answers.testingScope.includes("unit")) {
669
+ const unitTestPath = join(rootDir, "test/unit/example.test.ts");
670
+ const unitTest = `import { describe, expect, it } from 'vitest'
671
+
672
+ describe('example unit test', () => {
673
+ it('should pass', () => {
674
+ expect(1 + 1).toBe(2)
675
+ })
676
+ })
677
+ `;
678
+ await promises.writeFile(unitTestPath, unitTest, "utf-8");
679
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), unitTestPath))}`);
680
+ }
681
+ if (answers.testingScope.includes("runtime")) {
682
+ const componentTestPath = join(rootDir, "test/nuxt/component.test.ts");
683
+ const componentTest = `import { describe, expect, it } from 'vitest'
684
+ import { mountSuspended } from '@nuxt/test-utils/runtime'
685
+ import { defineComponent, h } from 'vue'
686
+
687
+ describe('component test example', () => {
688
+ it('can mount components', async () => {
689
+ const TestComponent = defineComponent({
690
+ setup() {
691
+ return () => h('div', 'Hello Nuxt!')
310
692
  },
311
- async load(id) {
312
- if (id.endsWith(STUB_ID) || id.endsWith(STUB_ID_WITH_EXT)) {
313
- const entryContents = readFileSync(options.entry, "utf-8");
314
- return entryContents.replace("#build/root-component.mjs", options.rootStubPath);
693
+ })
694
+
695
+ const component = await mountSuspended(TestComponent)
696
+
697
+ expect(component.text()).toBe('Hello Nuxt!')
698
+ })
699
+ })
700
+ `;
701
+ await promises.writeFile(componentTestPath, componentTest, "utf-8");
702
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), componentTestPath))}`);
703
+ }
704
+ if (answers.testingScope.includes("e2e")) {
705
+ if (answers.e2eRunner === "playwright") {
706
+ const e2eTestPath = join(rootDir, "tests/example.spec.ts");
707
+ const e2eTest = `import { expect, test } from '@nuxt/test-utils/playwright'
708
+
709
+ test('example e2e test', async ({ page, goto }) => {
710
+ await goto('/', { waitUntil: 'hydration' })
711
+ await expect(page).toHaveTitle(/Nuxt/)
712
+ })
713
+ `;
714
+ await promises.writeFile(e2eTestPath, e2eTest, "utf-8");
715
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eTestPath))}`);
716
+ } else {
717
+ const e2eTestPath = join(rootDir, "test/e2e/example.test.ts");
718
+ const e2eTest = `import { describe, expect, it } from 'vitest'
719
+ import { $fetch, setup } from '@nuxt/test-utils/e2e'
720
+
721
+ describe('example e2e test', async () => {
722
+ await setup()
723
+
724
+ it('renders the index page', async () => {
725
+ const html = await $fetch('/')
726
+ expect(html).toContain('Nuxt')
727
+ })
728
+ })
729
+ `;
730
+ await promises.writeFile(e2eTestPath, e2eTest, "utf-8");
731
+ logger.success(`Created ${colors.cyan(relative(process$1.cwd(), e2eTestPath))}`);
732
+ }
733
+ }
734
+ }
735
+ async function updatePackageScripts(nuxt, answers) {
736
+ const rootDir = nuxt.options.rootDir;
737
+ const packageJsonPath = join(rootDir, "package.json");
738
+ const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf-8"));
739
+ packageJson.scripts = packageJson.scripts || {};
740
+ const newScripts = getPackageScripts(answers);
741
+ Object.assign(packageJson.scripts, newScripts);
742
+ await promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
743
+ logger.success("Updated package.json scripts");
744
+ }
745
+ async function updateGitignore(nuxt, answers) {
746
+ const rootDir = nuxt.options.rootDir;
747
+ const gitignorePath = join(rootDir, ".gitignore");
748
+ let gitignore = "";
749
+ if (existsSync(gitignorePath)) {
750
+ gitignore = await promises.readFile(gitignorePath, "utf-8");
751
+ }
752
+ const lines = [];
753
+ if (answers.coverage && !gitignore.includes("coverage")) {
754
+ lines.push("# Test coverage", "coverage/", "");
755
+ }
756
+ if (answers.e2eRunner === "playwright") {
757
+ if (!gitignore.includes("playwright-report")) {
758
+ lines.push("# Playwright", "playwright-report/", "test-results/", "");
759
+ }
760
+ }
761
+ if (lines.length > 0) {
762
+ gitignore += "\n" + lines.join("\n");
763
+ await promises.writeFile(gitignorePath, gitignore, "utf-8");
764
+ logger.success("Updated .gitignore");
765
+ }
766
+ }
767
+
768
+ async function setupDevTools(vitestWrapper, nuxt = useNuxt()) {
769
+ const iframeSrc = "/__test_utils_vitest__/";
770
+ const updateTabs = debounce(() => {
771
+ nuxt.callHook("devtools:customTabs:refresh");
772
+ }, 100);
773
+ nuxt.hook("devtools:customTabs", (tabs) => {
774
+ const tab = createVitestCustomTab(vitestWrapper, { iframeSrc });
775
+ const index = tabs.findIndex(({ name }) => tab.name === name);
776
+ if (index === -1) {
777
+ tabs.push(tab);
778
+ } else {
779
+ tabs.splice(index, 1, tab);
780
+ }
781
+ });
782
+ addDevServerHandler({
783
+ route: iframeSrc,
784
+ handler: Object.assign(() => iframeContentHtml(vitestWrapper.uiUrl), { __is_handler__: true })
785
+ });
786
+ vitestWrapper.ons({
787
+ started() {
788
+ updateTabs();
789
+ },
790
+ updated() {
791
+ updateTabs();
792
+ },
793
+ finished() {
794
+ updateTabs();
795
+ },
796
+ exited() {
797
+ updateTabs();
798
+ }
799
+ });
800
+ }
801
+ function createVitestCustomTab(vitest, { iframeSrc }) {
802
+ const launchView = {
803
+ type: "launch",
804
+ description: "Start tests along with Nuxt",
805
+ actions: [
806
+ {
807
+ get label() {
808
+ switch (vitest.status) {
809
+ case "starting":
810
+ return "Starting...";
811
+ case "running":
812
+ return "Running Vitest";
813
+ case "stopped":
814
+ return "Start Vitest";
815
+ case "finished":
816
+ return "Start Vitest";
817
+ }
818
+ },
819
+ get pending() {
820
+ return vitest.status === "starting" || vitest.status === "running";
821
+ },
822
+ handle: () => {
823
+ vitest.start();
315
824
  }
316
825
  }
826
+ ]
827
+ };
828
+ const uiView = {
829
+ type: "iframe",
830
+ persistent: false,
831
+ src: iframeSrc
832
+ };
833
+ const tab = {
834
+ title: "Vitest",
835
+ name: "vitest",
836
+ icon: "logos-vitest",
837
+ get view() {
838
+ if (vitest.status === "stopped" || vitest.status === "starting" || !vitest.uiUrl) {
839
+ return launchView;
840
+ } else {
841
+ return uiView;
842
+ }
843
+ },
844
+ extraTabVNode: vitest.testSummary.totalCount ? h("div", { style: { color: vitest.testSummary.failedCount ? "orange" : "green" } }, [
845
+ h("span", {}, vitest.testSummary.passedCount),
846
+ h("span", { style: { opacity: "0.5", fontSize: "0.9em" } }, "/"),
847
+ h(
848
+ "span",
849
+ { style: { opacity: "0.8", fontSize: "0.9em" } },
850
+ vitest.testSummary.totalCount
851
+ )
852
+ ]) : void 0
853
+ };
854
+ return tab;
855
+ }
856
+ function iframeContentHtml(uiUrl) {
857
+ return [
858
+ "<html><head><script>",
859
+ `(${function redirect(uiUrl2, provider2) {
860
+ if (typeof window === "undefined") return;
861
+ if (!uiUrl2) return;
862
+ if (provider2 === "stackblitz") {
863
+ const url = new URL(window.location.href);
864
+ const newUrl = new URL(uiUrl2);
865
+ newUrl.host = url.host.replace(/--\d+--/, `--${newUrl.port}--`);
866
+ newUrl.protocol = url.protocol;
867
+ newUrl.port = url.port;
868
+ uiUrl2 = newUrl.toString();
869
+ }
870
+ window.location.replace(uiUrl2);
871
+ }})(${JSON.stringify(uiUrl)}, ${JSON.stringify(provider)})`,
872
+ "<\/script></head></html>"
873
+ ].join("\n");
874
+ }
875
+
876
+ function vitestWrapper(options) {
877
+ const { cwd, ...startOptions } = options;
878
+ let _status = "stopped";
879
+ let _uiUrl;
880
+ let _process;
881
+ let _testSummary = createVitestTestSummary();
882
+ const _handlers = {
883
+ started: [({ uiUrl }) => {
884
+ _uiUrl = uiUrl;
885
+ _status = "running";
886
+ _testSummary = createVitestTestSummary();
887
+ }],
888
+ updated: [(summary) => {
889
+ _testSummary = summary;
890
+ }],
891
+ finished: [(summary) => {
892
+ _status = "finished";
893
+ _testSummary = summary;
894
+ }],
895
+ exited: [clear]
896
+ };
897
+ function clear() {
898
+ _status = "stopped";
899
+ _uiUrl = void 0;
900
+ _process = void 0;
901
+ _testSummary = createVitestTestSummary();
902
+ }
903
+ function on(name, handler) {
904
+ _handlers[name] ??= [];
905
+ _handlers[name]?.push(handler);
906
+ }
907
+ function ons(handlers) {
908
+ for (const [name, handler] of Object.entries(handlers)) {
909
+ if (typeof handler === "function") {
910
+ on(name, handler);
911
+ }
912
+ }
913
+ }
914
+ async function stop() {
915
+ const vitest = _process;
916
+ if (!vitest || vitest.exitCode !== null) return;
917
+ return new Promise((resolve2) => {
918
+ vitest.once("exit", () => resolve2());
919
+ sendMessageToCli(vitest, "stop", { force: true });
920
+ });
921
+ }
922
+ async function start() {
923
+ if (_process) return false;
924
+ const vitest = fork(resolve(distDir, "./vitest-wrapper/cli.mjs"), {
925
+ cwd,
926
+ env: {
927
+ ...process.env,
928
+ NODE_ENV: "test",
929
+ MODE: "test"
930
+ },
931
+ stdio: startOptions.logToConsole ? void 0 : ["ignore", "ignore", "inherit", "ipc"]
932
+ });
933
+ _status = "starting";
934
+ _process = vitest;
935
+ vitest.once("exit", () => {
936
+ _handlers.exited.forEach((fn) => fn({ exitCode: vitest.exitCode ?? 0 }));
937
+ });
938
+ listenCliMessages(vitest, ({ type, payload }) => {
939
+ _handlers[type].forEach((fn) => fn(payload));
940
+ });
941
+ sendMessageToCli(vitest, "start", startOptions);
942
+ return true;
943
+ }
944
+ return {
945
+ on,
946
+ ons,
947
+ stop,
948
+ start,
949
+ get uiUrl() {
950
+ return _uiUrl;
951
+ },
952
+ get options() {
953
+ return options;
954
+ },
955
+ get status() {
956
+ return _status;
957
+ },
958
+ get testSummary() {
959
+ return { ..._testSummary };
317
960
  }
318
961
  };
319
- });
962
+ }
963
+
964
+ const version = "3.23.0";
965
+ const pkg = {
966
+ version: version};
320
967
 
321
- const vitePluginBlocklist = ["vite-plugin-vue-inspector", "vite-plugin-vue-inspector:post", "vite-plugin-inspect", "nuxt:type-check"];
322
968
  const module$1 = defineNuxtModule({
323
969
  meta: {
324
970
  name: "@nuxt/test-utils",
325
- configKey: "testUtils"
971
+ configKey: "testUtils",
972
+ version: pkg.version
326
973
  },
327
974
  defaults: {
328
975
  startOnBoot: false,
329
976
  logToConsole: false
330
977
  },
978
+ async onInstall(nuxt) {
979
+ await runInstallWizard(nuxt);
980
+ },
331
981
  async setup(options, nuxt) {
332
982
  if (nuxt.options.test || nuxt.options.dev) {
333
983
  await setupImportMocking(nuxt);
334
984
  }
335
985
  const { addVitePlugin } = await loadKit(nuxt.options.rootDir);
336
986
  const resolver = createResolver(import.meta.url);
337
- addVitePlugin(NuxtRootStubPlugin.vite({
338
- entry: await resolvePath("#app/entry", { alias: nuxt.options.alias }),
339
- rootStubPath: await resolvePath(resolver.resolve("./runtime/nuxt-root"))
340
- }));
987
+ if (nuxt.options.test || nuxt.options.dev) {
988
+ addVitePlugin(NuxtRootStubPlugin({
989
+ entry: await resolvePath("#app/entry", { alias: nuxt.options.alias }),
990
+ rootStubPath: await resolvePath(resolver.resolve("./runtime/nuxt-root"))
991
+ }));
992
+ }
341
993
  if (!nuxt.options.test && !nuxt.options.dev) {
342
994
  nuxt.options.vite.define ||= {};
343
995
  nuxt.options.vite.define["import.meta.vitest"] = "undefined";
344
996
  }
345
- nuxt.hook("prepare:types", (ctx2) => {
346
- ctx2.references.push({ types: "vitest/import-meta" });
347
- if (ctx2.nodeTsConfig) {
348
- ctx2.nodeTsConfig.include ||= [];
349
- ctx2.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.rootDir, "vitest.config.*")));
997
+ nuxt.hook("prepare:types", (ctx) => {
998
+ ctx.references.push({ types: "vitest/import-meta" });
999
+ if (ctx.nodeTsConfig) {
1000
+ ctx.nodeTsConfig.include ||= [];
1001
+ ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.rootDir, "vitest.config.*")));
350
1002
  if (nuxt.options.workspaceDir !== nuxt.options.rootDir) {
351
- ctx2.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.workspaceDir, "vitest.config.*")));
1003
+ ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.workspaceDir, "vitest.config.*")));
352
1004
  }
353
1005
  }
354
1006
  });
355
1007
  if (!nuxt.options.dev) return;
356
1008
  if (process.env.TEST || process.env.VITE_TEST) return;
357
- const rawViteConfigPromise = new Promise((resolve) => {
358
- nuxt.hook("app:resolve", () => {
359
- nuxt.hook("vite:configResolved", (config, { isClient }) => {
360
- if (isClient) resolve(config);
361
- });
362
- });
363
- });
364
- let loaded = false;
365
- let promise;
366
- let ctx = void 0;
367
- let testFiles = null;
368
- const updateTabs = debounce(() => {
369
- nuxt.callHook("devtools:customTabs:refresh");
370
- }, 100);
371
- let URL;
372
- async function start() {
373
- const { mergeConfig } = await importModule("vite", { paths: nuxt.options.modulesDir });
374
- const rawViteConfig = mergeConfig({}, await rawViteConfigPromise);
375
- const viteConfig = await getVitestConfigFromNuxt({ nuxt, viteConfig: defu({ test: options.vitestConfig }, rawViteConfig) });
376
- viteConfig.plugins = (viteConfig.plugins || []).filter((p) => {
377
- return !p || !("name" in p) || !vitePluginBlocklist.includes(p.name);
378
- });
379
- viteConfig.test.environmentMatchGlobs ||= [];
380
- viteConfig.test.environmentMatchGlobs.push(
381
- ["**/*.nuxt.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "nuxt"],
382
- ["{test,tests}/nuxt/**.*", "nuxt"]
383
- );
384
- process.env.__NUXT_VITEST_RESOLVED__ = "true";
385
- const { startVitest } = await import(pathToFileURL(await resolvePath("vitest/node")).href);
386
- const customReporter = {
387
- onInit(_ctx) {
388
- ctx = _ctx;
389
- },
390
- onTaskUpdate() {
391
- testFiles = ctx.state.getFiles();
392
- updateTabs();
393
- },
394
- onFinished() {
395
- testFiles = ctx.state.getFiles();
396
- updateTabs();
397
- }
398
- };
399
- const watchMode = !process.env.NUXT_VITEST_DEV_TEST && !isCI;
400
- const PORT = await getPort({ port: 15555 });
401
- const PROTOCOL = nuxt.options.devServer.https ? "https" : "http";
402
- URL = `${PROTOCOL}://localhost:${PORT}/__vitest__/`;
403
- const overrides = watchMode ? {
404
- passWithNoTests: true,
405
- reporters: options.logToConsole ? [
406
- ...toArray(options.vitestConfig?.reporters ?? ["default"]),
407
- customReporter
408
- ] : [customReporter],
409
- // do not report to console
410
- watch: true,
411
- ui: true,
412
- open: false,
413
- api: {
414
- port: PORT
415
- }
416
- } : { watch: false };
417
- const promise2 = startVitest("test", [], defu(overrides, viteConfig.test), viteConfig);
418
- promise2.catch(() => process.exit(1));
419
- if (watchMode) {
420
- logger.info(`Vitest UI starting on ${URL}`);
421
- nuxt.hook("close", () => promise2.then((v) => v?.close()));
422
- await new Promise((resolve) => setTimeout(resolve, 1e3));
423
- } else {
424
- promise2.then((v) => nuxt.close().then(() => v?.close()).then(() => process.exit()));
425
- }
426
- loaded = true;
427
- }
428
- nuxt.hook("devtools:customTabs", (tabs) => {
429
- const failedCount = testFiles?.filter((f) => f.result?.state === "fail").length ?? 0;
430
- const passedCount = testFiles?.filter((f) => f.result?.state === "pass").length ?? 0;
431
- const totalCount = testFiles?.length ?? 0;
432
- tabs.push({
433
- title: "Vitest",
434
- name: "vitest",
435
- icon: "logos-vitest",
436
- view: loaded ? {
437
- type: "iframe",
438
- src: URL
439
- } : {
440
- type: "launch",
441
- description: "Start tests along with Nuxt",
442
- actions: [
443
- {
444
- label: promise ? "Starting..." : "Start Vitest",
445
- pending: !!promise,
446
- handle: () => {
447
- promise = promise || start();
448
- return promise;
449
- }
450
- }
451
- ]
452
- },
453
- extraTabVNode: totalCount ? h("div", { style: { color: failedCount ? "orange" : "green" } }, [
454
- h("span", {}, passedCount),
455
- h("span", { style: { opacity: "0.5", fontSize: "0.9em" } }, "/"),
456
- h(
457
- "span",
458
- { style: { opacity: "0.8", fontSize: "0.9em" } },
459
- totalCount
460
- )
461
- ]) : void 0
462
- });
1009
+ const vitestWrapper2 = createVitestWrapper(options, nuxt);
1010
+ nuxt.hook("devtools:before", async () => {
1011
+ await setupDevTools(vitestWrapper2, nuxt);
463
1012
  });
464
1013
  if (options.startOnBoot) {
465
- promise = promise || start();
466
- promise.then(updateTabs);
1014
+ vitestWrapper2.start();
467
1015
  }
468
1016
  }
469
1017
  });
470
- function toArray(value) {
471
- return Array.isArray(value) ? value : [value];
1018
+ function createVitestWrapper(options, nuxt = useNuxt()) {
1019
+ const watchMode = !isCI;
1020
+ const wrapper = vitestWrapper({
1021
+ cwd: nuxt.options.rootDir,
1022
+ apiPorts: [15555],
1023
+ logToConsole: options.logToConsole ?? false,
1024
+ watchMode
1025
+ });
1026
+ wrapper.ons({
1027
+ started({ uiUrl }) {
1028
+ if (watchMode) {
1029
+ logger.info(`Vitest UI starting on ${uiUrl}`);
1030
+ }
1031
+ },
1032
+ exited({ exitCode }) {
1033
+ if (watchMode) {
1034
+ logger.info(`Vitest exited with code ${exitCode}`);
1035
+ } else {
1036
+ nuxt.close().finally(() => process.exit(exitCode));
1037
+ }
1038
+ }
1039
+ });
1040
+ nuxt.hooks.addHooks({
1041
+ close: () => wrapper.stop(),
1042
+ restart: () => wrapper.stop()
1043
+ });
1044
+ return wrapper;
472
1045
  }
473
1046
 
474
1047
  export { module$1 as default };