Skip to content

Commit 11fab9c

Browse files
committed
feat(@angular/build): add application builder karma testing to package
An `application` only variant of the `karma` builder found within the `@angular-devkit/build-angular` package is now available within the `@angular/build` package as `@angular/build:karma`. This builder will only use the `application` builder found within `@angular/build` and does not provide the `builderMode` option as `application` would be the only valid value. Testing behavior is effectively equivalent to using the `@angular-devkit/build-angular:karma` builder with the `builderMode` option set to `application`. However, several options have been adjusted: * `builderMode` was removed * `fileReplacements` legacy structure (`src`/`replaceWith`) removed * `polyfills` only accepts an array of strings * `loader` has been added * `define` has been added * `externalDependencies` has been added
1 parent e6deb82 commit 11fab9c

33 files changed

+2056
-68
lines changed

.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.npmrc=-1406867100
55
modules/testing/builder/package.json=973445093
66
package.json=-1990485513
7-
packages/angular/build/package.json=-1875938558
7+
packages/angular/build/package.json=517491420
88
packages/angular/cli/package.json=-803141029
99
packages/angular/pwa/package.json=1108903917
1010
packages/angular/ssr/package.json=1856194341

packages/angular/build/BUILD.bazel

+54
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ ts_json_schema(
2424
src = "src/builders/extract-i18n/schema.json",
2525
)
2626

