Skip to content

Commit 2ffc527

Browse files
clydinalan-agius4
authored andcommitted
feat(@schematics/angular): configure Vitest for new projects and allow runner choice
This commit updates the application, ng-new, and library schematics to configure Vitest as the default unit testing runner, replacing Karma and Jasmine. It also introduces a `testRunner` option to allow users to choose between `vitest` and `karma`. Key changes: Application & Ng-New Schematics: - Adds a `testRunner` option to allow choosing between `vitest` (default) and `karma`. - Sets "@angular/build:unit-test" as the builder for the "test" target when `vitest` is chosen, and "@angular/build:karma" for `karma`. - Conditionally adds dependencies based on the selected runner. - Updates "tsconfig.spec.json" to include "vitest/globals" or "jasmine" for type support. Library Schematic: - Conditionally configures the "test" target with the "@angular/build:unit-test" builder if Vitest is detected in the workspace. - Dynamically sets the "types" in "tsconfig.spec.json" to "vitest/globals" or "jasmine" based on the presence of Vitest.
1 parent f75128d commit 2ffc527

File tree

17 files changed

+217
-85
lines changed

17 files changed

+217
-85
lines changed

packages/schematics/angular/application/files/common-files/tsconfig.spec.json.template

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
55
"compilerOptions": {
66
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/spec",
7-
"types": [
8-
"jasmine"
9-
]
10-
},
7+
"types": [
8+
"<%= testRunner === 'vitest' ? 'vitest/globals' : 'jasmine' %>"
9+
] },
1110
"include": [
1211
"src/**/*.ts"
1312
]

packages/schematics/angular/application/index.ts

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export default function (options: ApplicationOptions): Rule {
132132
appName: options.name,
133133
folderName,
134134
suffix,
135+
testRunner: options.testRunner,
135136
}),
136137
move(appDir),
137138
]),
@@ -183,6 +184,65 @@ function addDependenciesToPackageJson(options: ApplicationOptions): Rule {
183184
);
184185
}
185186

187+
if (!options.skipTests) {
188+
if (options.testRunner === 'vitest') {
189+
rules.push(
190+
addDependency('vitest', latestVersions['vitest'], {
191+
type: DependencyType.Dev,
192+
existing: ExistingBehavior.Skip,
193+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
194+
}),
195+
addDependency('jsdom', latestVersions['jsdom'], {
196+
type: DependencyType.Dev,
197+
existing: ExistingBehavior.Skip,
198+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
199+
}),
200+
);
201+
} else {
202+
rules.push(
203+
addDependency('karma', latestVersions['karma'], {
204+
type: DependencyType.Dev,
205+
existing: ExistingBehavior.Skip,
206+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
207+
}),
208+
addDependency('karma-chrome-launcher', latestVersions['karma-chrome-launcher'], {
209+
type: DependencyType.Dev,
210+
existing: ExistingBehavior.Skip,
211+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
212+
}),
213+
addDependency('karma-coverage', latestVersions['karma-coverage'], {
214+
type: DependencyType.Dev,
215+
existing: ExistingBehavior.Skip,
216+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
217+
}),
218+
addDependency('karma-jasmine', latestVersions['karma-jasmine'], {
219+
type: DependencyType.Dev,
220+
existing: ExistingBehavior.Skip,
221+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
222+
}),
223+
addDependency(
224+
'karma-jasmine-html-reporter',
225+
latestVersions['karma-jasmine-html-reporter'],
226+
{
227+
type: DependencyType.Dev,
228+
existing: ExistingBehavior.Skip,
229+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
230+
},
231+
),
232+
addDependency('jasmine-core', latestVersions['jasmine-core'], {
233+
type: DependencyType.Dev,
234+
existing: ExistingBehavior.Skip,
235+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
236+
}),
237+
addDependency('@types/jasmine', latestVersions['@types/jasmine'], {
238+
type: DependencyType.Dev,
239+
existing: ExistingBehavior.Skip,
240+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
241+
}),
242+
);
243+
}
244+
}
245+
186246
return chain(rules);
187247
}
188248

