diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index ac3d1b4e9652..8fa5b14eda59 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -12,7 +12,7 @@ import path from 'node:path'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { getProjectRootPaths } from '../../utils/project-metadata'; import { isTTY } from '../../utils/tty'; -import type { Schema as UnitTestBuilderOptions } from './schema'; +import { Runner, type Schema as UnitTestBuilderOptions } from './schema'; export type NormalizedUnitTestBuilderOptions = Awaited>; @@ -56,7 +56,7 @@ export async function normalizeOptions( const { runner, browsers, progress, filter, browserViewport, ui, runnerConfig } = options; - if (ui && runner !== 'vitest') { + if (ui && runner !== Runner.Vitest) { throw new Error('The "ui" option is only available for the "vitest" runner.'); } @@ -95,7 +95,7 @@ export async function normalizeOptions( include: options.include ?? ['**/*.spec.ts'], exclude: options.exclude, filter, - runnerName: runner ?? 'vitest', + runnerName: runner ?? Runner.Vitest, coverage: { enabled: options.coverage, exclude: options.coverageExclude, diff --git a/packages/angular/build/src/builders/unit-test/tests/setup.ts b/packages/angular/build/src/builders/unit-test/tests/setup.ts index db03c2b0b348..e6770e115789 100644 --- a/packages/angular/build/src/builders/unit-test/tests/setup.ts +++ b/packages/angular/build/src/builders/unit-test/tests/setup.ts @@ -14,7 +14,7 @@ import { ApplicationBuilderOptions as ApplicationSchema, buildApplication, } from '../../../builders/application'; -import { Schema } from '../schema'; +import { Runner, Schema } from '../schema'; // TODO: Consider using package.json imports field instead of relative path // after the switch to rules_js. @@ -61,7 +61,7 @@ export const UNIT_TEST_BUILDER_INFO = Object.freeze({ export const BASE_OPTIONS = Object.freeze({ buildTarget: 'test:build', tsConfig: 'src/tsconfig.spec.json', - runner: 'vitest' as any, + runner: Runner.Vitest, }); /** diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 53ddd53acad0..e84a40530032 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -23,6 +23,7 @@ import { url, } from '@angular-devkit/schematics'; import { Schema as ComponentOptions, Style as ComponentStyle } from '../component/schema'; +import { addTestRunnerDependencies } from '../utility/dependencies'; import { DependencyType, ExistingBehavior, @@ -34,7 +35,7 @@ import { latestVersions } from '../utility/latest-versions'; import { relativePathToWorkspaceRoot } from '../utility/paths'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders, ProjectType } from '../utility/workspace-models'; -import { Schema as ApplicationOptions, Style } from './schema'; +import { Schema as ApplicationOptions, Style, TestRunner } from './schema'; const APPLICATION_DEV_DEPENDENCIES = [ { name: '@angular/compiler-cli', version: latestVersions.Angular }, @@ -187,62 +188,7 @@ function addDependenciesToPackageJson(options: ApplicationOptions): Rule { } if (!options.skipTests) { - if (options.testRunner === 'vitest') { - rules.push( - addDependency('vitest', latestVersions['vitest'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency('jsdom', latestVersions['jsdom'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - ); - } else { - rules.push( - addDependency('karma', latestVersions['karma'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency('karma-chrome-launcher', latestVersions['karma-chrome-launcher'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency('karma-coverage', latestVersions['karma-coverage'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency('karma-jasmine', latestVersions['karma-jasmine'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency( - 'karma-jasmine-html-reporter', - latestVersions['karma-jasmine-html-reporter'], - { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }, - ), - addDependency('jasmine-core', latestVersions['jasmine-core'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - addDependency('@types/jasmine', latestVersions['@types/jasmine'], { - type: DependencyType.Dev, - existing: ExistingBehavior.Skip, - install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto, - }), - ); - } + rules.push(...addTestRunnerDependencies(options.testRunner, !!options.skipInstall)); } return chain(rules); @@ -392,17 +338,15 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul test: options.skipTests || options.minimal ? undefined - : options.testRunner === 'vitest' - ? { - builder: Builders.BuildUnitTest, - options: {}, - } - : { - builder: Builders.BuildUnitTest, - options: { - runner: 'karma', - }, - }, + : { + builder: Builders.BuildUnitTest, + options: + options.testRunner === TestRunner.Vitest + ? {} + : { + runner: 'karma', + }, + }, }, }; diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 4d95be8b5ea1..c2f91d110f27 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -10,7 +10,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/te import { parse as parseJson } from 'jsonc-parser'; import { latestVersions } from '../utility/latest-versions'; import { Schema as WorkspaceOptions } from '../workspace/schema'; -import { Schema as ApplicationOptions, Style, ViewEncapsulation } from './schema'; +import { Schema as ApplicationOptions, Style, TestRunner, ViewEncapsulation } from './schema'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function readJsonFile(tree: UnitTestTree, path: string): any { @@ -442,7 +442,7 @@ describe('Application Schematic', () => { }); it('should set values in angular.json correctly when testRunner is karma', async () => { - const options = { ...defaultOptions, projectRoot: '', testRunner: 'karma' as const }; + const options = { ...defaultOptions, projectRoot: '', testRunner: TestRunner.Karma }; const tree = await schematicRunner.runSchematic('application', options, workspaceTree); const config = JSON.parse(tree.readContent('/angular.json')); diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index b02e35b27758..3069664c02d7 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -20,6 +20,7 @@ import { url, } from '@angular-devkit/schematics'; import { join } from 'node:path/posix'; +import { addTestRunnerDependencies } from '../utility/dependencies'; import { DependencyType, ExistingBehavior, @@ -32,7 +33,7 @@ import { latestVersions } from '../utility/latest-versions'; import { relativePathToWorkspaceRoot } from '../utility/paths'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; import { Builders, ProjectType } from '../utility/workspace-models'; -import { Schema as LibraryOptions } from './schema'; +import { Schema as LibraryOptions, TestRunner } from './schema'; const LIBRARY_DEV_DEPENDENCIES = [ { name: '@angular/compiler-cli', version: latestVersions.Angular }, @@ -69,7 +70,7 @@ function addTsProjectReference(...paths: string[]) { }; } -function addDependenciesToPackageJson(skipInstall: boolean): Rule { +function addDependenciesToPackageJson({ skipInstall, testRunner }: LibraryOptions): Rule { return chain([ ...LIBRARY_DEV_DEPENDENCIES.map((dependency) => addDependency(dependency.name, dependency.version, { @@ -78,6 +79,7 @@ function addDependenciesToPackageJson(skipInstall: boolean): Rule { install: skipInstall ? InstallBehavior.None : InstallBehavior.Auto, }), ), + ...addTestRunnerDependencies(testRunner, !!skipInstall), addDependency('tslib', latestVersions['tslib'], { type: DependencyType.Default, existing: ExistingBehavior.Skip, @@ -91,7 +93,6 @@ function addLibToWorkspaceFile( projectRoot: string, projectName: string, hasZoneDependency: boolean, - hasVitest: boolean, ): Rule { return updateWorkspace((workspace) => { workspace.projects.add({ @@ -113,20 +114,21 @@ function addLibToWorkspaceFile( }, }, }, - test: hasVitest - ? { - builder: Builders.BuildUnitTest, - options: { - tsConfig: `${projectRoot}/tsconfig.spec.json`, + test: + options.testRunner === TestRunner.Vitest + ? { + builder: Builders.BuildUnitTest, + options: { + tsConfig: `${projectRoot}/tsconfig.spec.json`, + }, + } + : { + builder: Builders.BuildKarma, + options: { + tsConfig: `${projectRoot}/tsconfig.spec.json`, + polyfills: hasZoneDependency ? ['zone.js', 'zone.js/testing'] : undefined, + }, }, - } - : { - builder: Builders.BuildKarma, - options: { - tsConfig: `${projectRoot}/tsconfig.spec.json`, - polyfills: hasZoneDependency ? ['zone.js', 'zone.js/testing'] : undefined, - }, - }, }, }); }); @@ -158,7 +160,6 @@ export default function (options: LibraryOptions): Rule { const distRoot = `dist/${folderName}`; const sourceDir = `${libDir}/src/lib`; - const hasVitest = getDependency(host, 'vitest') !== null; const templateSource = apply(url('./files'), [ applyTemplates({ @@ -172,7 +173,7 @@ export default function (options: LibraryOptions): Rule { angularLatestVersion: latestVersions.Angular.replace(/~|\^/, ''), tsLibLatestVersion: latestVersions['tslib'].replace(/~|\^/, ''), folderName, - testTypesPackage: hasVitest ? 'vitest/globals' : 'jasmine', + testTypesPackage: options.testRunner === TestRunner.Vitest ? 'vitest/globals' : 'jasmine', }), move(libDir), ]); @@ -181,8 +182,8 @@ export default function (options: LibraryOptions): Rule { return chain([ mergeWith(templateSource), - addLibToWorkspaceFile(options, libDir, packageName, hasZoneDependency, hasVitest), - options.skipPackageJson ? noop() : addDependenciesToPackageJson(!!options.skipInstall), + addLibToWorkspaceFile(options, libDir, packageName, hasZoneDependency), + options.skipPackageJson ? noop() : addDependenciesToPackageJson(options), options.skipTsConfig ? noop() : updateTsConfig(packageName, './' + distRoot), options.skipTsConfig ? noop() diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index 319abbaa5162..bf4f8714294e 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -407,11 +407,11 @@ describe('Library Schematic', () => { expect(workspace.projects.foo.architect.build.builder).toBe('@angular/build:ng-packagr'); }); - it(`should add 'karma' test builder`, async () => { + it(`should add 'unit-test' test builder`, async () => { const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree); const workspace = JSON.parse(tree.readContent('/angular.json')); - expect(workspace.projects.foo.architect.test.builder).toBe('@angular/build:karma'); + expect(workspace.projects.foo.architect.test.builder).toBe('@angular/build:unit-test'); }); it(`should add 'unit-test' test builder`, async () => { diff --git a/packages/schematics/angular/library/schema.json b/packages/schematics/angular/library/schema.json index 62ffdbb422a0..bb3d227e5245 100644 --- a/packages/schematics/angular/library/schema.json +++ b/packages/schematics/angular/library/schema.json @@ -53,6 +53,12 @@ "type": "boolean", "default": true, "x-user-analytics": "ep.ng_standalone" + }, + "testRunner": { + "description": "The unit testing runner to use.", + "type": "string", + "enum": ["vitest", "karma"], + "default": "vitest" } }, "required": ["name"] diff --git a/packages/schematics/angular/ng-new/index.ts b/packages/schematics/angular/ng-new/index.ts index 7fca64d69ce3..856343e82b8f 100644 --- a/packages/schematics/angular/ng-new/index.ts +++ b/packages/schematics/angular/ng-new/index.ts @@ -25,7 +25,7 @@ import { import { Schema as ApplicationOptions } from '../application/schema'; import { JSONFile } from '../utility/json-file'; import { Schema as WorkspaceOptions } from '../workspace/schema'; -import { Schema as NgNewOptions } from './schema'; +import { Schema as NgNewOptions, TestRunner } from './schema'; export default function (options: NgNewOptions): Rule { if (!options.directory) { @@ -67,16 +67,21 @@ export default function (options: NgNewOptions): Rule { mergeWith( apply(empty(), [ schematic('workspace', workspaceOptions), - options.createApplication ? schematic('application', applicationOptions) : noop, - schematic('ai-config', { - tool: options.aiConfig?.length ? options.aiConfig : undefined, - }), (tree: Tree) => { - if (options.testRunner === 'karma') { + if (options.testRunner === TestRunner.Karma) { const file = new JSONFile(tree, 'angular.json'); - file.modify(['schematics', '@schematics/angular:application', 'testRunner'], 'karma'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const schematics = file.get(['schematics']) ?? ({} as any); + (schematics['@schematics/angular:application'] ??= {}).testRunner = TestRunner.Karma; + (schematics['@schematics/angular:library'] ??= {}).testRunner = TestRunner.Karma; + + file.modify(['schematics'], schematics); } }, + options.createApplication ? schematic('application', applicationOptions) : noop, + schematic('ai-config', { + tool: options.aiConfig?.length ? options.aiConfig : undefined, + }), move(options.directory), ]), ), diff --git a/packages/schematics/angular/ng-new/index_spec.ts b/packages/schematics/angular/ng-new/index_spec.ts index 7f136908c747..28e1c13f315b 100644 --- a/packages/schematics/angular/ng-new/index_spec.ts +++ b/packages/schematics/angular/ng-new/index_spec.ts @@ -7,7 +7,7 @@ */ import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; -import { Schema as NgNewOptions } from './schema'; +import { Schema as NgNewOptions, TestRunner } from './schema'; describe('Ng New Schematic', () => { const schematicRunner = new SchematicTestRunner( @@ -159,7 +159,7 @@ describe('Ng New Schematic', () => { }); it(`should set 'testRunner' to 'karma'`, async () => { - const options = { ...defaultOptions, testRunner: 'karma' as const }; + const options = { ...defaultOptions, testRunner: TestRunner.Karma }; const tree = await schematicRunner.runSchematic('ng-new', options); const { @@ -178,11 +178,12 @@ describe('Ng New Schematic', () => { }); it(`should set 'testRunner' to 'karma' in workspace schematic options`, async () => { - const options = { ...defaultOptions, testRunner: 'karma' as const }; + const options = { ...defaultOptions, testRunner: TestRunner.Karma }; const tree = await schematicRunner.runSchematic('ng-new', options); const { schematics } = JSON.parse(tree.readContent('/bar/angular.json')); expect(schematics['@schematics/angular:application'].testRunner).toBe('karma'); + expect(schematics['@schematics/angular:library'].testRunner).toBe('karma'); }); it(`should not add type to class name when file name style guide is '2016'`, async () => { diff --git a/packages/schematics/angular/utility/dependencies.ts b/packages/schematics/angular/utility/dependencies.ts index 06c4f38653bd..deb78ca9c0e2 100644 --- a/packages/schematics/angular/utility/dependencies.ts +++ b/packages/schematics/angular/utility/dependencies.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Tree } from '@angular-devkit/schematics'; +import { Rule, Tree } from '@angular-devkit/schematics'; +import { TestRunner } from '../ng-new/schema'; +import { DependencyType, ExistingBehavior, InstallBehavior, addDependency } from './dependency'; import { JSONFile } from './json-file'; +import { latestVersions } from './latest-versions'; const PKG_JSON_PATH = '/package.json'; export enum NodeDependencyType { @@ -78,3 +81,29 @@ export function getPackageJsonDependency( return null; } + +export function addTestRunnerDependencies( + testRunner: TestRunner | undefined, + skipInstall: boolean, +): Rule[] { + const dependencies = + testRunner === TestRunner.Vitest + ? ['vitest', 'jsdom'] + : [ + 'karma', + 'karma-chrome-launcher', + 'karma-coverage', + 'karma-jasmine', + 'karma-jasmine-html-reporter', + 'jasmine-core', + '@types/jasmine', + ]; + + return dependencies.map((name) => + addDependency(name, latestVersions[name], { + type: DependencyType.Dev, + existing: ExistingBehavior.Skip, + install: skipInstall ? InstallBehavior.None : InstallBehavior.Auto, + }), + ); +}