27+
ts_json_schema(
28+
name = "ng_karma_schema",
29+
src = "src/builders/karma/schema.json",
30+
)
31+
2732
ts_json_schema(
2833
name = "ng_packagr_schema",
2934
src = "src/builders/ng-packagr/schema.json",
@@ -63,6 +68,7 @@ ts_project(
6368
"//packages/angular/build:src/builders/application/schema.ts",
6469
"//packages/angular/build:src/builders/dev-server/schema.ts",
6570
"//packages/angular/build:src/builders/extract-i18n/schema.ts",
71+
"//packages/angular/build:src/builders/karma/schema.ts",
6672
"//packages/angular/build:src/builders/ng-packagr/schema.ts",
6773
],
6874
data = RUNTIME_ASSETS,
@@ -85,6 +91,7 @@ ts_project(
8591
"//:node_modules/@babel/plugin-syntax-import-attributes",
8692
"//:node_modules/@inquirer/confirm",
8793
"//:node_modules/@types/babel__core",
94+
"//:node_modules/@types/karma",
8895
"//:node_modules/@types/less",
8996
"//:node_modules/@types/node",
9097
"//:node_modules/@types/picomatch",
@@ -99,6 +106,7 @@ ts_project(
99106
"//:node_modules/https-proxy-agent",
100107
"//:node_modules/istanbul-lib-instrument",
101108
"//:node_modules/jsonc-parser",
109+
"//:node_modules/karma",
102110
"//:node_modules/less",
103111
"//:node_modules/listr2",
104112
"//:node_modules/lmdb",
@@ -200,6 +208,39 @@ ts_project(
200208
],
201209
)
202210

211+
ts_project(
212+
name = "karma_integration_test_lib",
213+
testonly = True,
214+
srcs = glob(include = ["src/builders/karma/tests/**/*.ts"]),
215+
deps = [
216+
":build_rjs",
217+
"//packages/angular/build/private:private_rjs",
218+
"//modules/testing/builder:builder_rjs",
219+
":node_modules/@angular-devkit/architect",
220+
221+
# karma specific test deps
222+
"//:node_modules/karma-chrome-launcher",
223+
"//:node_modules/karma-coverage",
224+
"//:node_modules/karma-jasmine",
225+
"//:node_modules/karma-jasmine-html-reporter",
226+
"//:node_modules/puppeteer",
227+
228+
# Base dependencies for the karma in hello-world-app.
229+
"//:node_modules/@angular/common",
230+
"//:node_modules/@angular/compiler",
231+
"//:node_modules/@angular/compiler-cli",
232+
"//:node_modules/@angular/core",
233+
"//:node_modules/@angular/platform-browser",
234+
"//:node_modules/@angular/platform-browser-dynamic",
235+
"//:node_modules/@angular/router",
236+
"//:node_modules/rxjs",
237+
"//:node_modules/tslib",
238+
"//:node_modules/typescript",
239+
"//:node_modules/zone.js",
240+
"//:node_modules/buffer",
241+
],
242+
)
243+
203244
jasmine_test(
204245
name = "application_integration_tests",
205246
size = "large",
@@ -216,6 +257,19 @@ jasmine_test(
216257
shard_count = 10,
217258
)
218259

260+
jasmine_test(
261+
name = "karma_integration_tests",
262+
size = "large",
263+
data = [":karma_integration_test_lib_rjs"],
264+
env = {
265+
# TODO: Replace Puppeteer downloaded browsers with Bazel-managed browsers,
266+
# or standardize to avoid complex configuration like this!
267+
"PUPPETEER_DOWNLOAD_PATH": "../../../node_modules/puppeteer/downloads",
268+
},
269+
flaky = True,
270+
shard_count = 10,
271+
)
272+
219273
genrule(
220274
name = "license",
221275
srcs = ["//:LICENSE"],

packages/angular/build/builders.json

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"schema": "./src/builders/extract-i18n/schema.json",
1616
"description": "Extract i18n messages from an application."
1717
},
18+
"karma": {
19+
"implementation": "./src/builders/karma",
20+
"schema": "./src/builders/karma/schema.json",
21+
"description": "Run Karma unit tests."
22+
},
1823
"ng-packagr": {
1924
"implementation": "./src/builders/ng-packagr/index",
2025
"schema": "./src/builders/ng-packagr/schema.json",

packages/angular/build/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@angular/platform-server": "0.0.0-ANGULAR-FW-PEER-DEP",
5959
"@angular/service-worker": "0.0.0-ANGULAR-FW-PEER-DEP",
6060
"@angular/ssr": "^0.0.0-PLACEHOLDER",
61+
"karma": "^6.4.0",
6162
"less": "^4.2.0",
6263
"ng-packagr": "0.0.0-NG-PACKAGR-PEER-DEP",
6364
"postcss": "^8.4.0",
@@ -77,6 +78,9 @@
7778
"@angular/ssr": {
7879
"optional": true
7980
},
81+
"karma": {
82+
"optional": true
83+
},
8084
"less": {
8185
"optional": true
8286
},

packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts packages/angular/build/src/builders/karma/application_builder.ts

+55-59
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { BuildOutputFileType } from '@angular/build';
109
import {
1110
ApplicationBuilderInternalOptions,
1211
Result,
@@ -15,21 +14,22 @@ import {
1514
buildApplicationInternal,
1615
emitFilesToDisk,
1716
} from '@angular/build/private';
18-
import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
17+
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
1918
import glob from 'fast-glob';
20-
import type { Config, ConfigOptions, FilePattern, InlinePluginDef } from 'karma';
19+
import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from 'karma';
2120
import { randomUUID } from 'node:crypto';
2221
import * as fs from 'node:fs/promises';
23-
import { IncomingMessage, ServerResponse } from 'node:http';
22+
import type { IncomingMessage, ServerResponse } from 'node:http';
23+
import { createRequire } from 'node:module';
2424
import * as path from 'node:path';
25-
import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap } from 'rxjs';
26-
import { Configuration } from 'webpack';
27-
import { ExecutionTransformer } from '../../transforms';
28-
import { normalizeFileReplacements } from '../../utils';
29-
import { OutputHashing } from '../browser-esbuild/schema';
25+
import { ReadableStreamController } from 'node:stream/web';
26+
import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
27+
import { OutputHashing } from '../application/schema';
3028
import { findTests, getTestEntrypoints } from './find-tests';
3129
import { Schema as KarmaBuilderOptions } from './schema';
3230

31+
const localResolve = createRequire(__filename).resolve;
32+
3333
interface BuildOptions extends ApplicationBuilderInternalOptions {
3434
// We know that it's always a string since we set it.
3535
outputPath: string;
@@ -171,7 +171,7 @@ function injectKarmaReporter(
171171
buildOptions: BuildOptions,
172172
buildIterator: AsyncIterator<Result>,
173173
karmaConfig: Config & ConfigOptions,
174-
subscriber: Subscriber<BuilderOutput>,
174+
controller: ReadableStreamController<BuilderOutput>,
175175
) {
176176
const reporterName = 'angular-progress-notifier';
177177

@@ -205,7 +205,7 @@ function injectKarmaReporter(
205205
}
206206

207207
if (buildOutput.kind === ResultKind.Failure) {
208-
subscriber.next({ success: false, message: 'Build failed' });
208+
controller.enqueue({ success: false, message: 'Build failed' });
209209
} else if (
210210
buildOutput.kind === ResultKind.Incremental ||
211211
buildOutput.kind === ResultKind.Full
@@ -227,9 +227,9 @@ function injectKarmaReporter(
227227

228228
onRunComplete = function (_browsers: unknown, results: RunCompleteInfo) {
229229
if (results.exitCode === 0) {
230-
subscriber.next({ success: true });
230+
controller.enqueue({ success: true });
231231
} else {
232-
subscriber.next({ success: false });
232+
controller.enqueue({ success: false });
233233
}
234234
};
235235
}
@@ -255,44 +255,48 @@ export function execute(
255255
context: BuilderContext,
256256
karmaOptions: ConfigOptions,
257257
transforms: {
258-
webpackConfiguration?: ExecutionTransformer<Configuration>;
259258
// The karma options transform cannot be async without a refactor of the builder implementation
260259
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
261260
} = {},
262-
): Observable<BuilderOutput> {
263-
return from(initializeApplication(options, context, karmaOptions, transforms)).pipe(
264-
switchMap(
265-
([karma, karmaConfig, buildOptions, buildIterator]) =>
266-
new Observable<BuilderOutput>((subscriber) => {
267-
// If `--watch` is explicitly enabled or if we are keeping the Karma
268-
// process running, we should hook Karma into the build.
269-
if (buildIterator) {
270-
injectKarmaReporter(buildOptions, buildIterator, karmaConfig, subscriber);
271-
}
261+
): AsyncIterable<BuilderOutput> {
262+
let karmaServer: Server;
263+
264+
return new ReadableStream({
265+
async start(controller) {
266+
let init;
267+
try {
268+
init = await initializeApplication(options, context, karmaOptions, transforms);
269+
} catch (err) {
270+
if (err instanceof ApplicationBuildError) {
271+
controller.enqueue({ success: false, message: err.message });
272+
controller.close();
273+
274+
return;
275+
}
272276

273-
// Complete the observable once the Karma server returns.
274-
const karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => {
275-
subscriber.next({ success: exitCode === 0 });
276-
subscriber.complete();
277-
});
277+
throw err;
278+
}
279+
280+
const [karma, karmaConfig, buildOptions, buildIterator] = init;
278281

279-
const karmaStart = karmaServer.start();
280-
281-
// Cleanup, signal Karma to exit.
282-
return () => {
283-
void karmaStart.then(() => karmaServer.stop());
284-
};
285-
}),
286-
),
287-
catchError((err) => {
288-
if (err instanceof ApplicationBuildError) {
289-
return of({ success: false, message: err.message });
282+
// If `--watch` is explicitly enabled or if we are keeping the Karma
283+
// process running, we should hook Karma into the build.
284+
if (buildIterator) {
285+
injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controller);
290286
}
291287

292-
throw err;
293-
}),
294-
defaultIfEmpty({ success: false }),
295-
);
288+
// Close the stream once the Karma server returns.
289+
karmaServer = new karma.Server(karmaConfig as Config, (exitCode) => {
290+
controller.enqueue({ success: exitCode === 0 });
291+
controller.close();
292+
});
293+
294+
await karmaServer.start();
295+
},
296+
async cancel() {
297+
await karmaServer?.stop();
298+
},
299+
});
296300
}
297301

298302
async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
@@ -315,10 +319,8 @@ function normalizePolyfills(polyfills: string | string[] | undefined): [string[]
315319
polyfills = [];
316320
}
317321

318-
const jasmineGlobalEntryPoint =
319-
'@angular-devkit/build-angular/src/builders/karma/jasmine_global.js';
320-
const jasmineGlobalCleanupEntrypoint =
321-
'@angular-devkit/build-angular/src/builders/karma/jasmine_global_cleanup.js';
322+
const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js');
323+
const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js');
322324

323325
const zoneTestingEntryPoint = 'zone.js/testing';
324326
const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
@@ -352,18 +354,11 @@ async function initializeApplication(
352354
context: BuilderContext,
353355
karmaOptions: ConfigOptions,
354356
transforms: {
355-
webpackConfiguration?: ExecutionTransformer<Configuration>;
356357
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
357358
} = {},
358359
): Promise<
359360
[typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator<Result> | null]
360361
> {
361-
if (transforms.webpackConfiguration) {
362-
context.logger.warn(
363-
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
364-
);
365-
}
366-
367362
const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID());
368363
const projectSourceRoot = await getProjectSourceRoot(context);
369364

@@ -377,7 +372,7 @@ async function initializeApplication(
377372
if (options.main) {
378373
entryPoints.set(mainName, options.main);
379374
} else {
380-
entryPoints.set(mainName, '@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
375+
entryPoints.set(mainName, localResolve('./polyfills/init_test_bed.js'));
381376
}
382377

383378
const instrumentForCoverage = options.codeCoverage
@@ -416,9 +411,10 @@ async function initializeApplication(
416411
watch: options.watch ?? !karmaOptions.singleRun,
417412
stylePreprocessorOptions: options.stylePreprocessorOptions,
418413
inlineStyleLanguage: options.inlineStyleLanguage,
419-
fileReplacements: options.fileReplacements
420-
? normalizeFileReplacements(options.fileReplacements, './')
421-
: undefined,
414+
fileReplacements: options.fileReplacements,
415+
define: options.define,
416+
loader: options.loader,
417+
externalDependencies: options.externalDependencies,
422418
};
423419

424420
// Build tests with `application` builder, using test files as entry points.

0 commit comments

Comments
 (0)