@@ -12,7 +12,7 @@ import * as find from 'empathic/find';
1212import type { ResultPromise } from 'execa' ;
1313
1414import { SupportedBuilder , SupportedFramework } from '../types/index.ts' ;
15- import { AddonVitestService } from './AddonVitestService.ts' ;
15+ import { AddonVitestService , canInstallLatestVitest } from './AddonVitestService.ts' ;
1616
1717vi . mock ( 'node:fs/promises' , { spy : true } ) ;
1818vi . mock ( 'node:os' , { spy : true } ) ;
@@ -61,11 +61,12 @@ describe('AddonVitestService', () => {
6161 it ( 'should collect base packages when not installed' , async ( ) => {
6262 const deps = await service . collectDependencies ( ) ;
6363
64- expect ( deps ) . toContain ( 'vitest' ) ;
65- // The addon always installs the Vitest 4 browser provider
66- expect ( deps ) . toContain ( '@vitest/browser-playwright' ) ;
64+ // No vitest version is declared and no @types /node conflict exists, so the fresh install
65+ // resolves latest Vitest
66+ expect ( deps ) . toContain ( 'vitest@latest' ) ;
67+ expect ( deps ) . toContain ( '@vitest/browser-playwright@latest' ) ;
6768 expect ( deps ) . toContain ( 'playwright' ) ;
68- expect ( deps ) . toContain ( '@vitest/coverage-v8' ) ;
69+ expect ( deps ) . toContain ( '@vitest/coverage-v8@latest ' ) ;
6970 } ) ;
7071
7172 it ( 'should not include base packages if already installed' , async ( ) => {
@@ -92,11 +93,10 @@ describe('AddonVitestService', () => {
9293 const deps = await service . collectDependencies ( ) ;
9394
9495 // Should only contain base packages, not framework-specific ones
95- expect ( deps ) . toContain ( 'vitest' ) ;
96- // The addon always installs the Vitest 4 browser provider
97- expect ( deps ) . toContain ( '@vitest/browser-playwright' ) ;
96+ expect ( deps ) . toContain ( 'vitest@latest' ) ;
97+ expect ( deps ) . toContain ( '@vitest/browser-playwright@latest' ) ;
9898 expect ( deps ) . toContain ( 'playwright' ) ;
99- expect ( deps ) . toContain ( '@vitest/coverage-v8' ) ;
99+ expect ( deps ) . toContain ( '@vitest/coverage-v8@latest ' ) ;
100100 expect ( deps . every ( ( d ) => ! d . includes ( 'nextjs-vite' ) ) ) . toBe ( true ) ;
101101 } ) ;
102102
@@ -143,14 +143,88 @@ describe('AddonVitestService', () => {
143143 ) ;
144144 } ) ;
145145
146- it ( 'does not pin anything when the vitest version cannot be resolved ' , async ( ) => {
146+ it ( 'falls back to the Vitest 4 family when no vitest is declared and @types/node conflicts ' , async ( ) => {
147147 vi . mocked ( mockPackageManager . getDeclaredVersionSpecifier ) . mockResolvedValue ( null ) ;
148+ vi . mocked ( mockPackageManager . getAllDependencies ) . mockReturnValue ( {
149+ '@types/node' : '^20' , // e.g. create-next-app scaffolds
150+ } ) ;
151+
152+ const deps = await service . collectDependencies ( ) ;
153+
154+ expect ( deps ) . toContain ( 'vitest@^4' ) ;
155+ expect ( deps ) . toContain ( '@vitest/browser-playwright@^4' ) ;
156+ expect ( deps ) . toContain ( '@vitest/coverage-v8@^4' ) ;
157+ expect ( deps ) . toContain ( 'playwright' ) ; // playwright is versioned independently
158+ // The fallback flows through the same alignment path as a declared specifier, so the
159+ // package manager applies its own convention (e.g. pnpm catalog registration).
160+ expect ( mockPackageManager . applyVersionToRelatedPackages ) . toHaveBeenCalledWith (
161+ [ 'vitest' , '@vitest/browser-playwright' , '@vitest/coverage-v8' ] ,
162+ '^4' ,
163+ 'vitest'
164+ ) ;
165+ } ) ;
166+
167+ it ( 'falls back instead of blowing up collectDependencies on a pnpm catalog @types/node' , async ( ) => {
168+ vi . mocked ( mockPackageManager . getDeclaredVersionSpecifier ) . mockResolvedValue ( null ) ;
169+ vi . mocked ( mockPackageManager . getAllDependencies ) . mockReturnValue ( {
170+ '@types/node' : 'catalog:' , // pnpm catalog reference; unparseable as a semver range
171+ } ) ;
172+
173+ const deps = await service . collectDependencies ( ) ;
174+
175+ expect ( deps ) . toContain ( 'vitest@^4' ) ;
176+ expect ( deps ) . toContain ( '@vitest/browser-playwright@^4' ) ;
177+ } ) ;
178+
179+ it ( 'installs latest Vitest when no vitest is declared and no peer conflict exists' , async ( ) => {
180+ vi . mocked ( mockPackageManager . getDeclaredVersionSpecifier ) . mockResolvedValue ( null ) ;
181+ vi . mocked ( mockPackageManager . getAllDependencies ) . mockReturnValue ( { } ) ;
148182
149183 const deps = await service . collectDependencies ( ) ;
150184
151- expect ( deps ) . toContain ( '@vitest/coverage-v8' ) ;
152- expect ( deps . every ( ( d ) => ! d . includes ( '@catalog:' ) && ! d . includes ( '@3.' ) ) ) . toBe ( true ) ;
153- expect ( mockPackageManager . applyVersionToRelatedPackages ) . not . toHaveBeenCalled ( ) ;
185+ expect ( deps ) . toContain ( 'vitest@latest' ) ;
186+ expect ( deps ) . toContain ( '@vitest/browser-playwright@latest' ) ;
187+ expect ( deps ) . toContain ( '@vitest/coverage-v8@latest' ) ;
188+ expect ( deps ) . toContain ( 'playwright' ) ;
189+ expect ( mockPackageManager . applyVersionToRelatedPackages ) . toHaveBeenCalledWith (
190+ [ 'vitest' , '@vitest/browser-playwright' , '@vitest/coverage-v8' ] ,
191+ 'latest' ,
192+ 'vitest'
193+ ) ;
194+ } ) ;
195+
196+ it ( 'installs latest Vitest when the project @types/node range satisfies the latest peer' , async ( ) => {
197+ vi . mocked ( mockPackageManager . getDeclaredVersionSpecifier ) . mockResolvedValue ( null ) ;
198+ vi . mocked ( mockPackageManager . getAllDependencies ) . mockReturnValue ( {
199+ '@types/node' : '^22' ,
200+ } ) ;
201+
202+ const deps = await service . collectDependencies ( ) ;
203+
204+ expect ( deps ) . toContain ( 'vitest@latest' ) ;
205+ } ) ;
206+ } ) ;
207+
208+ describe ( 'canInstallLatestVitest' , ( ) => {
209+ it ( 'allows latest when @types/node is absent or unversioned' , ( ) => {
210+ expect ( canInstallLatestVitest ( { } ) ) . toBe ( true ) ;
211+ expect ( canInstallLatestVitest ( { '@types/node' : '*' } ) ) . toBe ( true ) ;
212+ } ) ;
213+
214+ it ( 'blocks latest when the @types/node range cannot satisfy the latest peer range' , ( ) => {
215+ expect ( canInstallLatestVitest ( { '@types/node' : '^20' } ) ) . toBe ( false ) ;
216+ expect ( canInstallLatestVitest ( { '@types/node' : '20.19.0' } ) ) . toBe ( false ) ;
217+ } ) ;
218+
219+ it ( 'allows latest when the @types/node range intersects the latest peer range' , ( ) => {
220+ expect ( canInstallLatestVitest ( { '@types/node' : '^22' } ) ) . toBe ( true ) ;
221+ expect ( canInstallLatestVitest ( { '@types/node' : '>=20' } ) ) . toBe ( true ) ;
222+ } ) ;
223+
224+ it ( 'falls back instead of throwing on unparseable specifiers like pnpm catalog references' , ( ) => {
225+ expect ( ( ) => canInstallLatestVitest ( { '@types/node' : 'catalog:' } ) ) . not . toThrow ( ) ;
226+ expect ( canInstallLatestVitest ( { '@types/node' : 'catalog:' } ) ) . toBe ( false ) ;
227+ expect ( canInstallLatestVitest ( { '@types/node' : 'workspace:*' } ) ) . toBe ( false ) ;
154228 } ) ;
155229 } ) ;
156230
0 commit comments