Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitaf08a63

Browse files
committed
feat(@angular/build): add experimental vitest unit-testing support
When using the application build system via the `@angular/build` package(default for new projects starting in v20), a new experimental unit-testbuilder is available that initially uses vitest. This experimental systemis intended to provide support for investigation of future unit testingefforts within the Angular CLI. As this is experimental, no SemVer guaranteesare provided, the API and behavior may change, and there may be unexpectedbehavior. Available test runners may be added or removed as well.The setup is somewhat different than the previous unit-testing builders.It uses a similar mechanism to that of the `dev-server` and requires a`buildTarget` option. This allows the code building aspects of the unit-testing process to leverage pre-existing option values that are alreadydefined for development. If differing option values are required for testing,an additional build target configuration specifically for testing can beused.The current vitest support has multiple caveats including but not limited to:* No watch support* `jsdom` based testing only (`jsdom` must be installed in the project)* Custom vitest configuration is not supportedAn example configuration that would replace the `test` target for a projectis as follows:```"test": { "builder": "@angular/build:unit-test", "options": { "tsConfig": "tsconfig.spec.json", "buildTarget": "::development", "runner": "vitest" }}```
1 parentc706d50 commitaf08a63

File tree

13 files changed

+1145
-3
lines changed

13 files changed

+1145
-3
lines changed

‎modules/testing/builder/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ts_project(
2020
# Needed at runtime by some builder tests relying on SSR being
2121
# resolvable in the test project.
2222
":node_modules/@angular/ssr",
23+
":node_modules/vitest",
2324
]+glob(["projects/**/*"]),
2425
deps= [
2526
":node_modules/@angular-devkit/architect",

‎modules/testing/builder/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"@angular-devkit/architect":"workspace:*",
55
"@angular/ssr":"workspace:*",
66
"@angular-devkit/build-angular":"workspace:*",
7-
"rxjs":"7.8.2"
7+
"rxjs":"7.8.2",
8+
"vitest":"3.1.1"
89
}
910
}

‎packages/angular/build/BUILD.bazel

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ ts_json_schema(
3434
src="src/builders/ng-packagr/schema.json",
3535
)
3636

37+
ts_json_schema(
38+
name="unit_test_schema",
39+
src="src/builders/unit-test/schema.json",
40+
)
41+
3742
copy_to_bin(
3843
name="schemas",
3944
srcs=glob(["**/schema.json"]),
@@ -70,6 +75,7 @@ ts_project(
7075
"//packages/angular/build:src/builders/extract-i18n/schema.ts",
7176
"//packages/angular/build:src/builders/karma/schema.ts",
7277
"//packages/angular/build:src/builders/ng-packagr/schema.ts",
78+
"//packages/angular/build:src/builders/unit-test/schema.ts",
7379
],
7480
data=RUNTIME_ASSETS,
7581
module_name="@angular/build",
@@ -101,6 +107,7 @@ ts_project(
101107
":node_modules/source-map-support",
102108
":node_modules/tinyglobby",
103109
":node_modules/vite",
110+
":node_modules/vitest",
104111
":node_modules/watchpack",
105112
"//:node_modules/@angular/common",
106113
"//:node_modules/@angular/compiler",
@@ -244,6 +251,36 @@ ts_project(
244251
],
245252
)
246253

254+
ts_project(
255+
name="unit-test_integration_test_lib",
256+
testonly=True,
257+
srcs=glob(include= ["src/builders/unit-test/tests/**/*.ts"]),
258+
deps= [
259+
":build",
260+
"//packages/angular/build/private",
261+
"//modules/testing/builder",
262+
":node_modules/@angular-devkit/architect",
263+
":node_modules/@angular-devkit/core",
264+
"//:node_modules/@types/node",
265+
266+
# unit test specific test deps
267+
":node_modules/vitest",
268+
":node_modules/jsdom",
269+
270+
# Base dependencies for the hello-world-app.
271+
"//:node_modules/@angular/common",
272+
"//:node_modules/@angular/compiler",
273+
"//:node_modules/@angular/compiler-cli",
274+
"//:node_modules/@angular/core",
275+
"//:node_modules/@angular/platform-browser",
276+
"//:node_modules/@angular/router",
277+
":node_modules/rxjs",
278+
"//:node_modules/tslib",
279+
"//:node_modules/typescript",
280+
"//:node_modules/zone.js",
281+
],
282+
)
283+
247284
jasmine_test(
248285
name="application_integration_tests",
249286
size="large",
@@ -273,6 +310,13 @@ jasmine_test(
273310
shard_count=10,
274311
)
275312

313+
jasmine_test(
314+
name="unit-test_integration_tests",
315+
size="large",
316+
data= [":unit-test_integration_test_lib"],
317+
shard_count=10,
318+
)
319+
276320
genrule(
277321
name="license",
278322
srcs= ["//:LICENSE"],

‎packages/angular/build/builders.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"implementation":"./src/builders/ng-packagr/index",
2525
"schema":"./src/builders/ng-packagr/schema.json",
2626
"description":"Build a library with ng-packagr."
27+
},
28+
"unit-test": {
29+
"implementation":"./src/builders/unit-test",
30+
"schema":"./src/builders/unit-test/schema.json",
31+
"description":"[EXPERIMENTAL] Run application unit tests."
2732
}
2833
}
2934
}

