Skip to content

Commit db74212

Browse files
committedDec 13, 2024·
fix(@angular-devkit/build-angular): jasmine.clock with app builder
(cherry picked from commit a9a3470)
1 parent 3551d57 commit db74212

File tree

4 files changed

+186
-7
lines changed

4 files changed

+186
-7
lines changed
 

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

+108-7
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,66 @@ class AngularAssetsMiddleware {
106106
}
107107
}
108108

109+
class AngularPolyfillsPlugin {
110+
static readonly $inject = ['config.files'];
111+
112+
static readonly NAME = 'angular-polyfills';
113+
114+
static createPlugin(
115+
polyfillsFile: FilePattern,
116+
jasmineCleanupFiles: FilePattern,
117+
): InlinePluginDef {
118+
return {
119+
// This has to be a "reporter" because reporters run _after_ frameworks
120+
// and karma-jasmine-html-reporter injects additional scripts that may
121+
// depend on Jasmine but aren't modules - which means that they would run
122+
// _before_ all module code (including jasmine).
123+
[`reporter:${AngularPolyfillsPlugin.NAME}`]: [
124+
'factory',
125+
Object.assign((files: (string | FilePattern)[]) => {
126+
// The correct order is zone.js -> jasmine -> zone.js/testing.
127+
// Jasmine has to see the patched version of the global `setTimeout`
128+
// function so it doesn't cache the unpatched version. And /testing
129+
// needs to see the global `jasmine` object so it can patch it.
130+
const polyfillsIndex = 0;
131+
files.splice(polyfillsIndex, 0, polyfillsFile);
132+
133+
// Insert just before test_main.js.
134+
const zoneTestingIndex = files.findIndex((f) => {
135+
if (typeof f === 'string') {
136+
return false;
137+
}
138+
139+
return f.pattern.endsWith('/test_main.js');
140+
});
141+
if (zoneTestingIndex === -1) {
142+
throw new Error('Could not find test entrypoint file.');
143+
}
144+
files.splice(zoneTestingIndex, 0, jasmineCleanupFiles);
145+
146+
// We need to ensure that all files are served as modules, otherwise
147+
// the order in the files list gets really confusing: Karma doesn't
148+
// set defer on scripts, so all scripts with type=js will run first,
149+
// even if type=module files appeared earlier in `files`.
150+
for (const f of files) {
151+
if (typeof f === 'string') {
152+
throw new Error(`Unexpected string-based file: "${f}"`);
153+
}
154+
if (f.included === false) {
155+
// Don't worry about files that aren't included on the initial
156+
// page load. `type` won't affect them.
157+
continue;
158+
}
159+
if ('js' === (f.type ?? 'js')) {
160+
f.type = 'module';
161+
}
162+
}
163+
}, AngularPolyfillsPlugin),
164+
],
165+
};
166+
}
167+
}
168+
109169
function injectKarmaReporter(
110170
buildOptions: BuildOptions,
111171
buildIterator: AsyncIterator<Result>,
@@ -247,12 +307,27 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247307
return path.join(context.workspaceRoot, sourceRoot);
248308
}
249309

250-
function normalizePolyfills(polyfills: string | string[] | undefined): string[] {
310+
function normalizePolyfills(polyfills: string | string[] | undefined): [string[], string[]] {
251311
if (typeof polyfills === 'string') {
252-
return [polyfills];
312+
polyfills = [polyfills];
313+
} else if (!polyfills) {
314+
polyfills = [];
253315
}
254316

255-
return polyfills ?? [];
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';
321+
322+
const zoneTestingEntryPoint = 'zone.js/testing';
323+
const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
324+
325+
return [
326+
polyfillsExludingZoneTesting.concat([jasmineGlobalEntryPoint]),
327+
polyfillsExludingZoneTesting.length === polyfills.length
328+
? [jasmineGlobalCleanupEntrypoint]
329+
: [jasmineGlobalCleanupEntrypoint, zoneTestingEntryPoint],
330+
];
256331
}
257332

258333
async function collectEntrypoints(
@@ -311,6 +386,11 @@ async function initializeApplication(
311386
)
312387
: undefined;
313388

389+
const [polyfills, jasmineCleanup] = normalizePolyfills(options.polyfills);
390+
for (let idx = 0; idx < jasmineCleanup.length; ++idx) {
391+
entryPoints.set(`jasmine-cleanup-${idx}`, jasmineCleanup[idx]);
392+
}
393+
314394
const buildOptions: BuildOptions = {
315395
assets: options.assets,
316396
entryPoints,
@@ -327,7 +407,7 @@ async function initializeApplication(
327407
},
328408
instrumentForCoverage,
329409
styles: options.styles,
330-
polyfills: normalizePolyfills(options.polyfills),
410+
polyfills,
331411
webWorkerTsConfig: options.webWorkerTsConfig,
332412
watch: options.watch ?? !karmaOptions.singleRun,
333413
stylePreprocessorOptions: options.stylePreprocessorOptions,
@@ -349,10 +429,25 @@ async function initializeApplication(
349429
// Write test files
350430
await writeTestFiles(buildOutput.files, buildOptions.outputPath);
351431

432+
// We need to add this to the beginning *after* the testing framework has
433+
// prepended its files.
434+
const polyfillsFile: FilePattern = {
435+
pattern: `${outputPath}/polyfills.js`,
436+
included: true,
437+
served: true,
438+
type: 'module',
439+
watched: false,
440+
};
441+
const jasmineCleanupFiles: FilePattern = {
442+
pattern: `${outputPath}/jasmine-cleanup-*.js`,
443+
included: true,
444+
served: true,
445+
type: 'module',
446+
watched: false,
447+
};
448+
352449
karmaOptions.files ??= [];
353450
karmaOptions.files.push(
354-
// Serve polyfills first.
355-
{ pattern: `${outputPath}/polyfills.js`, type: 'module', watched: false },
356451
// Serve global setup script.
357452
{ pattern: `${outputPath}/${mainName}.js`, type: 'module', watched: false },
358453
// Serve all source maps.
@@ -413,6 +508,12 @@ async function initializeApplication(
413508
parsedKarmaConfig.middleware ??= [];
414509
parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME);
415510

511+
parsedKarmaConfig.plugins.push(
512+
AngularPolyfillsPlugin.createPlugin(polyfillsFile, jasmineCleanupFiles),
513+
);
514+
parsedKarmaConfig.reporters ??= [];
515+
parsedKarmaConfig.reporters.push(AngularPolyfillsPlugin.NAME);
516+
416517
// When using code-coverage, auto-add karma-coverage.
417518
// This was done as part of the karma plugin for webpack.
418519
if (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
// See: https://github.com/jasmine/jasmine/issues/2015
10+
(function () {
11+
'use strict';
12+
13+
// jasmine will ignore `window` unless it returns this specific (but uncommon)
14+
// value from toString().
15+
window.toString = function () {
16+
return '[object GjsGlobal]';
17+
};
18+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
// See: https://github.com/jasmine/jasmine/issues/2015
10+
(function () {
11+
'use strict';
12+
13+
delete window.toString;
14+
})();
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)
Please sign in to comment.