Skip to content

Commit 586cc24

Browse files
fix(core): apply TestBed provider overrides to @defer dependencies (#54667)
This commit updates TestBed logic to take into account situations when dependencies loaded within `@defer` blocks may import NgModules with providers. For such components, we invoke provider override function, which recursively inspects and applies the necessary changes. PR Close #54667
1 parent b558a01 commit 586cc24

File tree

2 files changed

+111
-8
lines changed

2 files changed

+111
-8
lines changed

packages/core/test/test_bed_spec.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelementEnd as elementEnd, ɵɵelementStart as elementStart, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core';
9+
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
10+
import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, PLATFORM_ID, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelementEnd as elementEnd, ɵɵelementStart as elementStart, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core';
1011
import {DeferBlockBehavior} from '@angular/core/testing';
1112
import {TestBed, TestBedImpl} from '@angular/core/testing/src/test_bed';
1213
import {By} from '@angular/platform-browser';
@@ -1655,15 +1656,90 @@ describe('TestBed', () => {
16551656
.toBe('Override of a root template! Override of a nested template! CmpA!');
16561657
});
16571658

1658-
it('should allow import overrides on components with async metadata', async () => {
1659+
it('should override providers on dependencies of dynamically loaded components', async () => {
1660+
function timer(delay: number): Promise<void> {
1661+
return new Promise<void>((resolve) => {
1662+
setTimeout(() => resolve(), delay);
1663+
});
1664+
}
1665+
1666+
@Injectable({providedIn: 'root'})
1667+
class ImportantService {
1668+
value = 'original';
1669+
}
1670+
1671+
@NgModule({
1672+
providers: [ImportantService],
1673+
})
1674+
class ThisModuleProvidesService {
1675+
}
1676+
16591677
@Component({
16601678
standalone: true,
1661-
selector: 'cmp-a',
1662-
template: 'CmpA!',
1679+
selector: 'child',
1680+
imports: [ThisModuleProvidesService],
1681+
template: '<h1>{{value}}</h1>',
16631682
})
1664-
class CmpA {
1683+
class ChildCmp {
1684+
service = inject(ImportantService);
1685+
value = this.service.value;
1686+
}
1687+
1688+
@Component({
1689+
standalone: true,
1690+
selector: 'parent',
1691+
imports: [ChildCmp],
1692+
template: `
1693+
@defer (when true) {
1694+
<child />
1695+
}
1696+
`,
1697+
})
1698+
class ParentCmp {
16651699
}
16661700

1701+
const deferrableDependencies = [ChildCmp];
1702+
setClassMetadataAsync(
1703+
ParentCmp,
1704+
function() {
1705+
const promises: Array<Promise<Type<unknown>>> = deferrableDependencies.map(
1706+
// Emulates a dynamic import, e.g. `import('./cmp-a').then(m => m.CmpA)`
1707+
dep => new Promise((resolve) => setTimeout(() => resolve(dep))));
1708+
return promises;
1709+
},
1710+
function(...deferrableSymbols) {
1711+
setClassMetadata(
1712+
ParentCmp, [{
1713+
type: Component,
1714+
args: [{
1715+
selector: 'parent',
1716+
standalone: true,
1717+
imports: [...deferrableSymbols],
1718+
template: `<div>root cmp!</div>`,
1719+
}]
1720+
}],
1721+
null, null);
1722+
});
1723+
1724+
// Set `PLATFORM_ID` to a browser platform value to trigger defer loading
1725+
// while running tests in Node.
1726+
const COMMON_PROVIDERS = [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}];
1727+
1728+
TestBed.configureTestingModule({imports: [ParentCmp], providers: [COMMON_PROVIDERS]});
1729+
TestBed.overrideProvider(ImportantService, {useValue: {value: 'overridden'}});
1730+
1731+
await TestBed.compileComponents();
1732+
1733+
const fixture = TestBed.createComponent(ParentCmp);
1734+
fixture.detectChanges();
1735+
1736+
await timer(10);
1737+
fixture.detectChanges();
1738+
1739+
expect(fixture.nativeElement.textContent).toContain('overridden');
1740+
});
1741+
1742+
it('should allow import overrides on components with async metadata', async () => {
16671743
const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], []);
16681744
const RootAotComponent = getAOTCompiledComponent('root', [], []);
16691745

packages/core/testing/src/test_bed_compiler.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export class TestBedCompiler {
6565
private pendingDirectives = new Set<Type<any>>();
6666
private pendingPipes = new Set<Type<any>>();
6767

68+
// Set of components with async metadata, i.e. components with `@defer` blocks
69+
// in their templates.
70+
private componentsWithAsyncMetadata = new Set<Type<unknown>>();
71+
6872
// Keep track of all components and directives, so we can patch Providers onto defs later.
6973
private seenComponents = new Set<Type<any>>();
7074
private seenDirectives = new Set<Type<any>>();
@@ -177,6 +181,10 @@ export class TestBedCompiler {
177181
this.verifyNoStandaloneFlagOverrides(component, override);
178182
this.resolvers.component.addOverride(component, override);
179183
this.pendingComponents.add(component);
184+
185+
// If this is a component with async metadata (i.e. a component with a `@defer` block
186+
// in a template) - store it for future processing.
187+
this.maybeRegisterComponentWithAsyncMetadata(component);
180188
}
181189

182190
overrideDirective(directive: Type<any>, override: MetadataOverride<Directive>): void {
@@ -265,18 +273,26 @@ export class TestBedCompiler {
265273
}
266274

267275
private async resolvePendingComponentsWithAsyncMetadata() {
268-
if (this.pendingComponents.size === 0) return;
276+
if (this.componentsWithAsyncMetadata.size === 0) return;
269277

270278
const promises = [];
271-
for (const component of this.pendingComponents) {
279+
for (const component of this.componentsWithAsyncMetadata) {
272280
const asyncMetadataFn = getAsyncClassMetadataFn(component);
273281
if (asyncMetadataFn) {
274282
promises.push(asyncMetadataFn());
275283
}
276284
}
285+
this.componentsWithAsyncMetadata.clear();
277286

278287
const resolvedDeps = await Promise.all(promises);
279-
this.queueTypesFromModulesArray(resolvedDeps.flat(2));
288+
const flatResolvedDeps = resolvedDeps.flat(2);
289+
this.queueTypesFromModulesArray(flatResolvedDeps);
290+
291+
// Loaded standalone components might contain imports of NgModules
292+
// with providers, make sure we override providers there too.
293+
for (const component of flatResolvedDeps) {
294+
this.applyProviderOverridesInScope(component);
295+
}
280296
}
281297

282298
async compileComponents(): Promise<void> {
@@ -590,7 +606,18 @@ export class TestBedCompiler {
590606
compileNgModuleDefs(ngModule as NgModuleType<any>, metadata);
591607
}
592608

609+
private maybeRegisterComponentWithAsyncMetadata(type: Type<unknown>) {
610+
const asyncMetadataFn = getAsyncClassMetadataFn(type);
611+
if (asyncMetadataFn) {
612+
this.componentsWithAsyncMetadata.add(type);
613+
}
614+
}
615+
593616
private queueType(type: Type<any>, moduleType: Type<any>|TestingModuleOverride|null): void {
617+
// If this is a component with async metadata (i.e. a component with a `@defer` block
618+
// in a template) - store it for future processing.
619+
this.maybeRegisterComponentWithAsyncMetadata(type);
620+
594621
const component = this.resolvers.component.resolve(type);
595622
if (component) {
596623
// Check whether a give Type has respective NG def (ɵcmp) and compile if def is

0 commit comments

Comments
 (0)