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
21 changes: 20 additions & 1 deletion packages/angular/build/src/builders/karma/application_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { Config, ConfigOptions, FilePattern, InlinePluginDef, Server } from
import { randomUUID } from 'node:crypto';
import { rmSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { ReadableStream } from 'node:stream/web';
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
Expand Down Expand Up @@ -225,6 +224,26 @@ async function runEsbuild(
`});`,
];

if (buildOptions.aot !== false) {
contents.push(
`const originalOverrideComponent = getTestBed().overrideComponent;`,
`const warnTracker = new Set();`,
`getTestBed().overrideComponent = function (component, override) {`,
` const isAotComponent = !!component.ɵcmp;`,
` if (isAotComponent && !warnTracker.has(component)) {`,
` warnTracker.add(component);`,
` console.warn(`,
` "[Angular] WARNING: 'TestBed.overrideComponent' was called on '" + component.name + "' in AOT mode. " +`,
` "Overriding template or imports on AOT-compiled components is not fully supported and may cause " +`,
` "NG0304/NG0303 element resolution errors. \\n" +`,
` "👉 Workaround: Set 'aot: false' in the build configuration used for tests (e.g. 'buildTarget' in angular.json)."`,
` );`,
` }`,
` return originalOverrideComponent.call(this, component, override);`,
`};`,
);
}

return {
contents: contents.join('\n'),
loader: 'js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function createTestBedInitVirtualFile(
teardown: boolean,
zoneTestingStrategy: 'none' | 'static' | 'dynamic' | 'dynamic-zone',
hasLocalize: boolean,
isAot: boolean,
): string {
let providersImport = 'const providers = [];';
if (providersFile) {
Expand Down Expand Up @@ -127,6 +128,28 @@ function createTestBedInitVirtualFile(
errorOnUnknownProperties: true,
${teardown === false ? 'teardown: { destroyAfterEach: false },' : ''}
});

${
isAot
? `
const originalOverrideComponent = getTestBed().overrideComponent;
const warnTracker = new Set();
getTestBed().overrideComponent = function (component, override) {
const isAotComponent = !!component.ɵcmp;
if (isAotComponent && !warnTracker.has(component)) {
warnTracker.add(component);
console.warn(
\`[Angular] WARNING: 'TestBed.overrideComponent' was called on '\${component.name}' in AOT mode. \` +
\`Overriding template or imports on AOT-compiled components is not fully supported and may cause \` +
\`NG0304/NG0303 element resolution errors. \\n\` +
\`👉 Workaround: Set 'aot: false' in the build configuration used for tests (e.g. 'buildTarget' in angular.json).\`
);
}
return originalOverrideComponent.call(this, component, override);
};
`
: ''
}
}
`;
}
Expand Down Expand Up @@ -279,6 +302,7 @@ export async function getVitestBuildOptions(
!options.debug,
zoneTestingStrategy,
hasLocalize,
buildOptions.aot !== false,
);

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

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Behavior: "TestBed.overrideComponent warning in AOT"', () => {
it('should warn when overrideComponent is called in AOT mode', async () => {
setupApplicationTarget(harness, {
aot: true,
});

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

harness.writeFile(
'src/app/aot-warning.spec.ts',
`
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

@Component({
selector: 'test-comp',
template: '',
})
class TestComponent {}

describe('Override Warning', () => {
let warnSpy: any;

beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

it('should log warning', () => {
TestBed.configureTestingModule({
imports: [TestComponent],
}).overrideComponent(TestComponent, {});

expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls[0][0]).toContain('WARNING: \\'TestBed.overrideComponent\\' was called');
});
});
`,
);

// Overwrite default to avoid noise
harness.writeFile(
'src/app/app.component.spec.ts',
`
import { describe, it, expect } from 'vitest';
describe('Ignored', () => { it('pass', () => expect(true).toBe(true)); });
`,
);

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

it('should NOT warn when overrideComponent is called in JIT mode', async () => {
setupApplicationTarget(harness, {
aot: false,
});

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

harness.writeFile(
'src/app/jit-no-warning.spec.ts',
`
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

@Component({
selector: 'test-comp',
template: '',
})
class TestComponent {}

describe('JIT No Warning', () => {
let warnSpy: any;

beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

it('should not log warning', () => {
TestBed.configureTestingModule({
imports: [TestComponent],
}).overrideComponent(TestComponent, {});

expect(warnSpy).not.toHaveBeenCalled();
});
});
`,
);

// Overwrite default to avoid noise
harness.writeFile(
'src/app/app.component.spec.ts',
`
import { describe, it, expect } from 'vitest';
describe('Ignored', () => { it('pass', () => expect(true).toBe(true)); });
`,
);

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