@@ -327,18 +387,24 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul
327387
},
328388
},
329389
},
330-
test: options.minimal
331-
? undefined
332-
: {
333-
builder: Builders.BuildKarma,
334-
options: {
335-
polyfills: options.zoneless ? undefined : ['zone.js', 'zone.js/testing'],
336-
tsConfig: `${projectRoot}tsconfig.spec.json`,
337-
inlineStyleLanguage,
338-
assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }],
339-
styles: [`${sourceRoot}/styles.${options.style}`],
340-
},
341-
},
390+
test:
391+
options.skipTests || options.minimal
392+
? undefined
393+
: options.testRunner === 'vitest'
394+
? {
395+
builder: Builders.BuildUnitTest,
396+
options: {},
397+
}
398+
: {
399+
builder: Builders.BuildKarma,
400+
options: {
401+
polyfills: options.zoneless ? undefined : ['zone.js', 'zone.js/testing'],
402+
tsConfig: `${projectRoot}tsconfig.spec.json`,
403+
inlineStyleLanguage,
404+
assets: [{ 'glob': '**/*', 'input': `${projectRoot}public` }],
405+
styles: [`${sourceRoot}/styles.${options.style}`],
406+
},
407+
},
342408
},
343409
};
344410

packages/schematics/angular/application/index_spec.ts

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ describe('Application Schematic', () => {
110110
expect(_extends).toBe('../../tsconfig.json');
111111
});
112112

113+
it('should set the right types in the tsconfig.spec.json when testRunner is karma', async () => {
114+
const tree = await schematicRunner.runSchematic(
115+
'application',
116+
{ ...defaultOptions, testRunner: 'karma' },
117+
workspaceTree,
118+
);
119+
120+
const {
121+
compilerOptions: { types },
122+
} = readJsonFile(tree, '/projects/foo/tsconfig.spec.json');
123+
expect(types).toEqual(['jasmine']);
124+
});
125+
113126
it('should add project references in the root tsconfig.json', async () => {
114127
const tree = await schematicRunner.runSchematic('application', defaultOptions, workspaceTree);
115128

@@ -324,6 +337,30 @@ describe('Application Schematic', () => {
324337
expect(pkg.dependencies['zone.js']).toBeUndefined();
325338
});
326339

340+
it('should add karma dependencies when testRunner is karma', async () => {
341+
const tree = await schematicRunner.runSchematic(
342+
'application',
343+
{
344+
...defaultOptions,
345+
testRunner: 'karma',
346+
},
347+
workspaceTree,
348+
);
349+
350+
const pkg = JSON.parse(tree.readContent('/package.json'));
351+
expect(pkg.devDependencies['karma']).toEqual(latestVersions['karma']);
352+
expect(pkg.devDependencies['karma-chrome-launcher']).toEqual(
353+
latestVersions['karma-chrome-launcher'],
354+
);
355+
expect(pkg.devDependencies['karma-coverage']).toEqual(latestVersions['karma-coverage']);
356+
expect(pkg.devDependencies['karma-jasmine']).toEqual(latestVersions['karma-jasmine']);
357+
expect(pkg.devDependencies['karma-jasmine-html-reporter']).toEqual(
358+
latestVersions['karma-jasmine-html-reporter'],
359+
);
360+
expect(pkg.devDependencies['jasmine-core']).toEqual(latestVersions['jasmine-core']);
361+
expect(pkg.devDependencies['@types/jasmine']).toEqual(latestVersions['@types/jasmine']);
362+
});
363+
327364
it(`should not override existing users dependencies`, async () => {
328365
const oldPackageJson = workspaceTree.readContent('package.json');
329366
workspaceTree.overwrite(
@@ -391,12 +428,6 @@ describe('Application Schematic', () => {
391428
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'public' }]);
392429
expect(buildOpt.polyfills).toBeUndefined();
393430
expect(buildOpt.tsConfig).toEqual('tsconfig.app.json');
394-
395-
const testOpt = prj.architect.test.options;
396-
expect(testOpt.tsConfig).toEqual('tsconfig.spec.json');
397-
expect(testOpt.karmaConfig).toBeUndefined();
398-
expect(testOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'public' }]);
399-
expect(testOpt.styles).toEqual(['src/styles.css']);
400431
});
401432

402433
it('should set values in angular.json correctly when using a style preprocessor', async () => {
@@ -407,51 +438,20 @@ describe('Application Schematic', () => {
407438
const prj = config.projects.foo;
408439
const buildOpt = prj.architect.build.options;
409440
expect(buildOpt.styles).toEqual(['src/styles.sass']);
410-
const testOpt = prj.architect.test.options;
411-
expect(testOpt.styles).toEqual(['src/styles.sass']);
412441
expect(tree.exists('src/styles.sass')).toBe(true);
413442
});
414443

415-
it('sets "inlineStyleLanguage" in angular.json when using a style preprocessor', async () => {
416-
const options = { ...defaultOptions, projectRoot: '', style: Style.Sass };
417-
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
418-
419-
const config = JSON.parse(tree.readContent('/angular.json'));
420-
const prj = config.projects.foo;
421-
422-
const buildOpt = prj.architect.build.options;
423-
expect(buildOpt.inlineStyleLanguage).toBe('sass');
424-
425-
const testOpt = prj.architect.test.options;
426-
expect(testOpt.inlineStyleLanguage).toBe('sass');
427-
});
428-
429-
it('does not set "inlineStyleLanguage" in angular.json when not using a style preprocessor', async () => {
430-
const options = { ...defaultOptions, projectRoot: '' };
444+
it('should set values in angular.json correctly when testRunner is karma', async () => {
445+
const options = { ...defaultOptions, projectRoot: '', testRunner: 'karma' as const };
431446
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
432447

433448
const config = JSON.parse(tree.readContent('/angular.json'));
434449
const prj = config.projects.foo;
435-
436-
const buildOpt = prj.architect.build.options;
437-
expect(buildOpt.inlineStyleLanguage).toBeUndefined();
438-
439-
const testOpt = prj.architect.test.options;
440-
expect(testOpt.inlineStyleLanguage).toBeUndefined();
441-
});
442-
443-
it('does not set "inlineStyleLanguage" in angular.json when using CSS styles', async () => {
444-
const options = { ...defaultOptions, projectRoot: '', style: Style.Css };
445-
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);
446-
447-
const config = JSON.parse(tree.readContent('/angular.json'));
448-
const prj = config.projects.foo;
449-
450-
const buildOpt = prj.architect.build.options;
451-
expect(buildOpt.inlineStyleLanguage).toBeUndefined();
452-
453-
const testOpt = prj.architect.test.options;
454-
expect(testOpt.inlineStyleLanguage).toBeUndefined();
450+
const testOpt = prj.architect.test;
451+
expect(testOpt.builder).toEqual('@angular/build:karma');
452+
expect(testOpt.options.tsConfig).toEqual('tsconfig.spec.json');
453+
expect(testOpt.options.assets).toEqual([{ glob: '**/*', input: 'public' }]);
454+
expect(testOpt.options.styles).toEqual(['src/styles.css']);
455455
});
456456

457457
it('should set the relative tsconfig paths', async () => {
@@ -482,12 +482,6 @@ describe('Application Schematic', () => {
482482
expect(buildOpt.tsConfig).toEqual('foo/tsconfig.app.json');
483483
expect(buildOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'foo/public' }]);
484484

485-
const testOpt = project.architect.test.options;
486-
expect(testOpt.tsConfig).toEqual('foo/tsconfig.spec.json');
487-
expect(testOpt.karmaConfig).toBeUndefined();
488-
expect(testOpt.assets).toEqual([{ 'glob': '**/*', 'input': 'foo/public' }]);
489-
expect(testOpt.styles).toEqual(['foo/src/styles.css']);
490-
491485
const appTsConfig = readJsonFile(tree, '/foo/tsconfig.app.json');
492486
expect(appTsConfig.extends).toEqual('../tsconfig.json');
493487
const specTsConfig = readJsonFile(tree, '/foo/tsconfig.spec.json');

packages/schematics/angular/application/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
"default": false,
9090
"alias": "S"
9191
},
92+
"testRunner": {
93+
"description": "The unit testing runner to use.",
94+
"type": "string",
95+
"enum": ["vitest", "karma"],
96+
"default": "vitest"
97+
},
9298
"skipPackageJson": {
9399
"type": "boolean",
94100
"default": false,

packages/schematics/angular/config/index_spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ describe('Config Schematic', () => {
5050
defaultAppOptions,
5151
workspaceTree,
5252
);
53+
54+
// Set builder to a karma builder for testing purposes
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
const angularJson = applicationTree.readJson('angular.json') as any;
57+
angularJson['projects']['foo']['architect']['test']['builder'] = '@angular/build:karma';
58+
applicationTree.overwrite('angular.json', JSON.stringify(angularJson));
5359
});
5460

5561
describe(`when 'type' is 'karma'`, () => {

packages/schematics/angular/library/files/tsconfig.spec.json.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"compilerOptions": {
66
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/spec",
77
"types": [
8-
"jasmine"
8+
"<%= testTypesPackage %>"
99
]
1010
},
1111
"include": [

packages/schematics/angular/library/index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ function addLibToWorkspaceFile(
9191
projectRoot: string,
9292
projectName: string,
9393
hasZoneDependency: boolean,
94+
hasVitest: boolean,
9495
): Rule {
9596
return updateWorkspace((workspace) => {
9697
workspace.projects.add({
@@ -112,13 +113,20 @@ function addLibToWorkspaceFile(
112113
},
113114
},
114115
},
115-
test: {
116-
builder: Builders.BuildKarma,
117-
options: {
118-
tsConfig: `${projectRoot}/tsconfig.spec.json`,
119-
polyfills: hasZoneDependency ? ['zone.js', 'zone.js/testing'] : undefined,
120-
},
121-
},
116+
test: hasVitest
117+
? {
118+
builder: Builders.BuildUnitTest,
119+
options: {
120+
tsConfig: `${projectRoot}/tsconfig.spec.json`,
121+
},
122+
}
123+
: {
124+
builder: Builders.BuildKarma,
125+
options: {
126+
tsConfig: `${projectRoot}/tsconfig.spec.json`,
127+
polyfills: hasZoneDependency ? ['zone.js', 'zone.js/testing'] : undefined,
128+
},
129+
},
122130
},
123131
});
124132
});
@@ -150,6 +158,7 @@ export default function (options: LibraryOptions): Rule {
150158

151159
const distRoot = `dist/${folderName}`;
152160
const sourceDir = `${libDir}/src/lib`;
161+
const hasVitest = getDependency(host, 'vitest') !== null;
153162

154163
const templateSource = apply(url('./files'), [
155164
applyTemplates({
@@ -163,6 +172,7 @@ export default function (options: LibraryOptions): Rule {
163172
angularLatestVersion: latestVersions.Angular.replace(/~|\^/, ''),
164173
tsLibLatestVersion: latestVersions['tslib'].replace(/~|\^/, ''),
165174
folderName,
175+
testTypesPackage: hasVitest ? 'vitest/globals' : 'jasmine',
166176
}),
167177
move(libDir),
168178
]);
@@ -171,7 +181,7 @@ export default function (options: LibraryOptions): Rule {
171181

172182
return chain([
173183
mergeWith(templateSource),
174-
addLibToWorkspaceFile(options, libDir, packageName, hasZoneDependency),
184+
addLibToWorkspaceFile(options, libDir, packageName, hasZoneDependency, hasVitest),
175185
options.skipPackageJson ? noop() : addDependenciesToPackageJson(!!options.skipInstall),
176186
options.skipTsConfig ? noop() : updateTsConfig(packageName, './' + distRoot),
177187
options.skipTsConfig

packages/schematics/angular/library/index_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,17 @@ describe('Library Schematic', () => {
414414
expect(workspace.projects.foo.architect.test.builder).toBe('@angular/build:karma');
415415
});
416416

417+
it(`should add 'unit-test' test builder`, async () => {
418+
const packageJson = getJsonFileContent(workspaceTree, 'package.json');
419+
packageJson['devDependencies']['vitest'] = '^4.0.0';
420+
workspaceTree.overwrite('package.json', JSON.stringify(packageJson));
421+
422+
const tree = await schematicRunner.runSchematic('library', defaultOptions, workspaceTree);
423+
424+
const workspace = JSON.parse(tree.readContent('/angular.json'));
425+
expect(workspace.projects.foo.architect.test.builder).toBe('@angular/build:unit-test');
426+
});
427+
417428
describe('standalone=false', () => {
418429
const defaultNonStandaloneOptions = { ...defaultOptions, standalone: false };
419430

0 commit comments

Comments
 (0)