Skip to content
Merged
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
14 changes: 7 additions & 7 deletions adev/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {provideHttpClient} from '@angular/common/http';
import {provideHttpClientTesting} from '@angular/common/http/testing';
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
import {Search, WINDOW} from '@angular/docs';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import {AppComponent} from './app.component';
import {routes} from './routing/routes';
import {Search, WINDOW} from '@angular/docs';
import {provideHttpClient} from '@angular/common/http';
import {provideHttpClientTesting} from '@angular/common/http/testing';

describe('AppComponent', () => {
const fakeSearch = {};
const fakeWindow = {location: {hostname: 'angular.dev'}};

it('should create the app', async () => {
await TestBed.configureTestingModule({
it('should create the app', () => {
TestBed.configureTestingModule({
imports: [AppComponent],
providers: [
provideHttpClient(),
Expand All @@ -34,7 +34,7 @@ describe('AppComponent', () => {
useValue: fakeSearch,
},
],
}).compileComponents();
});

const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
Expand Down
2 changes: 1 addition & 1 deletion dev-app/src/app/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
});

it('should create the app', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import {ComponentFixture, TestBed} from '@angular/core/testing';

import {RouterTreeComponent} from './router-tree.component';
import SpyObj = jasmine.SpyObj;
import {FrameManager} from '../../application-services/frame_manager';
import {provideZoneChangeDetection} from '@angular/core';
import {Events, MessageBus} from '../../../../../protocol';
import {ApplicationOperations} from '../../application-operations';
import {provideZoneChangeDetection} from '@angular/core';
import {FrameManager} from '../../application-services/frame_manager';
import {RouterTreeComponent} from './router-tree.component';
import SpyObj = jasmine.SpyObj;

describe('RouterTreeComponent', () => {
let messageBus: MessageBus<Events>;
Expand All @@ -39,7 +39,7 @@ describe('RouterTreeComponent', () => {
{provide: MessageBus, useValue: messageBus},
{provide: FrameManager, useValue: frameManager},
],
}).compileComponents();
});

fixture = TestBed.createComponent(RouterTreeComponent);
fixture.componentRef.setInput('routes', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('AppComponent', () => {
useClass: applicationOperationsSPy,
},
],
}).compileComponents();
});
}));

it('should create the app', () => {
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/render3/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface TypeWithMetadata extends Type<any> {
*/
const ASYNC_COMPONENT_METADATA_FN = '__ngAsyncComponentMetadataFn__';

const ASYNC_METADATA_LOADED = '__ngAsyncMetadataLoaded__';

/**
* If a given component has unresolved async metadata - returns a reference
* to a function that applies component metadata after resolving defer-loadable
Expand All @@ -31,9 +33,22 @@ export function getAsyncClassMetadataFn(
type: Type<unknown>,
): (() => Promise<Array<Type<unknown>>>) | null {
const componentClass = type as any; // cast to `any`, so that we can read a monkey-patched field
if (componentClass[ASYNC_COMPONENT_METADATA_FN] === ASYNC_METADATA_LOADED) {
return null;
}

return componentClass[ASYNC_COMPONENT_METADATA_FN] ?? null;
}

/**
* Checks if a given component has async metadata.
* Independent of whether the metadata is already loaded or not.
*/
export function hasAsyncClassMetadata(type: Type<unknown>): boolean {
const componentClass = type as any; // cast to `any`, so that we can monkey-patch it
return !!componentClass[ASYNC_COMPONENT_METADATA_FN];
}

/**
* Handles the process of applying metadata info to a component class in case
* component template has defer blocks (thus some dependencies became deferrable).
Expand All @@ -53,7 +68,7 @@ export function setClassMetadataAsync(
metadataSetterFn(...dependencies);
// Metadata is now set, reset field value to indicate that this component
// can by used/compiled synchronously.
componentClass[ASYNC_COMPONENT_METADATA_FN] = null;
componentClass[ASYNC_COMPONENT_METADATA_FN] = ASYNC_METADATA_LOADED;

return dependencies;
});
Expand Down
228 changes: 195 additions & 33 deletions packages/core/test/test_bed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1701,38 +1701,141 @@ describe('TestBed', () => {
},
});
}
setClassMetadataAsync(
ComponentClass,
function () {
const promises: Array<Promise<Type<unknown>>> = deferrableDependencies.map(
// Emulates a dynamic import, e.g. `import('./cmp-a').then(m => m.CmpA)`
(dep) => new Promise((resolve) => setTimeout(() => resolve(dep))),
);
return promises;
},
function (...deferrableSymbols) {
setClassMetadata(
ComponentClass,
[
{
type: Component,
args: [
{
selector,
imports: [...dependencies, ...deferrableSymbols],
template: `<div>root cmp!</div>`,
},
],
},
],
null,
null,
);
},
);
if (dependencies.length || deferrableDependencies.length) {
setClassMetadataAsync(
ComponentClass,
function () {
const promises: Array<Promise<Type<unknown>>> = deferrableDependencies.map(
// Emulates a dynamic import, e.g. `import('./cmp-a').then(m => m.CmpA)`
(dep) => new Promise((resolve) => setTimeout(() => resolve(dep))),
);
return promises;
},
function (...deferrableSymbols) {
setClassMetadata(
ComponentClass,
[
{
type: Component,
args: [
{
selector,
imports: [...dependencies, ...deferrableSymbols],
template: `<div>root cmp!</div>`,
},
],
},
],
null,
null,
);
},
);
}
return ComponentClass;
};

it('should not require compilerComponents if the component with a defer-block is not overridden', async () => {
const DeferredComponent = getAOTCompiledComponent('deferred');
const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]);

TestBed.configureTestingModule({imports: [RootAotComponent]});

// If we had overriden the component, we would need to compile it
// but since we didn't, we can create the component synchronously
const fixture = TestBed.createComponent(RootAotComponent);
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toBe('root cmp!');

// Here we're just confirming that the same component would throw on override.
TestBed.resetTestingModule()
.configureTestingModule({imports: [RootAotComponent]})
.overrideComponent(RootAotComponent, {
set: {template: `Override of a root template!`},
});
expect(() => TestBed.createComponent(RootAotComponent)).toThrow();
});

it('should not throw an error in AOT component is overriden but has no async metadata', () => {
const RootAotComponent = getAOTCompiledComponent('root', [], []);
TestBed.configureTestingModule({imports: [RootAotComponent]});

TestBed.overrideComponent(RootAotComponent, {
set: {template: `Override of a root template! <nested-cmp />`},
});

expect(() => TestBed.createComponent(RootAotComponent)).not.toThrowError();
});

it('should throw an error if a deferred component is not compiled', () => {
@Component({
selector: 'cmp-a',
template: 'CmpA!',
})
class CmpA {}

const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], [CmpA]);
const RootAotComponent = getAOTCompiledComponent('root', [], [NestedAotComponent]);

TestBed.configureTestingModule({imports: [RootAotComponent]});

TestBed.overrideComponent(RootAotComponent, {
set: {template: `Override of a root template! <nested-cmp />`},
});
TestBed.overrideComponent(NestedAotComponent, {
set: {template: `Override of a nested template! <cmp-a />`},
});

// We did override but not compile, therefore we expect to throw on createComponent
expect(() => TestBed.createComponent(RootAotComponent)).toThrowError(
`Component 'ComponentClass' has unresolved metadata. Please call \`await TestBed.compileComponents()\` before running this test.`,
);
});

it('should not throw if component is created after override+reset', async () => {
@Component({
selector: 'cmp-a',
template: 'CmpA!',
})
class CmpA {}

const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], [CmpA]);
const RootAotComponent = getAOTCompiledComponent('root', [], [NestedAotComponent]);

TestBed.configureTestingModule({imports: [RootAotComponent]});

TestBed.overrideComponent(RootAotComponent, {
set: {template: `Override of a root template! <nested-cmp />`},
});
TestBed.overrideComponent(NestedAotComponent, {
set: {template: `Override of a nested template! <cmp-a />`},
});

// Not compiled yet, so we expect to throw
expect(() => TestBed.createComponent(RootAotComponent)).toThrowError();

await TestBed.compileComponents();

// We're compiled now, so we can create the component
const fixture = TestBed.createComponent(RootAotComponent);
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toBe(
'Override of a root template! Override of a nested template! CmpA!',
);

// We reset the override
TestBed.resetTestingModule();
TestBed.configureTestingModule({imports: [RootAotComponent]});

// We're back to the nominal behavior
const fixture2 = TestBed.createComponent(RootAotComponent);
fixture2.detectChanges();

expect(fixture2.nativeElement.textContent).toBe('root cmp!');
});

it('should handle async metadata on root and nested components', async () => {
@Component({
selector: 'cmp-a',
Expand All @@ -1752,7 +1855,7 @@ describe('TestBed', () => {
set: {template: `Override of a nested template! <cmp-a />`},
});

// This is only required because the components are AOT compiled and thus include setClassMetadataAsync
// We need to compile the component because it has async metadata + overrides
await TestBed.compileComponents();

const fixture = TestBed.createComponent(RootAotComponent);
Expand Down Expand Up @@ -1839,7 +1942,7 @@ describe('TestBed', () => {
TestBed.configureTestingModule({imports: [ParentCmp], providers: [COMMON_PROVIDERS]});
TestBed.overrideProvider(ImportantService, {useValue: {value: 'overridden'}});

// This is only required because the component has setClassMetadataAsync
// We need to compile the component because it has async metadata + overrides
await TestBed.compileComponents();

const fixture = TestBed.createComponent(ParentCmp);
Expand All @@ -1852,8 +1955,9 @@ describe('TestBed', () => {
});

it('should allow import overrides on components with async metadata', async () => {
const DeferredComponent = getAOTCompiledComponent('deferred', [], []);
const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], []);
const RootAotComponent = getAOTCompiledComponent('root', [], []);
const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]);

TestBed.configureTestingModule({imports: [RootAotComponent]});

Expand All @@ -1865,7 +1969,7 @@ describe('TestBed', () => {
},
});

// This is only required because the components are AOT compiled and thus include setClassMetadataAsync
// We need to compile the component because it has async metadata + overrides
await TestBed.compileComponents();

const fixture = TestBed.createComponent(RootAotComponent);
Expand All @@ -1875,6 +1979,64 @@ describe('TestBed', () => {
'Override of a root template! nested-cmp cmp!',
);
});

it('should throw when overriding template', async () => {
const DeferredComponent = getAOTCompiledComponent('deferred', [], []);
const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]);
TestBed.overrideTemplate(RootAotComponent, 'foobar');

expect(() => TestBed.createComponent(RootAotComponent)).toThrowError(
/has unresolved metadata/,
);
});

it('should throw if overriding template', async () => {
const DeferredComponent = getAOTCompiledComponent('deferred', [], []);
const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]);
class Foo {}

TestBed.overrideComponent(RootAotComponent, {set: {providers: [Foo]}});

expect(() => TestBed.createComponent(RootAotComponent)).toThrowError(
/has unresolved metadata/,
);
});

describe('ensure AsyncMetadata loading is side-effect free', () => {
let Component: any;
let run = 1;
beforeEach(() => {
const DeferredComponent = getAOTCompiledComponent('deferred', [], []);
Component = getAOTCompiledComponent('root', [], [DeferredComponent]);
TestBed.overrideComponent(Component, {set: {template: 'bar'}});
});

it('should throw if creating an overridden component', async () => {
if (run === 1) {
await TestBed.compileComponents();
const fixture = TestBed.createComponent(Component);
expect(fixture.nativeElement.textContent).toBe('bar');
} else {
// Component was compiled in the previous test
// but we still require compileComponents because of the override
expect(() => TestBed.createComponent(Component)).toThrowError();
}
run++;
});

it('should throw if creating an overridden component (run 2)', async () => {
if (run === 1) {
await TestBed.compileComponents();
const fixture = TestBed.createComponent(Component);
expect(fixture.nativeElement.textContent).toBe('bar');
} else {
// Component was compiled in the previous test
// but we still require compileComponents because of the override
expect(() => TestBed.createComponent(Component)).toThrowError();
}
run++;
});
});
});

describe('AOT pre-compiled components', () => {
Expand Down
Loading
Loading