diff --git a/packages/angular/cli/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts index 0dae016fba12..10361ece6b2b 100644 --- a/packages/angular/cli/src/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -84,6 +84,18 @@ const BUILT_IN_SCHEMATICS = { collection: '@schematics/angular', name: 'tailwind', }, + '@vitest/browser-playwright': { + collection: '@schematics/angular', + name: 'vitest-browser', + }, + '@vitest/browser-webdriverio': { + collection: '@schematics/angular', + name: 'vitest-browser', + }, + '@vitest/browser-preview': { + collection: '@schematics/angular', + name: 'vitest-browser', + }, } as const; export default class AddCommandModule @@ -260,6 +272,7 @@ export default class AddCommandModule ...options, collection: builtInSchematic.collection, schematicName: builtInSchematic.name, + package: packageName, }); } } diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index 8d876cf7cb5b..c275bc40144f 100755 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -149,6 +149,13 @@ "schema": "./refactor/jasmine-vitest/schema.json", "description": "[EXPERIMENTAL] Refactors Jasmine tests to use Vitest APIs.", "hidden": true + }, + "vitest-browser": { + "factory": "./vitest-browser", + "schema": "./vitest-browser/schema.json", + "hidden": true, + "private": true, + "description": "[INTERNAL] Adds a Vitest browser provider to a project. Intended for use for ng add." } } } diff --git a/packages/schematics/angular/tailwind/schema.json b/packages/schematics/angular/tailwind/schema.json index 3b52fc71c26d..801c6ebc45d7 100644 --- a/packages/schematics/angular/tailwind/schema.json +++ b/packages/schematics/angular/tailwind/schema.json @@ -10,6 +10,10 @@ "$source": "projectName" } }, + "package": { + "type": "string", + "description": "The package to be added." + }, "skipInstall": { "description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.", "type": "boolean", diff --git a/packages/schematics/angular/utility/latest-versions/package.json b/packages/schematics/angular/utility/latest-versions/package.json index 29ef3658f23b..0922fb6dc705 100644 --- a/packages/schematics/angular/utility/latest-versions/package.json +++ b/packages/schematics/angular/utility/latest-versions/package.json @@ -26,6 +26,11 @@ "ts-node": "~10.9.0", "typescript": "~5.9.2", "vitest": "^4.0.8", + "@vitest/browser-playwright": "^4.0.8", + "@vitest/browser-webdriverio": "^4.0.8", + "@vitest/browser-preview": "^4.0.8", + "playwright": "^1.48.0", + "webdriverio": "^9.0.0", "zone.js": "~0.16.0" } } diff --git a/packages/schematics/angular/vitest-browser/index.ts b/packages/schematics/angular/vitest-browser/index.ts new file mode 100644 index 000000000000..bdc366bf4423 --- /dev/null +++ b/packages/schematics/angular/vitest-browser/index.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + chain, +} from '@angular-devkit/schematics'; +import { join } from 'node:path/posix'; +import { + DependencyType, + ExistingBehavior, + InstallBehavior, + addDependency, +} from '../utility/dependency'; +import { JSONFile } from '../utility/json-file'; +import { latestVersions } from '../utility/latest-versions'; +import { getWorkspace } from '../utility/workspace'; +import { Builders } from '../utility/workspace-models'; +import { Schema as VitestBrowserOptions } from './schema'; + +export default function (options: VitestBrowserOptions): Rule { + return async (host: Tree, _context: SchematicContext) => { + const workspace = await getWorkspace(host); + const project = workspace.projects.get(options.project); + + if (!project) { + throw new SchematicsException(`Project "${options.project}" does not exist.`); + } + + const testTarget = project.targets.get('test'); + if (testTarget?.builder !== Builders.BuildUnitTest) { + throw new SchematicsException( + `Project "${options.project}" does not have a "test" target with a supported builder.`, + ); + } + + if (testTarget.options?.['runner'] === 'karma') { + throw new SchematicsException( + `Project "${options.project}" is configured to use Karma. ` + + 'Please migrate to Vitest before adding browser testing support.', + ); + } + + const packageName = options.package; + if (!packageName) { + return; + } + + const dependencies = [packageName]; + if (packageName === '@vitest/browser-playwright') { + dependencies.push('playwright'); + } else if (packageName === '@vitest/browser-webdriverio') { + dependencies.push('webdriverio'); + } + + // Update tsconfig.spec.json + const tsConfigPath = + (testTarget.options?.['tsConfig'] as string | undefined) ?? + join(project.root, 'tsconfig.spec.json'); + const updateTsConfigRule: Rule = (host) => { + if (host.exists(tsConfigPath)) { + const json = new JSONFile(host, tsConfigPath); + const typesPath = ['compilerOptions', 'types']; + const existingTypes = (json.get(typesPath) as string[] | undefined) ?? []; + const newTypes = existingTypes.filter((t) => t !== 'jasmine'); + + if (!newTypes.includes('vitest/globals')) { + newTypes.push('vitest/globals'); + } + + if (packageName && !newTypes.includes(packageName)) { + newTypes.push(packageName); + } + + if ( + newTypes.length !== existingTypes.length || + newTypes.some((t, i) => t !== existingTypes[i]) + ) { + json.modify(typesPath, newTypes); + } + } + }; + + return chain([ + updateTsConfigRule, + ...dependencies.map((name) => + addDependency(name, latestVersions[name], { + type: DependencyType.Dev, + existing: ExistingBehavior.Skip, + install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, + }), + ), + ]); + }; +} diff --git a/packages/schematics/angular/vitest-browser/schema.json b/packages/schematics/angular/vitest-browser/schema.json new file mode 100644 index 000000000000..6d65b6d0d5fe --- /dev/null +++ b/packages/schematics/angular/vitest-browser/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Vitest Browser Provider Schematic", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + }, + "package": { + "type": "string", + "description": "The package to be added." + }, + "skipInstall": { + "description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.", + "type": "boolean", + "default": false + } + }, + "required": ["project", "package"] +} diff --git a/tests/e2e.bzl b/tests/e2e.bzl index 07004fcb09d0..34ae2452a0fb 100644 --- a/tests/e2e.bzl +++ b/tests/e2e.bzl @@ -47,6 +47,7 @@ WEBPACK_IGNORE_TESTS = [ "tests/build/app-shell/**", "tests/i18n/ivy-localize-app-shell.js", "tests/i18n/ivy-localize-app-shell-service-worker.js", + "tests/commands/add/add-vitest-browser.js", "tests/commands/serve/ssr-http-requests-assets.js", "tests/build/styles/sass-pkg-importer.js", "tests/build/prerender/http-requests-assets.js", diff --git a/tests/e2e/tests/commands/add/add-vitest-browser.ts b/tests/e2e/tests/commands/add/add-vitest-browser.ts new file mode 100644 index 000000000000..4e7aaf1a4044 --- /dev/null +++ b/tests/e2e/tests/commands/add/add-vitest-browser.ts @@ -0,0 +1,20 @@ +import { expectFileToMatch } from '../../../utils/fs'; +import { uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { applyVitestBuilder } from '../../../utils/vitest'; + +export default async function () { + await applyVitestBuilder(); + + try { + await ng('add', '@vitest/browser-playwright', '--skip-confirmation'); + + await expectFileToMatch('package.json', /"@vitest\/browser-playwright":/); + await expectFileToMatch('package.json', /"playwright":/); + await expectFileToMatch('tsconfig.spec.json', /"vitest\/globals"/); + await expectFileToMatch('tsconfig.spec.json', /"@vitest\/browser-playwright"/); + } finally { + await uninstallPackage('@vitest/browser-playwright'); + await uninstallPackage('playwright'); + } +} diff --git a/tests/e2e/utils/vitest.ts b/tests/e2e/utils/vitest.ts index af40e66b18b1..471ca030169e 100644 --- a/tests/e2e/utils/vitest.ts +++ b/tests/e2e/utils/vitest.ts @@ -1,10 +1,11 @@ -import { silentNpm } from './process'; +import { installPackage } from './packages'; import { updateJsonFile } from './project'; /** Updates the `test` builder in the current workspace to use Vitest. */ export async function applyVitestBuilder(): Promise { // These deps matches the deps in `@schematics/angular` - await silentNpm('install', 'vitest@^4.0.8', 'jsdom@^27.1.0', '--save-dev'); + await installPackage('vitest@^4.0.8'); + await installPackage('jsdom@^27.1.0'); await updateJsonFile('angular.json', (json) => { const projects = Object.values(json['projects']);