Skip to content

Commit eeb8b0c

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 otherwise equivalent to using the `@angular-devkit/build-angular:karma` builder with the `builderMode` option set to `application`.
1 parent f5689e9 commit eeb8b0c

32 files changed

+1993
-68
lines changed

.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx

Lines changed: 1 addition & 1 deletion
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=551629798
7-
packages/angular/build/package.json=1444505662
7+
packages/angular/build/package.json=-450547974
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

Lines changed: 54 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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
},
Lines changed: 54 additions & 58 deletions
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,20 +14,22 @@ import {
1514
buildApplicationInternal,
1615
emitFilesToDisk,
1716
} from '@angular/build/private';
18-
import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
19-
import { randomUUID } from 'crypto';
17+
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
2018
import glob from 'fast-glob';
21-
import * as fs from 'fs/promises';
22-
import { IncomingMessage, ServerResponse } from 'http';
23-
import type { Config, ConfigOptions, FilePattern, InlinePluginDef } from 'karma';
24-
import * as path from 'path';
25-
import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap } from 'rxjs';
26-
import { Configuration } from 'webpack';
27-
import { ExecutionTransformer } from '../../transforms';
28-
import { OutputHashing } from '../browser-esbuild/schema';
19+
import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from 'karma';
20+
import { randomUUID } from 'node:crypto';
21+
import * as fs from 'node:fs/promises';
22+
import type { IncomingMessage, ServerResponse } from 'node:http';
23+
import { createRequire } from 'node:module';
24+
import * as path from 'node:path';
25+
import { ReadableStreamController } from 'node:stream/web';
26+
import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
27+
import { OutputHashing } from '../application/schema';
2928
import { findTests, getTestEntrypoints } from './find-tests';
3029
import { Schema as KarmaBuilderOptions } from './schema';
3130

31+
const localResolve = createRequire(__filename).resolve;
32+
3233
interface BuildOptions extends ApplicationBuilderInternalOptions {
3334
// We know that it's always a string since we set it.
3435
outputPath: string;
@@ -170,7 +171,7 @@ function injectKarmaReporter(
170171
buildOptions: BuildOptions,
171172
buildIterator: AsyncIterator<Result>,
172173
karmaConfig: Config & ConfigOptions,
173-
subscriber: Subscriber<BuilderOutput>,
174+
controller: ReadableStreamController<BuilderOutput>,
174175
) {
175176
const reporterName = 'angular-progress-notifier';
176177

@@ -204,7 +205,7 @@ function injectKarmaReporter(
204205
}
205206

206207
if (buildOutput.kind === ResultKind.Failure) {
207-
subscriber.next({ success: false, message: 'Build failed' });
208+
controller.enqueue({ success: false, message: 'Build failed' });
208209
} else if (
209210
buildOutput.kind === ResultKind.Incremental ||
210211
buildOutput.kind === ResultKind.Full
@@ -226,9 +227,9 @@ function injectKarmaReporter(
226227

227228
onRunComplete = function (_browsers: unknown, results: RunCompleteInfo) {
228229
if (results.exitCode === 0) {
229-
subscriber.next({ success: true });
230+
controller.enqueue({ success: true });
230231
} else {
231-
subscriber.next({ success: false });
232+
controller.enqueue({ success: false });
232233
}
233234
};
234235
}
@@ -254,44 +255,48 @@ export function execute(
254255
context: BuilderContext,
255256
karmaOptions: ConfigOptions,
256257
transforms: {
257-
webpackConfiguration?: ExecutionTransformer<Configuration>;
258258
// The karma options transform cannot be async without a refactor of the builder implementation
259259
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
260260
} = {},
261-
): Observable<BuilderOutput> {
262-
return from(initializeApplication(options, context, karmaOptions, transforms)).pipe(
263-
switchMap(
264-
([karma, karmaConfig, buildOptions, buildIterator]) =>
265-
new Observable<BuilderOutput>((subscriber) => {
266-
// If `--watch` is explicitly enabled or if we are keeping the Karma
267-
// process running, we should hook Karma into the build.
268-
if (buildIterator) {
269-
injectKarmaReporter(buildOptions, buildIterator, karmaConfig, subscriber);
270-
}
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+
}
271276

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

278-
const karmaStart = karmaServer.start();
279-
280-
// Cleanup, signal Karma to exit.
281-
return () => {
282-
void karmaStart.then(() => karmaServer.stop());
283-
};
284-
}),
285-
),
286-
catchError((err) => {
287-
if (err instanceof ApplicationBuildError) {
288-
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);
289286
}
290287

291-
throw err;
292-
}),
293-
defaultIfEmpty({ success: false }),
294-
);
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+
});
295300
}
296301

297302
async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
@@ -314,10 +319,8 @@ function normalizePolyfills(polyfills: string | string[] | undefined): [string[]
314319
polyfills = [];
315320
}
316321

317-
const jasmineGlobalEntryPoint =
318-
'@angular-devkit/build-angular/src/builders/karma/jasmine_global.js';
319-
const jasmineGlobalCleanupEntrypoint =
320-
'@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');
321324

322325
const zoneTestingEntryPoint = 'zone.js/testing';
323326
const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
@@ -351,18 +354,11 @@ async function initializeApplication(
351354
context: BuilderContext,
352355
karmaOptions: ConfigOptions,
353356
transforms: {
354-
webpackConfiguration?: ExecutionTransformer<Configuration>;
355357
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
356358
} = {},
357359
): Promise<
358360
[typeof import('karma'), Config & ConfigOptions, BuildOptions, AsyncIterator<Result> | null]
359361
> {
360-
if (transforms.webpackConfiguration) {
361-
context.logger.warn(
362-
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`,
363-
);
364-
}
365-
366362
const outputPath = path.join(context.workspaceRoot, 'dist/test-out', randomUUID());
367363
const projectSourceRoot = await getProjectSourceRoot(context);
368364

@@ -376,7 +372,7 @@ async function initializeApplication(
376372
if (options.main) {
377373
entryPoints.set(mainName, options.main);
378374
} else {
379-
entryPoints.set(mainName, '@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
375+
entryPoints.set(mainName, localResolve('./polyfills/init_test_bed.js'));
380376
}
381377

382378
const instrumentForCoverage = options.codeCoverage

0 commit comments

Comments
 (0)