@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
66import { parse as parseYaml } from 'yaml' ;
77
88import { PackageManager } from '../../types/index.js' ;
9+ import { VITEST_VERSION } from '../../utils/constants.js' ;
910import { createMigrationReport } from '../report.js' ;
1011
1112// Mock VITE_PLUS_VERSION to a stable value for snapshot tests.
@@ -330,6 +331,54 @@ describe('rewritePackageJson', () => {
330331 expect ( pkg . dependencies ) . toHaveProperty ( 'playwright' , '^1.40.0' ) ;
331332 expect ( pkg . devDependencies ) . not . toHaveProperty ( 'playwright' ) ;
332333 } ) ;
334+
335+ it ( 'adds a direct vitest devDependency when the package uses browser mode' , async ( ) => {
336+ // A package that drives vitest browser mode but has no direct vitest dep
337+ // (e.g. it only imports `vite-plus/test/browser-playwright`). `@vitest/browser`
338+ // needs `vitest` resolvable from the package root, so the migration must
339+ // pin it as a direct devDependency.
340+ const pkg = {
341+ devDependencies : {
342+ playwright : '^1.58.0' ,
343+ } ,
344+ } ;
345+ rewritePackageJson ( pkg , PackageManager . pnpm , true , undefined , undefined , true ) ;
346+ expect ( pkg . devDependencies ) . toHaveProperty ( 'vitest' , 'catalog:' ) ;
347+ expect ( pkg . devDependencies ) . toHaveProperty ( 'vite-plus' , 'catalog:' ) ;
348+ } ) ;
349+
350+ it ( 'uses a concrete vitest version for browser mode in non-catalog package managers' , async ( ) => {
351+ const pkg = {
352+ devDependencies : {
353+ playwright : '^1.58.0' ,
354+ } ,
355+ } ;
356+ rewritePackageJson ( pkg , PackageManager . npm , false , undefined , undefined , true ) ;
357+ expect ( ( pkg as { devDependencies ?: Record < string , string > } ) . devDependencies ?. vitest ) . toBe (
358+ VITEST_VERSION ,
359+ ) ;
360+ } ) ;
361+
362+ it ( 'does not overwrite an existing direct vitest dep in browser mode' , async ( ) => {
363+ const pkg = {
364+ devDependencies : {
365+ vitest : '^4.0.0' ,
366+ } ,
367+ } ;
368+ rewritePackageJson ( pkg , PackageManager . pnpm , true , undefined , undefined , true ) ;
369+ // existing direct dep is normalized through the override path, not replaced
370+ expect ( pkg . devDependencies . vitest ) . toBe ( 'catalog:' ) ;
371+ } ) ;
372+
373+ it ( 'does not add vitest when browser mode is not detected' , async ( ) => {
374+ const pkg = {
375+ devDependencies : {
376+ vite : '^7.0.0' ,
377+ } ,
378+ } ;
379+ rewritePackageJson ( pkg , PackageManager . pnpm , true , undefined , undefined , false ) ;
380+ expect ( pkg . devDependencies ) . not . toHaveProperty ( 'vitest' ) ;
381+ } ) ;
333382} ) ;
334383
335384describe ( 'parseNvmrcVersion' , ( ) => {
@@ -779,6 +828,72 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => {
779828 expect ( pkg . peerDependencies ) . not . toHaveProperty ( 'tsdown' ) ;
780829 } ) ;
781830
831+ it ( 'adds a direct vitest dep when a vite config enables browser mode' , ( ) => {
832+ // A package whose vite config imports a browser provider but has no direct
833+ // vitest dep — `@vitest/browser` needs `vitest` resolvable from the package
834+ // root, so the migration must pin it. Mirrors the vibe-dashboard regression.
835+ fs . writeFileSync (
836+ path . join ( tmpDir , 'package.json' ) ,
837+ JSON . stringify ( { name : 'test' , devDependencies : { playwright : '^1.58.0' } } ) ,
838+ ) ;
839+ fs . writeFileSync (
840+ path . join ( tmpDir , 'vite.config.ts' ) ,
841+ [
842+ "import { playwright } from 'vite-plus/test/browser-playwright';" ,
843+ "import { defineConfig } from 'vite-plus';" ,
844+ 'export default defineConfig({' ,
845+ ' test: { browser: { enabled: true, provider: playwright() } },' ,
846+ '});' ,
847+ '' ,
848+ ] . join ( '\n' ) ,
849+ ) ;
850+ rewriteStandaloneProject ( tmpDir , makeWorkspaceInfo ( tmpDir , PackageManager . pnpm ) , true , true ) ;
851+
852+ const pkg = readJson ( path . join ( tmpDir , 'package.json' ) ) ;
853+ const devDeps = pkg . devDependencies as Record < string , string > ;
854+ expect ( devDeps . vitest ) . toBe ( 'catalog:' ) ;
855+ expect ( devDeps [ 'vite-plus' ] ) . toBe ( 'catalog:' ) ;
856+ } ) ;
857+
858+ it ( 'detects browser mode from a test file when the config has no hint' , ( ) => {
859+ // Browser config can live in a workspace-referenced config under any name;
860+ // the source scan also catches `@vitest/browser*` imports in test files.
861+ fs . writeFileSync (
862+ path . join ( tmpDir , 'package.json' ) ,
863+ JSON . stringify ( { name : 'test' , devDependencies : { vite : '^7.0.0' } } ) ,
864+ ) ;
865+ fs . mkdirSync ( path . join ( tmpDir , 'src' , '__tests__' ) , { recursive : true } ) ;
866+ fs . writeFileSync (
867+ path . join ( tmpDir , 'src' , '__tests__' , 'app.test.ts' ) ,
868+ "import { page } from '@vitest/browser/context';\n" ,
869+ ) ;
870+ rewriteStandaloneProject ( tmpDir , makeWorkspaceInfo ( tmpDir , PackageManager . pnpm ) , true , true ) ;
871+
872+ const devDeps = readJson ( path . join ( tmpDir , 'package.json' ) ) . devDependencies as Record <
873+ string ,
874+ string
875+ > ;
876+ expect ( devDeps . vitest ) . toBe ( 'catalog:' ) ;
877+ } ) ;
878+
879+ it ( 'does not add vitest for a package without browser mode' , ( ) => {
880+ fs . writeFileSync (
881+ path . join ( tmpDir , 'package.json' ) ,
882+ JSON . stringify ( { name : 'test' , devDependencies : { vite : '^7.0.0' } } ) ,
883+ ) ;
884+ fs . writeFileSync (
885+ path . join ( tmpDir , 'vite.config.ts' ) ,
886+ "import { defineConfig } from 'vite';\nexport default defineConfig({});\n" ,
887+ ) ;
888+ rewriteStandaloneProject ( tmpDir , makeWorkspaceInfo ( tmpDir , PackageManager . pnpm ) , true , true ) ;
889+
890+ const devDeps = readJson ( path . join ( tmpDir , 'package.json' ) ) . devDependencies as Record <
891+ string ,
892+ string
893+ > ;
894+ expect ( devDeps ) . not . toHaveProperty ( 'vitest' ) ;
895+ } ) ;
896+
782897 it ( 'preserves named pnpm overrides when moving root overrides to pnpm-workspace.yaml' , ( ) => {
783898 fs . writeFileSync (
784899 path . join ( tmpDir , 'package.json' ) ,
@@ -888,6 +1003,51 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => {
8881003 // resolve to the override target are coerced to '*'.
8891004 expect ( pkg . peerDependencies . vitest ) . toBe ( '*' ) ;
8901005 } ) ;
1006+
1007+ it ( 'adds vitest only to the monorepo package that uses browser mode' , ( ) => {
1008+ // Root has no browser config; only `apps/dashboard` does. The browser-mode
1009+ // scan must stop at the nested package.json boundary so the root package
1010+ // does not inherit the sub-package's signal.
1011+ fs . writeFileSync (
1012+ path . join ( tmpDir , 'package.json' ) ,
1013+ JSON . stringify ( { name : 'root' , devDependencies : { } } ) ,
1014+ ) ;
1015+ fs . writeFileSync ( path . join ( tmpDir , 'pnpm-workspace.yaml' ) , 'packages:\n - apps/*\n' ) ;
1016+ const appDir = path . join ( tmpDir , 'apps' , 'dashboard' ) ;
1017+ fs . mkdirSync ( appDir , { recursive : true } ) ;
1018+ fs . writeFileSync (
1019+ path . join ( appDir , 'package.json' ) ,
1020+ JSON . stringify ( { name : '@vibe/dashboard' , devDependencies : { playwright : '^1.58.0' } } ) ,
1021+ ) ;
1022+ fs . writeFileSync (
1023+ path . join ( appDir , 'vite.config.ts' ) ,
1024+ [
1025+ "import { playwright } from 'vite-plus/test/browser-playwright';" ,
1026+ "import { defineConfig } from 'vite-plus';" ,
1027+ 'export default defineConfig({ test: { browser: { provider: playwright() } } });' ,
1028+ '' ,
1029+ ] . join ( '\n' ) ,
1030+ ) ;
1031+
1032+ const workspaceInfo = makeWorkspaceInfo ( tmpDir , PackageManager . pnpm ) ;
1033+ workspaceInfo . isMonorepo = true ;
1034+ workspaceInfo . packages = [
1035+ { name : '@vibe/dashboard' , path : 'apps/dashboard' , isTemplatePackage : false } ,
1036+ ] ;
1037+ rewriteMonorepo ( workspaceInfo , true ) ;
1038+
1039+ const rootDeps = ( readJson ( path . join ( tmpDir , 'package.json' ) ) . devDependencies ?? { } ) as Record <
1040+ string ,
1041+ string
1042+ > ;
1043+ expect ( rootDeps ) . not . toHaveProperty ( 'vitest' ) ;
1044+
1045+ const appDeps = readJson ( path . join ( appDir , 'package.json' ) ) . devDependencies as Record <
1046+ string ,
1047+ string
1048+ > ;
1049+ expect ( appDeps . vitest ) . toBe ( 'catalog:' ) ;
1050+ } ) ;
8911051} ) ;
8921052
8931053describe ( 'rewriteMonorepo yarn catalog' , ( ) => {
0 commit comments