-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathbuilder.ts
223 lines (189 loc) · 7.25 KB
/
builder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**
* @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 type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
import assert from 'node:assert';
import { randomUUID } from 'node:crypto';
import { createRequire } from 'node:module';
import path from 'node:path';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
import { loadEsmModule } from '../../utils/load-esm';
import { buildApplicationInternal } from '../application';
import type {
ApplicationBuilderExtensions,
ApplicationBuilderInternalOptions,
} from '../application/options';
import { ResultKind } from '../application/results';
import { OutputHashing } from '../application/schema';
import { writeTestFiles } from '../karma/application_builder';
import { findTests, getTestEntrypoints } from '../karma/find-tests';
import { useKarmaBuilder } from './karma-bridge';
import { normalizeOptions } from './options';
import type { Schema as UnitTestOptions } from './schema';
export type { UnitTestOptions };
/**
* @experimental Direct usage of this function is considered experimental.
*/
export async function* execute(
options: UnitTestOptions,
context: BuilderContext,
extensions: ApplicationBuilderExtensions = {},
): AsyncIterable<BuilderOutput> {
// Determine project name from builder context target
const projectName = context.target?.project;
if (!projectName) {
context.logger.error(
`The "${context.builder.builderName}" builder requires a target to be specified.`,
);
return;
}
context.logger.warn(
`NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`,
);
const normalizedOptions = await normalizeOptions(context, projectName, options);
const { projectSourceRoot, workspaceRoot, runnerName } = normalizedOptions;
// Translate options and use karma builder directly if specified
if (runnerName === 'karma') {
const karmaBridge = await useKarmaBuilder(context, normalizedOptions);
yield* karmaBridge;
return;
}
if (runnerName !== 'vitest') {
context.logger.error('Unknown test runner: ' + runnerName);
return;
}
// Find test files
const testFiles = await findTests(
normalizedOptions.include,
normalizedOptions.exclude,
workspaceRoot,
projectSourceRoot,
);
if (testFiles.length === 0) {
context.logger.error('No tests found.');
return { success: false };
}
const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot });
entryPoints.set('init-testbed', 'angular:test-bed-init');
const { startVitest } = await loadEsmModule<typeof import('vitest/node')>('vitest/node');
// Setup test file build options based on application build target options
const buildTargetOptions = (await context.validateOptions(
await context.getTargetOptions(normalizedOptions.buildTarget),
await context.getBuilderNameForTarget(normalizedOptions.buildTarget),
)) as unknown as ApplicationBuilderInternalOptions;
if (buildTargetOptions.polyfills?.includes('zone.js')) {
buildTargetOptions.polyfills.push('zone.js/testing');
}
const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID());
const buildOptions: ApplicationBuilderInternalOptions = {
...buildTargetOptions,
watch: normalizedOptions.watch,
outputPath,
index: false,
browser: undefined,
server: undefined,
localize: false,
budgets: [],
serviceWorker: false,
appShell: false,
ssr: false,
prerender: false,
sourceMap: { scripts: true, vendor: false, styles: false },
outputHashing: OutputHashing.None,
optimization: false,
tsConfig: normalizedOptions.tsConfig,
entryPoints,
externalDependencies: ['vitest', ...(buildTargetOptions.externalDependencies ?? [])],
};
extensions ??= {};
extensions.codePlugins ??= [];
const virtualTestBedInit = createVirtualModulePlugin({
namespace: 'angular:test-bed-init',
loadContent: async () => {
const contents: string[] = [
// Initialize the Angular testing environment
`import { getTestBed } from '@angular/core/testing';`,
`import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
`getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
` errorOnUnknownElements: true,`,
` errorOnUnknownProperties: true,`,
'});',
];
return {
contents: contents.join('\n'),
loader: 'js',
resolveDir: projectSourceRoot,
};
},
});
extensions.codePlugins.unshift(virtualTestBedInit);
let instance: import('vitest/node').Vitest | undefined;
// Setup vitest browser options if configured
let browser: import('vitest/node').BrowserConfigOptions | undefined;
if (normalizedOptions.browsers) {
const provider = findBrowserProvider(projectSourceRoot);
if (!provider) {
context.logger.error(
'The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' +
' Please install one of these packages and rerun the test command.',
);
return { success: false };
}
browser = {
enabled: true,
provider,
instances: normalizedOptions.browsers.map((browserName) => ({
browser: browserName,
})),
};
}
for await (const result of buildApplicationInternal(buildOptions, context, extensions)) {
if (result.kind === ResultKind.Failure) {
continue;
} else if (result.kind !== ResultKind.Full) {
assert.fail('A full build result is required from the application builder.');
}
assert(result.files, 'Builder did not provide result files.');
await writeTestFiles(result.files, outputPath);
const setupFiles = ['init-testbed.js'];
if (buildTargetOptions?.polyfills?.length) {
setupFiles.push('polyfills.js');
}
instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, {
test: {
root: outputPath,
setupFiles,
// Use `jsdom` if no browsers are explicitly configured.
// `node` is effectively no "environment" and the default.
environment: browser ? 'node' : 'jsdom',
watch: normalizedOptions.watch,
browser,
coverage: {
enabled: normalizedOptions.codeCoverage,
exclude: normalizedOptions.codeCoverageExclude,
excludeAfterRemap: true,
},
},
});
// Check if all the tests pass to calculate the result
const testModules = instance.state.getTestModules();
yield { success: testModules.every((testModule) => testModule.ok()) };
}
}
function findBrowserProvider(
projectSourceRoot: string,
): import('vitest/node').BrowserBuiltinProvider | undefined {
const projectResolver = createRequire(projectSourceRoot + '/').resolve;
// These must be installed in the project to be used
const vitestBuiltinProviders = ['playwright', 'webdriverio'] as const;
for (const providerName of vitestBuiltinProviders) {
try {
projectResolver(providerName);
return providerName;
} catch {}
}
}