diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 295c65ba93ca..95c8380b0817 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -396,13 +396,9 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul options: {}, } : { - builder: Builders.BuildKarma, + builder: Builders.BuildUnitTest, options: { - polyfills: options.zoneless ? undefined : ['zone.js', 'zone.js/testing'], - tsConfig: `${projectRoot}tsconfig.spec.json`, - inlineStyleLanguage, - assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }], - styles: [`${sourceRoot}/styles.${options.style}`], + runner: 'karma', }, }, }, diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index e1867b24cf41..5c56e2ca7a4d 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -448,10 +448,11 @@ describe('Application Schematic', () => { const config = JSON.parse(tree.readContent('/angular.json')); const prj = config.projects.foo; const testOpt = prj.architect.test; - expect(testOpt.builder).toEqual('@angular/build:karma'); - expect(testOpt.options.tsConfig).toEqual('tsconfig.spec.json'); - expect(testOpt.options.assets).toEqual([{ glob: '**/*', input: 'public' }]); - expect(testOpt.options.styles).toEqual(['src/styles.css']); + expect(testOpt.builder).toEqual('@angular/build:unit-test'); + expect(testOpt.options.runner).toEqual('karma'); + expect(testOpt.options.tsConfig).toBeUndefined(); + expect(testOpt.options.assets).toBeUndefined(); + expect(testOpt.options.styles).toBeUndefined(); }); it('should set the relative tsconfig paths', async () => { diff --git a/packages/schematics/angular/config/index.ts b/packages/schematics/angular/config/index.ts index 209ac0de91f9..6bfd36f41b84 100644 --- a/packages/schematics/angular/config/index.ts +++ b/packages/schematics/angular/config/index.ts @@ -47,49 +47,88 @@ async function addBrowserslistConfig(projectRoot: string): Promise { } function addKarmaConfig(options: ConfigOptions): Rule { - return updateWorkspace((workspace) => { - const project = workspace.projects.get(options.project); - if (!project) { - throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`); - } + return (_, context) => + updateWorkspace((workspace) => { + const project = workspace.projects.get(options.project); + if (!project) { + throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`); + } - const testTarget = project.targets.get('test'); - if (!testTarget) { - throw new SchematicsException( - `No "test" target found for project "${options.project}".` + - ' A "test" target is required to generate a karma configuration.', - ); - } + const testTarget = project.targets.get('test'); + if (!testTarget) { + throw new SchematicsException( + `No "test" target found for project "${options.project}".` + + ' A "test" target is required to generate a karma configuration.', + ); + } - if ( - testTarget.builder !== AngularBuilder.Karma && - testTarget.builder !== AngularBuilder.BuildKarma - ) { - throw new SchematicsException( - `Cannot add a karma configuration as builder for "test" target in project does not` + - ` use "${AngularBuilder.Karma}" or "${AngularBuilder.BuildKarma}".`, - ); - } + if ( + testTarget.builder !== AngularBuilder.Karma && + testTarget.builder !== AngularBuilder.BuildKarma && + testTarget.builder !== AngularBuilder.BuildUnitTest + ) { + throw new SchematicsException( + `Cannot add a karma configuration as builder for "test" target in project does not` + + ` use "${AngularBuilder.Karma}", "${AngularBuilder.BuildKarma}", or ${AngularBuilder.BuildUnitTest}.`, + ); + } + + testTarget.options ??= {}; + if (testTarget.builder !== AngularBuilder.BuildUnitTest) { + testTarget.options.karmaConfig = path.join(project.root, 'karma.conf.js'); + } else { + // `unit-test` uses the `runnerConfig` option which has configuration discovery if enabled + testTarget.options.runnerConfig = true; - testTarget.options ??= {}; - testTarget.options.karmaConfig = path.join(project.root, 'karma.conf.js'); + let isKarmaRunnerConfigured = false; + // Check runner option + if (testTarget.options.runner) { + if (testTarget.options.runner === 'karma') { + isKarmaRunnerConfigured = true; + } else { + context.logger.warn( + `The "test" target is configured to use a runner other than "karma" in the main options.` + + ' The generated "karma.conf.js" file may not be used.', + ); + } + } - // If scoped project (i.e. "@foo/bar"), convert dir to "foo/bar". - let folderName = options.project.startsWith('@') ? options.project.slice(1) : options.project; - if (/[A-Z]/.test(folderName)) { - folderName = strings.dasherize(folderName); - } + for (const [name, config] of Object.entries(testTarget.configurations ?? {})) { + if (config && typeof config === 'object' && 'runner' in config) { + if (config.runner !== 'karma') { + context.logger.warn( + `The "test" target's "${name}" configuration is configured to use a runner other than "karma".` + + ' The generated "karma.conf.js" file may not be used for that configuration.', + ); + } else { + isKarmaRunnerConfigured = true; + } + } + } - return mergeWith( - apply(url('https://github.com/files'), [ - filter((p) => p.endsWith('karma.conf.js.template')), - applyTemplates({ - relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(project.root), - folderName, - needDevkitPlugin: testTarget.builder === AngularBuilder.Karma, - }), - move(project.root), - ]), - ); - }); + if (!isKarmaRunnerConfigured) { + context.logger.warn( + `The "test" target is not explicitly configured to use the "karma" runner.` + + ' The generated "karma.conf.js" file may not be used as the default runner is "vitest".', + ); + } + } + // If scoped project (i.e. "@foo/bar"), convert dir to "foo/bar". + let folderName = options.project.startsWith('@') ? options.project.slice(1) : options.project; + if (/[A-Z]/.test(folderName)) { + folderName = strings.dasherize(folderName); + } + + return mergeWith( + apply(url('https://github.com/files'), [ + filter((p) => p.endsWith('karma.conf.js.template')), + applyTemplates({ + relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(project.root), + folderName, + needDevkitPlugin: testTarget.builder === AngularBuilder.Karma, + }), + move(project.root), + ]), + ); + }); } diff --git a/packages/schematics/angular/config/index_spec.ts b/packages/schematics/angular/config/index_spec.ts index 1bbc08d9e85b..a63a58ab6887 100644 --- a/packages/schematics/angular/config/index_spec.ts +++ b/packages/schematics/angular/config/index_spec.ts @@ -95,6 +95,88 @@ describe('Config Schematic', () => { const { karmaConfig } = prj.architect.test.options; expect(karmaConfig).toBe('projects/foo/karma.conf.js'); }); + + describe('with "unit-test" builder', () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.builder = '@angular/build:unit-test'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + }); + + it(`should not set 'karmaConfig' in test builder`, async () => { + const tree = await runConfigSchematic(ConfigType.Karma); + const config = JSON.parse(tree.readContent('/angular.json')); + const prj = config.projects.foo; + const { karmaConfig } = prj.architect.test.options; + expect(karmaConfig).toBeUndefined(); + }); + + it(`should warn when 'runner' is not specified`, async () => { + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Karma); + expect( + logs.some((v) => v.includes('may not be used as the default runner is "vitest"')), + ).toBeTrue(); + }); + + it(`should warn when 'runner' is 'vitest' in options`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.options ??= {}; + angularJson.projects.foo.architect.test.options.runner = 'vitest'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Karma); + expect( + logs.some((v) => v.includes('runner other than "karma" in the main options')), + ).toBeTrue(); + }); + + it(`should warn when 'runner' is 'vitest' in a configuration`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.configurations ??= {}; + angularJson.projects.foo.architect.test.configurations.ci = { runner: 'vitest' }; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Karma); + expect( + logs.some((v) => v.includes(`"ci" configuration is configured to use a runner`)), + ).toBeTrue(); + }); + + it(`should not warn when 'runner' is 'karma' in options`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.options ??= {}; + angularJson.projects.foo.architect.test.options.runner = 'karma'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Karma); + expect(logs.length).toBe(0); + }); + + it(`should not warn when 'runner' is 'karma' in a configuration`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.configurations ??= {}; + angularJson.projects.foo.architect.test.configurations.ci = { runner: 'karma' }; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Karma); + expect(logs.length).toBe(0); + }); + }); }); describe(`when 'type' is 'browserslist'`, () => { diff --git a/packages/schematics/angular/ng-new/index_spec.ts b/packages/schematics/angular/ng-new/index_spec.ts index 20306b875a9a..7f136908c747 100644 --- a/packages/schematics/angular/ng-new/index_spec.ts +++ b/packages/schematics/angular/ng-new/index_spec.ts @@ -169,7 +169,8 @@ describe('Ng New Schematic', () => { }, }, } = JSON.parse(tree.readContent('/bar/angular.json')); - expect(test.builder).toBe('@angular/build:karma'); + expect(test.builder).toBe('@angular/build:unit-test'); + expect(test.options).toEqual({ runner: 'karma' }); const { devDependencies } = JSON.parse(tree.readContent('/bar/package.json')); expect(devDependencies['karma']).toBeDefined(); diff --git a/tests/legacy-cli/e2e/initialize/500-create-project.ts b/tests/legacy-cli/e2e/initialize/500-create-project.ts index c6c0f8ef243d..b48c8733a9a5 100644 --- a/tests/legacy-cli/e2e/initialize/500-create-project.ts +++ b/tests/legacy-cli/e2e/initialize/500-create-project.ts @@ -64,6 +64,9 @@ export default async function () { const test = json['projects']['test-project']['architect']['test']; test.builder = '@angular-devkit/build-angular:karma'; + test.options ??= {}; + test.options.tsConfig = 'tsconfig.spec.json'; + delete test.options.runner; }); await updateJsonFile('tsconfig.json', (tsconfig) => { delete tsconfig.compilerOptions.esModuleInterop; diff --git a/tests/legacy-cli/e2e/tests/basic/test.ts b/tests/legacy-cli/e2e/tests/basic/test.ts index d9066946ae8e..50580581d442 100644 --- a/tests/legacy-cli/e2e/tests/basic/test.ts +++ b/tests/legacy-cli/e2e/tests/basic/test.ts @@ -1,5 +1,6 @@ import { ng } from '../../utils/process'; import { writeMultipleFiles } from '../../utils/fs'; +import { getGlobalVariable } from '../../utils/env'; export default async function () { // make sure both --watch=false work @@ -48,5 +49,11 @@ export default async function () { `, }); - await ng('test', '--watch=false', '--karma-config=karma.conf.bis.js'); + const isWebpack = !getGlobalVariable('argv')['esbuild']; + + if (isWebpack) { + await ng('test', '--watch=false', '--karma-config=karma.conf.bis.js'); + } else { + await ng('test', '--watch=false', '--runner-config=karma.conf.bis.js'); + } } diff --git a/tests/legacy-cli/e2e/tests/test/test-code-coverage-exclude.ts b/tests/legacy-cli/e2e/tests/test/test-code-coverage-exclude.ts index 3533e6c8e9a9..808bcb59e2d9 100644 --- a/tests/legacy-cli/e2e/tests/test/test-code-coverage-exclude.ts +++ b/tests/legacy-cli/e2e/tests/test/test-code-coverage-exclude.ts @@ -1,10 +1,14 @@ +import { getGlobalVariable } from '../../utils/env'; import { expectFileToExist, rimraf } from '../../utils/fs'; import { silentNg } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; + const coverageOptionName = isWebpack ? '--code-coverage' : '--coverage'; + // This test is already in build-angular, but that doesn't run on Windows. - await silentNg('test', '--no-watch', '--code-coverage'); + await silentNg('test', '--no-watch', coverageOptionName); await expectFileToExist('coverage/test-project/app.ts.html'); // Delete coverage directory await rimraf('coverage'); @@ -12,8 +16,8 @@ export default async function () { await silentNg( 'test', '--no-watch', - '--code-coverage', - `--code-coverage-exclude='src/**/app.ts'`, + coverageOptionName, + `${coverageOptionName}-exclude='src/**/app.ts'`, ); // Doesn't include excluded. diff --git a/tests/legacy-cli/e2e/tests/test/test-environment.ts b/tests/legacy-cli/e2e/tests/test/test-environment.ts index 1a4dadaa4317..1670cb246521 100644 --- a/tests/legacy-cli/e2e/tests/test/test-environment.ts +++ b/tests/legacy-cli/e2e/tests/test/test-environment.ts @@ -1,21 +1,23 @@ import { ng } from '../../utils/process'; import { writeFile, writeMultipleFiles } from '../../utils/fs'; import { updateJsonFile } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; -export default function () { // Tests run in 'dev' environment by default. - return ( - writeMultipleFiles({ - 'src/environment.prod.ts': ` + await writeMultipleFiles({ + 'src/environment.prod.ts': ` export const environment = { production: true };`, - 'src/environment.ts': ` + 'src/environment.ts': ` export const environment = { production: false }; `, - 'src/app/environment.spec.ts': ` + 'src/app/environment.spec.ts': ` import { environment } from '../environment'; describe('Test environment', () => { @@ -24,29 +26,33 @@ export default function () { }); }); `, - }) - .then(() => ng('test', '--watch=false')) - .then(() => - updateJsonFile('angular.json', (configJson) => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.test.configurations = { - production: { - fileReplacements: [ - { - replace: 'src/environment.ts', - with: 'src/environment.prod.ts', - }, - ], - }, - }; - }), - ) - - // Tests can run in different environment. - .then(() => - writeFile( - 'src/app/environment.spec.ts', - ` + }); + + await ng('test', '--watch=false'); + + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + appArchitect[isWebpack ? 'test' : 'build'].configurations = { + production: { + fileReplacements: [ + { + replace: 'src/environment.ts', + with: 'src/environment.prod.ts', + }, + ], + }, + }; + if (!isWebpack) { + appArchitect.test.options ??= {}; + appArchitect.test.options.buildTarget = '::production'; + } + }); + + // Tests can run in different environment. + + await writeFile( + 'src/app/environment.spec.ts', + ` import { environment } from '../environment'; describe('Test environment', () => { @@ -55,8 +61,11 @@ export default function () { }); }); `, - ), - ) - .then(() => ng('test', '--configuration=production', '--watch=false')) ); + + if (isWebpack) { + await ng('test', '--watch=false', '--configuration=production'); + } else { + await ng('test', '--watch=false'); + } } diff --git a/tests/legacy-cli/e2e/tests/test/test-scripts.ts b/tests/legacy-cli/e2e/tests/test/test-scripts.ts index acbcc66dc230..1537cdddf349 100644 --- a/tests/legacy-cli/e2e/tests/test/test-scripts.ts +++ b/tests/legacy-cli/e2e/tests/test/test-scripts.ts @@ -1,3 +1,4 @@ +import { getGlobalVariable } from '../../utils/env'; import { writeMultipleFiles } from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; @@ -60,9 +61,10 @@ export default async function () { // should fail because the global scripts were not added to scripts array await expectToFail(() => ng('test', '--watch=false')); + const isWebpack = !getGlobalVariable('argv')['esbuild']; await updateJsonFile('angular.json', (workspaceJson) => { const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.test.options.scripts = [ + appArchitect[isWebpack ? 'test' : 'build'].options.scripts = [ { input: 'src/string-script.js' }, { input: 'src/input-script.js' }, ]; diff --git a/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts b/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts index 9e2a8e3f36fa..6c1cf16cd7b3 100644 --- a/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts +++ b/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts @@ -1,10 +1,13 @@ import assert from 'node:assert'; -import { getGlobalVariable } from '../../utils/env'; import { writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { assertIsError } from '../../utils/utils'; +import { updateJsonFile } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; + await writeFile( 'src/app/app.spec.ts', ` @@ -15,8 +18,16 @@ export default async function () { ); // when sourcemaps are 'on' the stacktrace will point to the spec.ts file. + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + if (isWebpack) { + appArchitect['test'].options.sourceMap = true; + } else { + appArchitect['build'].configurations.development.sourceMap = true; + } + }); try { - await ng('test', '--no-watch', '--source-map'); + await ng('test', '--no-watch'); throw new Error('ng test should have failed.'); } catch (error) { assertIsError(error); @@ -25,8 +36,16 @@ export default async function () { } // when sourcemaps are 'off' the stacktrace won't point to the spec.ts file. + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + if (isWebpack) { + appArchitect['test'].options.sourceMap = false; + } else { + appArchitect['build'].configurations.development.sourceMap = false; + } + }); try { - await ng('test', '--no-watch', '--no-source-map'); + await ng('test', '--no-watch'); throw new Error('ng test should have failed.'); } catch (error) { assertIsError(error); diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index ea764bb20314..c84b7fce8e1d 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -191,7 +191,11 @@ export async function useCIChrome(projectName: string, projectDir = ''): Promise return updateJsonFile('angular.json', (workspaceJson) => { const project = workspaceJson.projects[projectName]; const appTargets = project.targets || project.architect; - appTargets.test.options.browsers = 'ChromeHeadlessNoSandbox'; + if (appTargets.test.builder === '@angular/build:unit-test') { + appTargets.test.options.browsers = ['ChromeHeadlessNoSandbox']; + } else { + appTargets.test.options.browsers = 'ChromeHeadlessNoSandbox'; + } }); }