fix(ivy): TestBed should tolerate synchronous use of `compileComponents` (#28350)

TestBed.compileComponents has always been an async API. However,
ViewEngine tolerated using this API in a synchronous manner if the
components declared in the testing module did not have any async
resources (templateUrl, styleUrls). This change makes the ivy TestBed
mirror this tolerance by configuring such components synchronously.

Ref: FW-992

PR Close #28350
This commit is contained in:
Jeremy Elbourn 2019-01-23 15:09:27 -08:00 committed by Jason Aden
parent f8c70011b1
commit 3deda898d0
3 changed files with 54 additions and 19 deletions

View File

@ -91,8 +91,8 @@ export function maybeQueueResolutionOfComponentResources(metadata: Component) {
} }
} }
export function componentNeedsResolution(component: Component) { export function componentNeedsResolution(component: Component): boolean {
return component.templateUrl || component.styleUrls && component.styleUrls.length; return !!(component.templateUrl || component.styleUrls && component.styleUrls.length);
} }
export function clearResolutionOfComponentResourcesQueue() { export function clearResolutionOfComponentResourcesQueue() {
componentResourceResolutionQueue.clear(); componentResourceResolutionQueue.clear();

View File

@ -79,6 +79,10 @@ export class ComponentWithPropBindings {
export class SimpleApp { export class SimpleApp {
} }
@Component({selector: 'inline-template', template: '<p>Hello</p>'})
export class ComponentWithInlineTemplate {
}
@NgModule({ @NgModule({
declarations: [ declarations: [
HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings, HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings,
@ -232,6 +236,26 @@ describe('TestBed', () => {
expect(simpleApp.nativeElement).toHaveText('simple - inherited'); expect(simpleApp.nativeElement).toHaveText('simple - inherited');
}); });
it('should resolve components without async resources synchronously', (done) => {
TestBed
.configureTestingModule({
declarations: [ComponentWithInlineTemplate],
})
.compileComponents()
.then(done)
.catch(error => {
// This should not throw any errors. If an error is thrown, the test will fail.
// Specifically use `catch` here to mark the test as done and *then* throw the error
// so that the test isn't treated as a timeout.
done();
throw error;
});
// Intentionally call `createComponent` before `compileComponents` is resolved. We want this to
// work for components that don't have any async resources (templateUrl, styleUrls).
TestBed.createComponent(ComponentWithInlineTemplate);
});
onlyInIvy('patched ng defs should be removed after resetting TestingModule') onlyInIvy('patched ng defs should be removed after resetting TestingModule')
.it('make sure we restore ng defs to their initial states', () => { .it('make sure we restore ng defs to their initial states', () => {
@Pipe({name: 'somePipe', pure: true}) @Pipe({name: 'somePipe', pure: true})

View File

@ -54,7 +54,7 @@ import {
// clang-format on // clang-format on
import {ResourceLoader} from '@angular/compiler'; import {ResourceLoader} from '@angular/compiler';
import {clearResolutionOfComponentResourcesQueue, resolveComponentResources} from '../../src/metadata/resource_loading'; import {clearResolutionOfComponentResourcesQueue, componentNeedsResolution, resolveComponentResources} from '../../src/metadata/resource_loading';
import {ComponentFixture} from './component_fixture'; import {ComponentFixture} from './component_fixture';
import {MetadataOverride} from './metadata_override'; import {MetadataOverride} from './metadata_override';
import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers';
@ -374,6 +374,8 @@ export class TestBedRender3 implements Injector, TestBed {
const declarations: Type<any>[] = flatten(this._declarations || EMPTY_ARRAY, resolveForwardRef); const declarations: Type<any>[] = flatten(this._declarations || EMPTY_ARRAY, resolveForwardRef);
const componentOverrides: [Type<any>, Component][] = []; const componentOverrides: [Type<any>, Component][] = [];
let hasAsyncResources = false;
// Compile the components declared by this module // Compile the components declared by this module
declarations.forEach(declaration => { declarations.forEach(declaration => {
const component = resolvers.component.resolve(declaration); const component = resolvers.component.resolve(declaration);
@ -382,25 +384,34 @@ export class TestBedRender3 implements Injector, TestBed {
const metadata = {...component}; const metadata = {...component};
compileComponent(declaration, metadata); compileComponent(declaration, metadata);
componentOverrides.push([declaration, metadata]); componentOverrides.push([declaration, metadata]);
hasAsyncResources = hasAsyncResources || componentNeedsResolution(component);
} }
}); });
let resourceLoader: ResourceLoader; const overrideComponents = () => {
componentOverrides.forEach((override: [Type<any>, Component]) => {
// Override the existing metadata, ensuring that the resolved resources
// are only available until the next TestBed reset (when `resetTestingModule` is called)
this.overrideComponent(override[0], {set: override[1]});
});
};
return resolveComponentResources(url => { // If the component has no async resources (templateUrl, styleUrls), we can finish
if (!resourceLoader) { // synchronously. This is important so that users who mistakenly treat `compileComponents`
resourceLoader = this.compilerInjector.get(ResourceLoader); // as synchronous don't encounter an error, as ViewEngine was tolerant of this.
} if (!hasAsyncResources) {
return Promise.resolve(resourceLoader.get(url)); overrideComponents();
}) return Promise.resolve();
.then(() => { } else {
componentOverrides.forEach((override: [Type<any>, Component]) => { let resourceLoader: ResourceLoader;
// Once resolved, we override the existing metadata, ensuring that the resolved return resolveComponentResources(url => {
// resources if (!resourceLoader) {
// are only available until the next TestBed reset (when `resetTestingModule` is called) resourceLoader = this.compilerInjector.get(ResourceLoader);
this.overrideComponent(override[0], {set: override[1]}); }
}); return Promise.resolve(resourceLoader.get(url));
}); })
.then(overrideComponents);
}
} }
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {