Skip to content

Commit f80db6f

Browse files
committed
feat(@schematics/angular): add ng-add support for Vitest browser providers
This commit adds a new internal schematic `vitest-browser` to `@schematics/angular` which streamlines the setup of Vitest browser testing by handling `ng add` for the following packages: - `@vitest/browser-playwright` - `@vitest/browser-webdriverio` - `@vitest/browser-preview` The schematic performs the following actions: - Verifies the project is using the `@angular/build:unit-test` builder. - Updates `tsconfig.spec.json` to include `vitest/globals` and the respective browser provider package in `compilerOptions.types`. - Installs the requested package along with necessary peer dependencies (e.g., `playwright` or `webdriverio`). Additionally, the `ng add` command implementation in the CLI has been updated to support passing the package name to built-in schematics, allowing the `vitest-browser` schematic to know which package was requested.
1 parent 66ffafd commit f80db6f

File tree

9 files changed

+180
-2
lines changed

9 files changed

+180
-2
lines changed

packages/angular/cli/src/commands/add/cli.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ const BUILT_IN_SCHEMATICS = {
8484
collection: '@schematics/angular',
8585
name: 'tailwind',
8686
},
87+
'@vitest/browser-playwright': {
88+
collection: '@schematics/angular',
89+
name: 'vitest-browser',
90+
},
91+
'@vitest/browser-webdriverio': {
92+
collection: '@schematics/angular',
93+
name: 'vitest-browser',
94+
},
95+
'@vitest/browser-preview': {
96+
collection: '@schematics/angular',
97+
name: 'vitest-browser',
98+
},
8799
} as const;
88100

89101
export default class AddCommandModule
@@ -260,6 +272,7 @@ export default class AddCommandModule
260272
...options,
261273
collection: builtInSchematic.collection,
262274
schematicName: builtInSchematic.name,
275+
package: packageName,
263276
});
264277
}
265278
}

packages/schematics/angular/collection.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@
149149
"schema": "./refactor/jasmine-vitest/schema.json",
150150
"description": "[EXPERIMENTAL] Refactors Jasmine tests to use Vitest APIs.",
151151
"hidden": true
152+
},
153+
"vitest-browser": {
154+
"factory": "./vitest-browser",
155+
"schema": "./vitest-browser/schema.json",
156+
"hidden": true,
157+
"private": true,
158+
"description": "[INTERNAL] Adds a Vitest browser provider to a project. Intended for use for ng add."
152159
}
153160
}
154161
}

