diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/package.json new file mode 100644 index 0000000000..b1246e8cd1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/package.json @@ -0,0 +1,19 @@ +{ + "name": "playwright-test-app", + "private": true, + "devDependencies": { + "@playwright/test": "1.60.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "vite": "catalog:", + "vite-plus": "catalog:", + "vitest": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "11.1.1", + "onFail": "download" + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/pnpm-workspace.yaml new file mode 100644 index 0000000000..aae9c0138f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/pnpm-workspace.yaml @@ -0,0 +1,16 @@ +packages: + - . +catalog: + vite: npm:@voidzero-dev/vite-plus-core@latest + vite-plus: latest + vitest: 4.1.10 +overrides: + vite: 'catalog:' + vitest: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots.toml new file mode 100644 index 0000000000..079ebdfc76 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots.toml @@ -0,0 +1,8 @@ +[[case]] +name = "migration_playwright_test" +vp = "global" +steps = [ + { argv = ["vp", "migrate", "--no-interactive"], comment = "existing Vite+ upgrade: @playwright/test should remain without adding a direct playwright dependency", continue-on-failure = true }, + { argv = ["vpt", "print-file", "package.json"], comment = "@vitest/browser-playwright and @vitest/coverage-v8 should become catalog:", continue-on-failure = true }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"], comment = "the default catalog should own the aligned @vitest/* packages", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md new file mode 100644 index 0000000000..983c329bc3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md @@ -0,0 +1,70 @@ +# migration_playwright_test + +## `vp migrate --no-interactive` + +existing Vite+ upgrade: @playwright/test should remain without adding a direct playwright dependency + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → + vitest 4.1.10 → + @vitest/browser-playwright 4.1.10 → + @vitest/coverage-v8 4.1.10 → +• Package manager settings configured +``` + +## `vpt print-file package.json` + +@vitest/browser-playwright and @vitest/coverage-v8 should become catalog: + +``` +{ + "name": "playwright-test-app", + "private": true, + "devDependencies": { + "@playwright/test": "1.60.0", + "@vitest/browser-playwright": "catalog:", + "@vitest/coverage-v8": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:", + "vitest": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +the default catalog should own the aligned @vitest/* packages + +``` +packages: + - . +catalog: + vite: npm:@voidzero-dev/vite-plus-core@ + vite-plus: + vitest: + '@vitest/browser-playwright': + '@vitest/coverage-v8': +overrides: + vite@*: 'catalog:' + vitest@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: '*' + vitest: '*' +``` diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index c07158b82b..d3e92bdc6c 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -632,10 +632,9 @@ describe('rewritePackageJson', () => { expect(pkg.devDependencies).not.toHaveProperty('@vitest/browser'); }); - it('pins the provider framework peer to a lockstep sibling instead of * (npmx.dev #27)', () => { - // `playwright` and `@playwright/test` release in lockstep, so a newly-added - // `playwright` peer should reuse the pinned @playwright/test version rather - // than a non-deterministic `*`. + it('does not add playwright when @playwright/test already provides it', () => { + // `@playwright/test` has a dependency on `playwright`, + // so adding a second direct dependency is redundant. const pkg = { devDependencies: { '@vitest/browser-playwright': '^4.0.0', @@ -644,7 +643,7 @@ describe('rewritePackageJson', () => { }, }; rewritePackageJson(pkg, PackageManager.pnpm); - expect(pkg.devDependencies).toHaveProperty('playwright', '1.60.0'); + expect(pkg.devDependencies).not.toHaveProperty('playwright'); }); it('injects a direct vite devDependency for an npm project that uses an opt-in browser provider', async () => { @@ -3409,6 +3408,56 @@ describe('ensureVitePlusBootstrap', () => { expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false); }); + it('does not add playwright on upgrade when @playwright/test already provides it', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'browser-app', + devDependencies: { + '@playwright/test': '1.60.0', + 'vite-plus': 'catalog:', + }, + devEngines: { + packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' }, + }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + [ + "import { defineConfig } from 'vite-plus';", + "import { playwright } from 'vite-plus/test/browser-playwright';", + 'export default defineConfig({ test: { browser: { enabled: true, provider: playwright() } } });', + ].join('\n'), + ); + fs.writeFileSync( + path.join(tmpDir, 'pnpm-workspace.yaml'), + [ + 'catalog:', + ' vite-plus: latest', + ' vite: npm:@voidzero-dev/vite-plus-core@latest', + 'overrides:', + " vite: 'catalog:'", + 'peerDependencyRules:', + ' allowAny: [vite]', + ' allowedVersions:', + " vite: '*'", + '', + ].join('\n'), + ); + + expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true); + ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm)); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + devDependencies: Record; + }; + expect(pkg.devDependencies).not.toHaveProperty('playwright'); + expect(pkg.devDependencies.vitest).toBe('catalog:'); + expect(pkg.devDependencies['@vitest/browser-playwright']).toBe('catalog:'); + expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false); + }); + it('references the catalog for an installed browser provider the injection adds to the catalog', () => { // #2005: a workspace package uses `@vitest/browser-playwright` only through a // Vite+ shim (no dep), so migration ADDS the provider to the default catalog. diff --git a/packages/cli/src/migration/migrator/package-json.ts b/packages/cli/src/migration/migrator/package-json.ts index 8b8234c283..4c78dd163e 100644 --- a/packages/cli/src/migration/migrator/package-json.ts +++ b/packages/cli/src/migration/migrator/package-json.ts @@ -24,7 +24,7 @@ import { } from '../migrator.ts'; import { BROWSER_PROVIDER_PEER_DEPS, - findDeclaredSpec, + hasProviderPeerDependency, resolveProviderPeerSpec, OPT_IN_BROWSER_PROVIDERS, OXLINT_PLUGINS_PACKAGE, @@ -280,8 +280,7 @@ export function rewritePackageJson( ); } const peer = BROWSER_PROVIDER_PEER_DEPS[provider]; // 'webdriverio' / 'playwright' - const peerPresent = findDeclaredSpec(pkg, peer); - if (peer && !peerPresent) { + if (!hasProviderPeerDependency(pkg, peer)) { pkg.devDependencies ??= {}; pkg.devDependencies[peer] = resolveProviderPeerSpec( pkg, diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts index 7ab78a21c7..5e8361a567 100644 --- a/packages/cli/src/migration/migrator/shared.ts +++ b/packages/cli/src/migration/migrator/shared.ts @@ -82,10 +82,14 @@ export const BROWSER_PROVIDER_PEER_DEPS: Record = { '@vitest/browser-webdriverio': 'webdriverio', }; +// Packages that include runtime peer as a dependency. +const RUNTIME_PEER_INCLUDED_BY: Record = { + playwright: ['@playwright/test'], +}; + // Lockstep sibling packages whose declared version a browser provider's runtime // framework peer should reuse (they publish together). Keyed by the peer name. export const PROVIDER_PEER_VERSION_SIBLINGS: Record = { - playwright: ['@playwright/test'], webdriverio: ['@wdio/cli', '@wdio/globals'], }; @@ -99,6 +103,17 @@ export function findDeclaredSpec(pkg: DependencyBag, name: string): string | und ); } +// A provider's runtime peer is available when declared directly or included by +// another declared package. +export function hasProviderPeerDependency(pkg: DependencyBag, runtimePeer: string): boolean { + return ( + findDeclaredSpec(pkg, runtimePeer) !== undefined || + (RUNTIME_PEER_INCLUDED_BY[runtimePeer] ?? []).some( + (packageName) => findDeclaredSpec(pkg, packageName) !== undefined, + ) + ); +} + // A deterministic spec for a browser provider's framework peer instead of `*`: // reference the catalog when it already owns the peer, otherwise reuse a declared // lockstep sibling's version (concrete, or a catalog reference resolved to its diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts index 4a327f91e9..9459496c95 100644 --- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts +++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts @@ -55,6 +55,7 @@ import { import { type DependencyVersionChange, type MigrationReport } from '../report.ts'; import { BROWSER_PROVIDER_PEER_DEPS, + hasProviderPeerDependency, resolveProviderPeerSpec, OPT_IN_BROWSER_PROVIDERS, REMOVE_PACKAGES, @@ -405,10 +406,7 @@ function reconcileVitePlusBootstrapPackage( ); } const frameworkPeer = BROWSER_PROVIDER_PEER_DEPS[provider]; - const frameworkPresent = dependencyGroups.some( - (dependencies) => dependencies?.[frameworkPeer] !== undefined, - ); - if (frameworkPeer && !frameworkPresent) { + if (!hasProviderPeerDependency(pkg, frameworkPeer)) { pkg.devDependencies ??= {}; pkg.devDependencies[frameworkPeer] = resolveProviderPeerSpec( pkg,