‎packages/angular/build/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@
5151
"devDependencies": {
5252
"@angular/ssr":"workspace:*",
5353
"@angular-devkit/core":"workspace:*",
54+
"jsdom":"26.1.0",
5455
"less":"4.3.0",
5556
"ng-packagr":"20.0.0-next.6",
5657
"postcss":"8.5.3",
57-
"rxjs":"7.8.2"
58+
"rxjs":"7.8.2",
59+
"vitest":"3.1.1"
5860
},
5961
"peerDependencies": {
6062
"@angular/core":"0.0.0-ANGULAR-FW-PEER-DEP",
@@ -71,7 +73,8 @@
7173
"postcss":"^8.4.0",
7274
"tailwindcss":"^2.0.0 || ^3.0.0 || ^4.0.0",
7375
"tslib":"^2.3.0",
74-
"typescript":">=5.8 <5.9"
76+
"typescript":">=5.8 <5.9",
77+
"vitest":"^3.1.1"
7578
},
7679
"peerDependenciesMeta": {
7780
"@angular/core": {
@@ -106,6 +109,9 @@
106109
},
107110
"tailwindcss": {
108111
"optional":true
112+
},
113+
"vitest": {
114+
"optional":true
109115
}
110116
}
111117
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
importtype{BuilderContext,BuilderOutput}from'@angular-devkit/architect';
10+
importassertfrom'node:assert';
11+
import{randomUUID}from'node:crypto';
12+
importpathfrom'node:path';
13+
import{loadEsmModule}from'../../utils/load-esm';
14+
import{buildApplicationInternal}from'../application';
15+
importtype{
16+
ApplicationBuilderExtensions,
17+
ApplicationBuilderInternalOptions,
18+
}from'../application/options';
19+
import{ResultKind}from'../application/results';
20+
import{OutputHashing}from'../application/schema';
21+
import{writeTestFiles}from'../karma/application_builder';
22+
import{findTests,getTestEntrypoints}from'../karma/find-tests';
23+
import{normalizeOptions}from'./options';
24+
importtype{SchemaasUnitTestOptions}from'./schema';
25+
26+
exporttype{UnitTestOptions};
27+
28+
/**
29+
*@experimental Direct usage of this function is considered experimental.
30+
*/
31+
exportasyncfunction*execute(
32+
options:UnitTestOptions,
33+
context:BuilderContext,
34+
extensions:ApplicationBuilderExtensions={},
35+
):AsyncIterable<BuilderOutput>{
36+
// Determine project name from builder context target
37+
constprojectName=context.target?.project;
38+
if(!projectName){
39+
context.logger.error(
40+
`The "${context.builder.builderName}" builder requires a target to be specified.`,
41+
);
42+
43+
return;
44+
}
45+
46+
context.logger.warn(
47+
`NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`,
48+
);
49+
50+
constnormalizedOptions=awaitnormalizeOptions(context,projectName,options);
51+
const{ projectSourceRoot, workspaceRoot, runnerName}=normalizedOptions;
52+
53+
if(runnerName!=='vitest'){
54+
context.logger.error('Unknown test runner: '+runnerName);
55+
56+
return;
57+
}
58+
59+
// Find test files
60+
consttestFiles=awaitfindTests(
61+
normalizedOptions.include,
62+
normalizedOptions.exclude,
63+
workspaceRoot,
64+
projectSourceRoot,
65+
);
66+
67+
if(testFiles.length===0){
68+
context.logger.error('No tests found.');
69+
70+
return{success:false};
71+
}
72+
73+
constentryPoints=getTestEntrypoints(testFiles,{ projectSourceRoot, workspaceRoot});
74+
entryPoints.set(
75+
'init-testbed',
76+
path.join(__dirname,'..','karma','polyfills','init_test_bed.js'),
77+
);
78+
79+
const{ startVitest}=awaitloadEsmModule<typeofimport('vitest/node')>('vitest/node');
80+
81+
// Setup test file build options based on application build target options
82+
constbuildTargetOptions=(awaitcontext.validateOptions(
83+
awaitcontext.getTargetOptions(normalizedOptions.buildTarget),
84+
awaitcontext.getBuilderNameForTarget(normalizedOptions.buildTarget),
85+
))asunknownasApplicationBuilderInternalOptions;
86+
87+
if(buildTargetOptions.polyfills?.includes('zone.js')){
88+
buildTargetOptions.polyfills.push('zone.js/testing');
89+
}
90+
91+
constoutputPath=path.join(context.workspaceRoot,'dist/test-out',randomUUID());
92+
constbuildOptions:ApplicationBuilderInternalOptions={
93+
...buildTargetOptions,
94+
watch:normalizedOptions.watch,
95+
outputPath,
96+
index:false,
97+
browser:undefined,
98+
server:undefined,
99+
localize:false,
100+
budgets:[],
101+
serviceWorker:false,
102+
appShell:false,
103+
ssr:false,
104+
prerender:false,
105+
sourceMap:{scripts:true,vendor:false,styles:false},
106+
outputHashing:OutputHashing.None,
107+
optimization:false,
108+
tsConfig:normalizedOptions.tsConfig,
109+
entryPoints,
110+
externalDependencies:['vitest', ...(buildTargetOptions.externalDependencies??[])],
111+
};
112+
113+
letinstance:import('vitest/node').Vitest|undefined;
114+
115+
forawait(constresultofbuildApplicationInternal(buildOptions,context,extensions)){
116+
if(result.kind===ResultKind.Failure){
117+
continue;
118+
}elseif(result.kind!==ResultKind.Full){
119+
assert.fail('A full build result is required from the application builder.');
120+
}
121+
122+
assert(result.files,'Builder did not provide result files.');
123+
124+
awaitwriteTestFiles(result.files,outputPath);
125+
126+
constsetupFiles=['init-testbed.js'];
127+
if(buildTargetOptions?.polyfills?.length){
128+
setupFiles.push('polyfills.js');
129+
}
130+
131+
instance??=awaitstartVitest(
132+
'test',
133+
undefined/* cliFilters */,
134+
undefined/* options */,
135+
{
136+
test:{
137+
root:outputPath,
138+
setupFiles,
139+
environment:'jsdom',
140+
watch:normalizedOptions.watch,
141+
reporters:normalizedOptions.reporters,
142+
coverage:{
143+
enabled:normalizedOptions.codeCoverage,
144+
exclude:normalizedOptions.codeCoverageExclude,
145+
excludeAfterRemap:true,
146+
},
147+
},
148+
},
149+
{},
150+
);
151+
152+
// Check if all the tests pass to calculate the result
153+
consttestModules=instance.state.getTestModules();
154+
155+
yield{success:testModules.every((testModule)=>testModule.ok())};
156+
}
157+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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{typeBuilder,createBuilder}from'@angular-devkit/architect';
10+
import{typeUnitTestOptions,execute}from'./builder';
11+
12+
export{typeUnitTestOptions,execute};
13+
14+
constbuilder:Builder<UnitTestOptions>=createBuilder(execute);
15+
16+
exportdefaultbuilder;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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{typeBuilderContext,targetFromTargetString}from'@angular-devkit/architect';
10+
importpathfrom'node:path';
11+
import{normalizeCacheOptions}from'../../utils/normalize-cache';
12+
importtype{SchemaasUnitTestOptions}from'./schema';
13+
14+
exporttypeNormalizedUnitTestOptions=Awaited<ReturnType<typeofnormalizeOptions>>;
15+
16+
exportasyncfunctionnormalizeOptions(
17+
context:BuilderContext,
18+
projectName:string,
19+
options:UnitTestOptions,
20+
){
21+
// Setup base paths based on workspace root and project information
22+
constworkspaceRoot=context.workspaceRoot;
23+
constprojectMetadata=awaitcontext.getProjectMetadata(projectName);
24+
constprojectRoot=normalizeDirectoryPath(
25+
path.join(workspaceRoot,(projectMetadata.rootasstring|undefined)??''),
26+
);
27+
constprojectSourceRoot=normalizeDirectoryPath(
28+
path.join(workspaceRoot,(projectMetadata.sourceRootasstring|undefined)??'src'),
29+
);
30+
31+
// Gather persistent caching option and provide a project specific cache location
32+
constcacheOptions=normalizeCacheOptions(projectMetadata,workspaceRoot);
33+
cacheOptions.path=path.join(cacheOptions.path,projectName);
34+
35+
// Target specifier defaults to the current project's build target using a development configuration
36+
constbuildTargetSpecifier=options.buildTarget??`::development`;
37+
constbuildTarget=targetFromTargetString(buildTargetSpecifier,projectName,'build');
38+
39+
const{ codeCoverage, codeCoverageExclude, tsConfig, runner, runnerConfig, reporters, browsers}=
40+
options;
41+
42+
return{
43+
// Project/workspace information
44+
workspaceRoot,
45+
projectRoot,
46+
projectSourceRoot,
47+
cacheOptions,
48+
// Target/configuration specified options
49+
buildTarget,
50+
include:options.include??['**/*.spec.ts'],
51+
exclude:options.exclude??[],
52+
runnerName:runner,
53+
runnerConfig,
54+
codeCoverage,
55+
codeCoverageExclude,
56+
tsConfig,
57+
reporters,
58+
browsers,
59+
// TODO: Implement watch support
60+
watch:false,
61+
};
62+
}
63+
64+
/**
65+
* Normalize a directory path string.
66+
* Currently only removes a trailing slash if present.
67+
*@param path A path string.
68+
*@returns A normalized path string.
69+
*/
70+
functionnormalizeDirectoryPath(path:string):string{
71+
constlast=path[path.length-1];
72+
if(last==='/'||last==='\\'){
73+
returnpath.slice(0,-1);
74+
}
75+
76+
returnpath;
77+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp