Skip to content

Commit 701405f

Browse files
AndrewKushniralxhub
authored andcommitted
fix(core): handle AOT-compiled standalone components in TestBed correctly (#46052)
Previously, the code in TestBed didn't take into account the fact that the `cmp.dependencies` array after the AOT compilation might contain regular (NgModule-based) Components/Directive/Pipes. As a result, some NgModule-specific code paths were invoked for non-NgModule types, thus leading to errors. This commit updates the code to handle AOT-compiled structure of standalone components correctly. PR Close #46052
1 parent 4f1a813 commit 701405f

File tree

2 files changed

+60
-8
lines changed

2 files changed

+60
-8
lines changed

packages/core/test/test_bed_spec.ts

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

9-
import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core';
9+
import {CommonModule} from '@angular/common';
10+
import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, Inject, Injectable, InjectionToken, 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';
1011
import {getTestBed, TestBed} from '@angular/core/testing/src/test_bed';
1112
import {By} from '@angular/platform-browser';
1213
import {expect} from '@angular/platform-browser/testing/src/matchers';
1314

14-
import {getNgModuleById} from '../public_api';
1515
import {TestBedRender3} from '../testing/src/r3_test_bed';
1616
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from '../testing/src/test_bed_common';
1717

@@ -1426,6 +1426,8 @@ describe('TestBed', () => {
14261426
* Function returns a class that represents AOT-compiled version of the following Component:
14271427
*
14281428
* @Component({
1429+
* standalone: true|false,
1430+
* imports: [...], // for standalone only
14291431
* selector: 'comp',
14301432
* templateUrl: './template.ng.html',
14311433
* styleUrls: ['./style.css']
@@ -1436,18 +1438,23 @@ describe('TestBed', () => {
14361438
* outside of TestBed) without changing TestBed state and/or Component metadata to compile
14371439
* them via TestBed with external resources.
14381440
*/
1439-
const getAOTCompiledComponent = () => {
1441+
const getAOTCompiledComponent = (standalone: boolean = false, dependencies: any[] = []) => {
14401442
class ComponentClass {
14411443
static ɵfac = () => new ComponentClass();
14421444
static ɵcmp = defineComponent({
1445+
standalone,
14431446
type: ComponentClass,
14441447
selectors: [['comp']],
1445-
decls: 1,
1448+
decls: 2,
14461449
vars: 0,
1450+
dependencies,
1451+
consts: [['dir']],
14471452
template:
14481453
(rf: any, ctx: any) => {
14491454
if (rf & 1) {
1450-
text(0, 'Some template');
1455+
elementStart(0, 'div', 0);
1456+
text(1, 'Some template');
1457+
elementEnd();
14511458
}
14521459
},
14531460
styles: ['body { margin: 0; }']
@@ -1457,6 +1464,8 @@ describe('TestBed', () => {
14571464
ComponentClass, [{
14581465
type: Component,
14591466
args: [{
1467+
standalone,
1468+
imports: dependencies,
14601469
selector: 'comp',
14611470
templateUrl: './template.ng.html',
14621471
styleUrls: ['./style.css'],
@@ -1466,6 +1475,30 @@ describe('TestBed', () => {
14661475
return ComponentClass;
14671476
};
14681477

1478+
it('should allow to override a provider used in a dependency of a standalone component', () => {
1479+
const A = new InjectionToken('A');
1480+
1481+
@Directive({
1482+
selector: '[dir]',
1483+
providers: [{provide: A, useValue: 'A'}],
1484+
})
1485+
class SomeDir {
1486+
constructor(@Inject(A) private tokenA: string, private elementRef: ElementRef) {}
1487+
1488+
ngAfterViewInit() {
1489+
this.elementRef.nativeElement.innerHTML = this.tokenA;
1490+
}
1491+
}
1492+
1493+
const SomeComponent = getAOTCompiledComponent(true, [SomeDir]);
1494+
TestBed.configureTestingModule({imports: [SomeComponent]});
1495+
TestBed.overrideProvider(A, {useValue: 'Overridden A'});
1496+
const fixture = TestBed.createComponent(SomeComponent);
1497+
fixture.detectChanges();
1498+
1499+
expect(fixture.nativeElement.textContent).toBe('Overridden A');
1500+
});
1501+
14691502
it('should have an ability to override template', () => {
14701503
const SomeComponent = getAOTCompiledComponent();
14711504
TestBed.configureTestingModule({declarations: [SomeComponent]});
@@ -1515,7 +1548,8 @@ describe('TestBed', () => {
15151548
});
15161549
const fixture = TestBed.createComponent(TestFixture);
15171550
// The regex avoids any issues with styling attributes.
1518-
expect(fixture.nativeElement.innerHTML).toMatch(/<comp[^>]*>Some template<\/comp>/);
1551+
expect(fixture.nativeElement.innerHTML)
1552+
.toMatch(/<comp[^>]*><div[^>]*>Some template<\/div><\/comp>/);
15191553
});
15201554
});
15211555

packages/core/testing/src/r3_test_bed_compiler.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,14 @@ export class R3TestBedCompiler {
441441
const def = getComponentDef(moduleType);
442442
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
443443
for (const dependency of dependencies) {
444-
this.applyProviderOverridesToModule(dependency);
444+
// Proceed with examining dependencies recursively
445+
// when a dependency is a standalone component or an NgModule.
446+
// In AOT, the `dependencies` might also contain regular (NgModule-based)
447+
// Component, Directive and Pipes. Skip them here, they are handled in a
448+
// different location (in the `configureTestingModule` function).
449+
if (isStandaloneComponent(dependency) || hasNgModuleDef(dependency)) {
450+
this.applyProviderOverridesToModule(dependency);
451+
}
445452
}
446453
} else {
447454
const providers = [
@@ -577,7 +584,18 @@ export class R3TestBedCompiler {
577584
} else if (isStandaloneComponent(value)) {
578585
this.queueType(value, null);
579586
const def = getComponentDef(value);
580-
queueTypesFromModulesArrayRecur(maybeUnwrapFn(def.dependencies ?? []));
587+
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
588+
dependencies.forEach((dependency) => {
589+
// Note: in AOT, the `dependencies` might also contain regular
590+
// (NgModule-based) Component, Directive and Pipes, so we handle
591+
// them separately and proceed with recursive process for standalone
592+
// Components and NgModules only.
593+
if (isStandaloneComponent(dependency) || hasNgModuleDef(dependency)) {
594+
queueTypesFromModulesArrayRecur([dependency]);
595+
} else {
596+
this.queueType(dependency, null);
597+
}
598+
});
581599
}
582600
}
583601
};

0 commit comments

Comments
 (0)