packages/schematics/angular/tailwind/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"$source": "projectName"
1111
}
1212
},
13+
"package": {
14+
"type": "string",
15+
"description": "The package to be added."
16+
},
1317
"skipInstall": {
1418
"description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.",
1519
"type": "boolean",

packages/schematics/angular/utility/latest-versions/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"ts-node": "~10.9.0",
2727
"typescript": "~5.9.2",
2828
"vitest": "^4.0.8",
29+
"@vitest/browser-playwright": "^4.0.8",
30+
"@vitest/browser-webdriverio": "^4.0.8",
31+
"@vitest/browser-preview": "^4.0.8",
32+
"playwright": "^1.48.0",
33+
"webdriverio": "^9.0.0",
2934
"zone.js": "~0.16.0"
3035
}
3136
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Rule,
11+
SchematicContext,
12+
SchematicsException,
13+
Tree,
14+
chain,
15+
} from '@angular-devkit/schematics';
16+
import { join } from 'node:path/posix';
17+
import {
18+
DependencyType,
19+
ExistingBehavior,
20+
InstallBehavior,
21+
addDependency,
22+
} from '../utility/dependency';
23+
import { JSONFile } from '../utility/json-file';
24+
import { latestVersions } from '../utility/latest-versions';
25+
import { getWorkspace } from '../utility/workspace';
26+
import { Builders } from '../utility/workspace-models';
27+
import { Schema as VitestBrowserOptions } from './schema';
28+
29+
export default function (options: VitestBrowserOptions): Rule {
30+
return async (host: Tree, _context: SchematicContext) => {
31+
const workspace = await getWorkspace(host);
32+
const project = workspace.projects.get(options.project);
33+
34+
if (!project) {
35+
throw new SchematicsException(`Project "${options.project}" does not exist.`);
36+
}
37+
38+
const testTarget = project.targets.get('test');
39+
if (testTarget?.builder !== Builders.BuildUnitTest) {
40+
throw new SchematicsException(
41+
`Project "${options.project}" does not have a "test" target with a supported builder.`,
42+
);
43+
}
44+
45+
if (testTarget.options?.['runner'] === 'karma') {
46+
throw new SchematicsException(
47+
`Project "${options.project}" is configured to use Karma. ` +
48+
'Please migrate to Vitest before adding browser testing support.',
49+
);
50+
}
51+
52+
const packageName = options.package;
53+
if (!packageName) {
54+
return;
55+
}
56+
57+
const dependencies = [packageName];
58+
if (packageName === '@vitest/browser-playwright') {
59+
dependencies.push('playwright');
60+
} else if (packageName === '@vitest/browser-webdriverio') {
61+
dependencies.push('webdriverio');
62+
}
63+
64+
// Update tsconfig.spec.json
65+
const tsConfigPath =
66+
(testTarget.options?.['tsConfig'] as string | undefined) ??
67+
join(project.root, 'tsconfig.spec.json');
68+
const updateTsConfigRule: Rule = (host) => {
69+
if (host.exists(tsConfigPath)) {
70+
const json = new JSONFile(host, tsConfigPath);
71+
const typesPath = ['compilerOptions', 'types'];
72+
const existingTypes = (json.get(typesPath) as string[] | undefined) ?? [];
73+
const newTypes = existingTypes.filter((t) => t !== 'jasmine');
74+
75+
if (!newTypes.includes('vitest/globals')) {
76+
newTypes.push('vitest/globals');
77+
}
78+
79+
if (packageName && !newTypes.includes(packageName)) {
80+
newTypes.push(packageName);
81+
}
82+
83+
if (
84+
newTypes.length !== existingTypes.length ||
85+
newTypes.some((t, i) => t !== existingTypes[i])
86+
) {
87+
json.modify(typesPath, newTypes);
88+
}
89+
}
90+
};
91+
92+
return chain([
93+
updateTsConfigRule,
94+
...dependencies.map((name) =>
95+
addDependency(name, latestVersions[name], {
96+
type: DependencyType.Dev,
97+
existing: ExistingBehavior.Skip,
98+
install: options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto,
99+
}),
100+
),
101+
]);
102+
};
103+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "Vitest Browser Provider Schematic",
4+
"type": "object",
5+
"properties": {
6+
"project": {
7+
"type": "string",
8+
"description": "The name of the project.",
9+
"$default": {
10+
"$source": "projectName"
11+
}
12+
},
13+
"package": {
14+
"type": "string",
15+
"description": "The package to be added."
16+
},
17+
"skipInstall": {
18+
"description": "Skip the automatic installation of packages. You will need to manually install the dependencies later.",
19+
"type": "boolean",
20+
"default": false
21+
}
22+
},
23+
"required": ["project", "package"]
24+
}

tests/e2e.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ WEBPACK_IGNORE_TESTS = [
4747
"tests/build/app-shell/**",
4848
"tests/i18n/ivy-localize-app-shell.js",
4949
"tests/i18n/ivy-localize-app-shell-service-worker.js",
50+
"tests/commands/add/add-vitest-browser.js",
5051
"tests/commands/serve/ssr-http-requests-assets.js",
5152
"tests/build/styles/sass-pkg-importer.js",
5253
"tests/build/prerender/http-requests-assets.js",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expectFileToMatch } from '../../../utils/fs';
2+
import { uninstallPackage } from '../../../utils/packages';
3+
import { ng } from '../../../utils/process';
4+
import { applyVitestBuilder } from '../../../utils/vitest';
5+
6+
export default async function () {
7+
await applyVitestBuilder();
8+
9+
try {
10+
await ng('add', '@vitest/browser-playwright', '--skip-confirmation');
11+
12+
await expectFileToMatch('package.json', /"@vitest\/browser-playwright":/);
13+
await expectFileToMatch('package.json', /"playwright":/);
14+
await expectFileToMatch('tsconfig.spec.json', /"vitest\/globals"/);
15+
await expectFileToMatch('tsconfig.spec.json', /"@vitest\/browser-playwright"/);
16+
} finally {
17+
await uninstallPackage('@vitest/browser-playwright');
18+
await uninstallPackage('playwright');
19+
}
20+
}

tests/e2e/utils/vitest.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { silentNpm } from './process';
1+
import { installPackage } from './packages';
22
import { updateJsonFile } from './project';
33

44
/** Updates the `test` builder in the current workspace to use Vitest. */
55
export async function applyVitestBuilder(): Promise<void> {
66
// These deps matches the deps in `@schematics/angular`
7-
await silentNpm('install', 'vitest@^4.0.8', 'jsdom@^27.1.0', '--save-dev');
7+
await installPackage('vitest@^4.0.8');
8+
await installPackage('jsdom@^27.1.0');
89

910
await updateJsonFile('angular.json', (json) => {
1011
const projects = Object.values(json['projects']);

0 commit comments

Comments
 (0)