Skip to content

Commit 5e2836e

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

File tree

2 files changed

+122
-7
lines changed

2 files changed

+122
-7
lines changed

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

Lines changed: 76 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,41 @@ 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+
return f.pattern.endsWith('karma-jasmine/lib/boot.js');
130+
});
131+
if (jasmineBootIndex === -1) {
132+
// Insert after polyfills.
133+
files.splice(1, 0, zoneTestingFile);
134+
} else {
135+
files.splice(jasmineBootIndex + 1, 0, zoneTestingFile);
136+
}
137+
}
138+
}, AngularPolyfillsFramework),
139+
],
140+
};
141+
}
142+
}
143+
109144
function injectKarmaReporter(
110145
buildOptions: BuildOptions,
111146
buildIterator: AsyncIterator<Result>,
@@ -247,12 +282,20 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247282
return path.join(context.workspaceRoot, sourceRoot);
248283
}
249284

250-
function normalizePolyfills(polyfills: string | string[] | undefined): string[] {
285+
function normalizePolyfills(polyfills: string | string[] | undefined): [string[], string | null] {
251286
if (typeof polyfills === 'string') {
252-
return [polyfills];
287+
polyfills = [polyfills];
288+
} else if (!polyfills) {
289+
polyfills = [];
253290
}
254291

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

258301
async function collectEntrypoints(
@@ -311,6 +354,11 @@ async function initializeApplication(
311354
)
312355
: undefined;
313356

357+
const [polyfills, zoneTesting] = normalizePolyfills(options.polyfills);
358+
if (zoneTesting) {
359+
entryPoints.set('zone-testing', zoneTesting);
360+
}
361+
314362
const buildOptions: BuildOptions = {
315363
assets: options.assets,
316364
entryPoints,
@@ -327,7 +375,7 @@ async function initializeApplication(
327375
},
328376
instrumentForCoverage,
329377
styles: options.styles,
330-
polyfills: normalizePolyfills(options.polyfills),
378+
polyfills,
331379
webWorkerTsConfig: options.webWorkerTsConfig,
332380
watch: options.watch ?? !karmaOptions.singleRun,
333381
stylePreprocessorOptions: options.stylePreprocessorOptions,
@@ -349,10 +397,26 @@ async function initializeApplication(
349397
// Write test files
350398
await writeTestFiles(buildOutput.files, buildOptions.outputPath);
351399

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

480+
parsedKarmaConfig.plugins.push(
481+
AngularPolyfillsFramework.createPlugin(polyfillsFile, zoneTestingFile),
482+
);
483+
parsedKarmaConfig.frameworks.push(AngularPolyfillsFramework.NAME);
484+
416485
// When using code-coverage, auto-add karma-coverage.
417486
// This was done as part of the karma plugin for webpack.
418487
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)