Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { createRequire } from 'node:module';
import path from 'node:path';
import { toPosixPath } from '../../../../utils/path';
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
import { OutputHashing } from '../../../application/schema';
import { NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from '../../options';
import { NormalizedUnitTestBuilderOptions } from '../../options';
import { findTests, getTestEntrypoints } from '../../test-discovery';
import { RunnerOptions } from '../api';

function createTestBedInitVirtualFile(
providersFile: string | undefined,
projectSourceRoot: string,
teardown: boolean,
polyfills: string[] = [],
zoneTestingStrategy: 'none' | 'static' | 'dynamic',
): string {
const usesZoneJS = polyfills.includes('zone.js');
let providersImport = 'const providers = [];';
if (providersFile) {
const relativePath = path.relative(projectSourceRoot, providersFile);
Expand All @@ -31,12 +31,25 @@ function createTestBedInitVirtualFile(

return `
// Initialize the Angular testing environment
import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';
import { NgModule, provideZoneChangeDetection } from '@angular/core';
import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
import { afterEach, beforeEach } from 'vitest';
${providersImport}

${
zoneTestingStrategy === 'static'
? `import 'zone.js/testing';`
: zoneTestingStrategy === 'dynamic'
? `
if (typeof Zone !== 'undefined') {
// 'zone.js/testing' is used to initialize the ZoneJS testing environment.
// It must be imported dynamically to avoid a static dependency on 'zone.js'.
await import('zone.js/testing');
}`
: ''
}

// The beforeEach and afterEach hooks are registered outside the globalThis guard.
// This ensures that the hooks are always applied, even in non-isolated browser environments.
// Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/srcs/test_hooks.ts#L21-L29
Expand All @@ -52,7 +65,10 @@ function createTestBedInitVirtualFile(
// The guard condition above ensures that the setup is only performed once.

@NgModule({
providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers],
providers: [
...(typeof Zone !== 'undefined' ? [provideZoneChangeDetection()] : []),
...providers,
],
})
class TestModule {}

Expand Down Expand Up @@ -145,13 +161,30 @@ export async function getVitestBuildOptions(
externalDependencies,
};

buildOptions.polyfills = injectTestingPolyfills(buildOptions.polyfills);
// Inject the zone.js testing polyfill if Zone.js is installed.
let zoneTestingStrategy: 'none' | 'static' | 'dynamic' = 'none';
let isZoneJsInstalled = false;
try {
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this by intention to use the projectSourceRoot instead of the workspaceRoot?

projectRequire.resolve('zone.js');
isZoneJsInstalled = true;
} catch {}

if (isZoneJsInstalled) {
if (buildOptions.polyfills?.includes('zone.js/testing')) {
zoneTestingStrategy = 'none';
} else if (buildOptions.polyfills?.includes('zone.js')) {
zoneTestingStrategy = 'static';
} else {
zoneTestingStrategy = 'dynamic';
}
}
Comment on lines +165 to +181
Copy link
Collaborator

@alan-agius4 alan-agius4 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case zone.js is listed in the polyfills, it seems redundant to check if it's installed.

Suggested change
let zoneTestingStrategy: 'none' | 'static' | 'dynamic' = 'none';
let isZoneJsInstalled = false;
try {
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
projectRequire.resolve('zone.js');
isZoneJsInstalled = true;
} catch {}
if (isZoneJsInstalled) {
if (buildOptions.polyfills?.includes('zone.js/testing')) {
zoneTestingStrategy = 'none';
} else if (buildOptions.polyfills?.includes('zone.js')) {
zoneTestingStrategy = 'static';
} else {
zoneTestingStrategy = 'dynamic';
}
}
// Inject the zone.js testing polyfill if Zone.js is installed.
let zoneTestingStrategy: 'none' | 'static' | 'dynamic';
if (buildOptions.polyfills?.includes('zone.js/testing')) {
zoneTestingStrategy = 'none';
} else if (buildOptions.polyfills?.includes('zone.js')) {
zoneTestingStrategy = 'static';
} else {
try {
const projectRequire = createRequire(path.join(projectSourceRoot, 'package.json'));
projectRequire.resolve('zone.js');
zoneTestingStrategy = 'dynamic';
} catch {
zoneTestingStrategy = 'none';
}
}


const testBedInitContents = createTestBedInitVirtualFile(
providersFile,
projectSourceRoot,
!options.debug,
buildOptions.polyfills,
zoneTestingStrategy,
);

const mockPatchContents = `
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { execute } from '../../index';
import {
BASE_OPTIONS,
describeBuilder,
UNIT_TEST_BUILDER_INFO,
setupApplicationTarget,
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Behavior: "Vitest Zone initialization"', () => {
// Zone.js does not current provide fakAsync support for Vitest
xit('should load Zone and Zone testing support by default', async () => {
setupApplicationTarget(harness); // Defaults include zone.js

harness.useTarget('test', {
...BASE_OPTIONS,
});

harness.writeFile(
'src/app/app.component.spec.ts',
`
import { describe, it, expect } from 'vitest';
import { fakeAsync, tick } from '@angular/core/testing';

describe('Zone Test', () => {
it('should have Zone defined', () => {
expect((globalThis as any).Zone).toBeDefined();
});

it('should support fakeAsync', fakeAsync(() => {
let val = false;
setTimeout(() => { val = true; }, 100);
tick(100);
expect(val).toBeTrue();
}));
});
`,
);

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
});

it('should NOT load Zone when zoneless (no zone.js in polyfills)', async () => {
// Setup application target WITHOUT zone.js in polyfills
setupApplicationTarget(harness, {
polyfills: [],
});

harness.useTarget('test', {
...BASE_OPTIONS,
});

harness.writeFile(
'src/app/app.component.spec.ts',
`
import { describe, it, expect } from 'vitest';

describe('Zoneless Test', () => {
it('should NOT have Zone defined', () => {
expect((globalThis as any).Zone).toBeUndefined();
});
});
`,
);

const { result } = await harness.executeOnce();
expect(result?.success).toBe(true);
});
});
});