mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Alex Rickabaugh
parent
4f1a813596
commit
701405fa71
@@ -6,12 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
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';
|
||||
import {CommonModule} from '@angular/common';
|
||||
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';
|
||||
import {getTestBed, TestBed} from '@angular/core/testing/src/test_bed';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
import {getNgModuleById} from '../public_api';
|
||||
import {TestBedRender3} from '../testing/src/r3_test_bed';
|
||||
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from '../testing/src/test_bed_common';
|
||||
|
||||
@@ -1426,6 +1426,8 @@ describe('TestBed', () => {
|
||||
* Function returns a class that represents AOT-compiled version of the following Component:
|
||||
*
|
||||
* @Component({
|
||||
* standalone: true|false,
|
||||
* imports: [...], // for standalone only
|
||||
* selector: 'comp',
|
||||
* templateUrl: './template.ng.html',
|
||||
* styleUrls: ['./style.css']
|
||||
@@ -1436,18 +1438,23 @@ describe('TestBed', () => {
|
||||
* outside of TestBed) without changing TestBed state and/or Component metadata to compile
|
||||
* them via TestBed with external resources.
|
||||
*/
|
||||
const getAOTCompiledComponent = () => {
|
||||
const getAOTCompiledComponent = (standalone: boolean = false, dependencies: any[] = []) => {
|
||||
class ComponentClass {
|
||||
static ɵfac = () => new ComponentClass();
|
||||
static ɵcmp = defineComponent({
|
||||
standalone,
|
||||
type: ComponentClass,
|
||||
selectors: [['comp']],
|
||||
decls: 1,
|
||||
decls: 2,
|
||||
vars: 0,
|
||||
dependencies,
|
||||
consts: [['dir']],
|
||||
template:
|
||||
(rf: any, ctx: any) => {
|
||||
if (rf & 1) {
|
||||
text(0, 'Some template');
|
||||
elementStart(0, 'div', 0);
|
||||
text(1, 'Some template');
|
||||
elementEnd();
|
||||
}
|
||||
},
|
||||
styles: ['body { margin: 0; }']
|
||||
@@ -1457,6 +1464,8 @@ describe('TestBed', () => {
|
||||
ComponentClass, [{
|
||||
type: Component,
|
||||
args: [{
|
||||
standalone,
|
||||
imports: dependencies,
|
||||
selector: 'comp',
|
||||
templateUrl: './template.ng.html',
|
||||
styleUrls: ['./style.css'],
|
||||
@@ -1466,6 +1475,30 @@ describe('TestBed', () => {
|
||||
return ComponentClass;
|
||||
};
|
||||
|
||||
it('should allow to override a provider used in a dependency of a standalone component', () => {
|
||||
const A = new InjectionToken('A');
|
||||
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
providers: [{provide: A, useValue: 'A'}],
|
||||
})
|
||||
class SomeDir {
|
||||
constructor(@Inject(A) private tokenA: string, private elementRef: ElementRef) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.elementRef.nativeElement.innerHTML = this.tokenA;
|
||||
}
|
||||
}
|
||||
|
||||
const SomeComponent = getAOTCompiledComponent(true, [SomeDir]);
|
||||
TestBed.configureTestingModule({imports: [SomeComponent]});
|
||||
TestBed.overrideProvider(A, {useValue: 'Overridden A'});
|
||||
const fixture = TestBed.createComponent(SomeComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('Overridden A');
|
||||
});
|
||||
|
||||
it('should have an ability to override template', () => {
|
||||
const SomeComponent = getAOTCompiledComponent();
|
||||
TestBed.configureTestingModule({declarations: [SomeComponent]});
|
||||
@@ -1515,7 +1548,8 @@ describe('TestBed', () => {
|
||||
});
|
||||
const fixture = TestBed.createComponent(TestFixture);
|
||||
// The regex avoids any issues with styling attributes.
|
||||
expect(fixture.nativeElement.innerHTML).toMatch(/<comp[^>]*>Some template<\/comp>/);
|
||||
expect(fixture.nativeElement.innerHTML)
|
||||
.toMatch(/<comp[^>]*><div[^>]*>Some template<\/div><\/comp>/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -441,7 +441,14 @@ export class R3TestBedCompiler {
|
||||
const def = getComponentDef(moduleType);
|
||||
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
|
||||
for (const dependency of dependencies) {
|
||||
this.applyProviderOverridesToModule(dependency);
|
||||
// Proceed with examining dependencies recursively
|
||||
// when a dependency is a standalone component or an NgModule.
|
||||
// In AOT, the `dependencies` might also contain regular (NgModule-based)
|
||||
// Component, Directive and Pipes. Skip them here, they are handled in a
|
||||
// different location (in the `configureTestingModule` function).
|
||||
if (isStandaloneComponent(dependency) || hasNgModuleDef(dependency)) {
|
||||
this.applyProviderOverridesToModule(dependency);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const providers = [
|
||||
@@ -577,7 +584,18 @@ export class R3TestBedCompiler {
|
||||
} else if (isStandaloneComponent(value)) {
|
||||
this.queueType(value, null);
|
||||
const def = getComponentDef(value);
|
||||
queueTypesFromModulesArrayRecur(maybeUnwrapFn(def.dependencies ?? []));
|
||||
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
|
||||
dependencies.forEach((dependency) => {
|
||||
// Note: in AOT, the `dependencies` might also contain regular
|
||||
// (NgModule-based) Component, Directive and Pipes, so we handle
|
||||
// them separately and proceed with recursive process for standalone
|
||||
// Components and NgModules only.
|
||||
if (isStandaloneComponent(dependency) || hasNgModuleDef(dependency)) {
|
||||
queueTypesFromModulesArrayRecur([dependency]);
|
||||
} else {
|
||||
this.queueType(dependency, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user