Skip to content

Commit ca8bd32

Browse files
committed
fix(@angular-devkit/build-angular): jasmine.clock with app builder
1 parent 0d2c648 commit ca8bd32

File tree

2 files changed

+123
-7
lines changed

2 files changed

+123
-7
lines changed

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

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { randomUUID } from 'crypto';
2020
import glob from 'fast-glob';
2121
import * as fs from 'fs/promises';
2222
import { IncomingMessage, ServerResponse } from 'http';
23-
import type { Config, ConfigOptions, InlinePluginDef } from 'karma';
23+
import type { Config, ConfigOptions, FilePattern, InlinePluginDef } from 'karma';
2424
import * as path from 'path';
2525
import { Observable, Subscriber, catchError, defaultIfEmpty, from, of, switchMap } from 'rxjs';
2626
import { Configuration } from 'webpack';
@@ -106,6 +106,42 @@ class AngularAssetsMiddleware {
106106
}
107107
}
108108

109+
class AngularPolyfillsFramework {
110+
static readonly $inject = ['config.files'];
111+
112+
static readonly NAME = 'angular-test-polyfills';
113+
114+
static createPlugin(
115+
polyfillsFile: FilePattern,
116+
zoneTestingFile: FilePattern | null,
117+
): InlinePluginDef {
118+
return {
119+
[`framework:${AngularPolyfillsFramework.NAME}`]: [
120+
'factory',
121+
Object.assign((files: (string | FilePattern)[]) => {
122+
// The correct order is zone.js -> jasmine -> zone.js/testing.
123+
files.unshift(polyfillsFile);
124+
if (zoneTestingFile) {
125+
const jasmineBootIndex = files.findIndex((f) => {
126+
if (typeof f === 'string') {
127+
return false;
128+
}
129+
130+
return f.pattern.endsWith('karma-jasmine/lib/boot.js');
131+
});
132+
if (jasmineBootIndex === -1) {
133+
// Insert after polyfills.
134+
files.splice(1, 0, zoneTestingFile);
135+
} else {
136+
files.splice(jasmineBootIndex + 1, 0, zoneTestingFile);
137+
}
138+
}
139+
}, AngularPolyfillsFramework),
140+
],
141+
};
142+
}
143+
}
144+
109145
function injectKarmaReporter(
110146
buildOptions: BuildOptions,
111147
buildIterator: AsyncIterator<Result>,
@@ -247,12 +283,20 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247283
return path.join(context.workspaceRoot, sourceRoot);
248284
}
249285

250-
function normalizePolyfills(polyfills: string | string[] | undefined): string[] {
286+
function normalizePolyfills(polyfills: string | string[] | undefined): [string[], string | null] {
251287
if (typeof polyfills === 'string') {
252-
return [polyfills];
288+
polyfills = [polyfills];
289+
} else if (!polyfills) {
290+
polyfills = [];
253291
}
254292

255-
return polyfills ?? [];
293+
const zoneTestingEntryPoint = 'zone.js/testing';
294+
const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
295+
296+
return [
297+
polyfillsExludingZoneTesting,
298+
polyfillsExludingZoneTesting.length === polyfills.length ? null : zoneTestingEntryPoint,
299+
];
256300
}
257301

258302
async function collectEntrypoints(
@@ -311,6 +355,11 @@ async function initializeApplication(
311355
)
312356
: undefined;
313357

358+
const [polyfills, zoneTesting] = normalizePolyfills(options.polyfills);
359+
if (zoneTesting) {
360+
entryPoints.set('zone-testing', zoneTesting);
361+
}
362+
314363
const buildOptions: BuildOptions = {
315364
assets: options.assets,
316365
entryPoints,
@@ -327,7 +376,7 @@ async function initializeApplication(
327376
},
328377
instrumentForCoverage,
329378
styles: options.styles,
330-
polyfills: normalizePolyfills(options.polyfills),
379+
polyfills,
331380
webWorkerTsConfig: options.webWorkerTsConfig,
332381
watch: options.watch ?? !karmaOptions.singleRun,
333382
stylePreprocessorOptions: options.stylePreprocessorOptions,
@@ -349,10 +398,26 @@ async function initializeApplication(
349398
// Write test files
350399
await writeTestFiles(buildOutput.files, buildOptions.outputPath);
351400

401+
// We need to add this to the beginning *after* the testing framework has
402+
// prepended its files.
403+
const polyfillsFile: FilePattern = {
404+
pattern: `${outputPath}/polyfills.js`,
405+
included: true,
406+
served: true,
407+
watched: false,
408+
};
409+
const zoneTestingFile: FilePattern | null = zoneTesting
410+
? {
411+
pattern: `${outputPath}/zone-testing.js`,
412+
included: true,
413+
served: true,
414+
type: 'module',
415+
watched: false,
416+
}
417+
: null;
418+
352419
karmaOptions.files ??= [];
353420
karmaOptions.files.push(
354-
// Serve polyfills first.
355-
{ pattern: `${outputPath}/polyfills.js`, type: 'module', watched: false },
356421
// Serve global setup script.
357422
{ pattern: `${outputPath}/${mainName}.js`, type: 'module', watched: false },
358423
// Serve all source maps.
@@ -413,6 +478,11 @@ async function initializeApplication(
413478
parsedKarmaConfig.middleware ??= [];
414479
parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME);
415480

481+
parsedKarmaConfig.plugins.push(
482+
AngularPolyfillsFramework.createPlugin(polyfillsFile, zoneTestingFile),
483+
);
484+
parsedKarmaConfig.frameworks.push(AngularPolyfillsFramework.NAME);
485+
416486
// When using code-coverage, auto-add karma-coverage.
417487
// This was done as part of the karma plugin for webpack.
418488
if (
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 { execute } from '../../index';
10+
import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup';
11+
12+
describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget) => {
13+
describe('Behavior: "jasmine.clock()"', () => {
14+
beforeEach(async () => {
15+
await setupTarget(harness);
16+
});
17+
18+
it('can install and uninstall the mock clock', async () => {
19+
await harness.writeFiles({
20+
'./src/app/app.component.spec.ts': `
21+
import { AppComponent } from './app.component';
22+
23+
describe('Using jasmine.clock()', () => {
24+
beforeEach(async () => {
25+
jasmine.clock().install();
26+
});
27+
28+
afterEach(() => {
29+
jasmine.clock().uninstall();
30+
});
31+
32+
it('runs a basic test case', () => {
33+
expect(!!AppComponent).toBe(true);
34+
});
35+
});`,
36+
});
37+
38+
harness.useTarget('test', {
39+
...BASE_OPTIONS,
40+
});
41+
42+
const { result } = await harness.executeOnce();
43+
expect(result?.success).toBeTrue();
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)