Skip to content
Draft
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,28 @@ 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'));
projectRequire.resolve('zone.js');
isZoneJsInstalled = true;
} catch {}

if (isZoneJsInstalled) {
if (buildOptions.polyfills?.includes('zone.js')) {
zoneTestingStrategy = 'static';
} else {
zoneTestingStrategy = 'dynamic';
}
}

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);
});
});
});