Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/angular/cli/src/commands/add/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -260,6 +272,7 @@ export default class AddCommandModule
...options,
collection: builtInSchematic.collection,
schematicName: builtInSchematic.name,
package: packageName,
});
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/schematics/angular/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
4 changes: 4 additions & 0 deletions packages/schematics/angular/tailwind/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
103 changes: 103 additions & 0 deletions packages/schematics/angular/vitest-browser/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
),
]);
};
}
24 changes: 24 additions & 0 deletions packages/schematics/angular/vitest-browser/schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}
1 change: 1 addition & 0 deletions tests/e2e.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions tests/e2e/tests/commands/add/add-vitest-browser.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
5 changes: 3 additions & 2 deletions tests/e2e/utils/vitest.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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']);
Expand Down