feat(compiler): support sync runtime compile

Adds new abstraction `Compiler` with methods
`compileComponentAsync` and `compileComponentSync`.
This is in preparation of deprecating `ComponentResolver`.

`compileComponentSync` is able to compile components
synchronously given all components either have an inline
template or they have been compiled before.

Also changes `TestComponentBuilder.createSync` to
take a `Type` and use the new `compileComponentSync` method.

Also supports overriding the component metadata even if
the component has already been compiled.

Also fixes #7084 in a better way.

BREAKING CHANGE:
`TestComponentBuilder.createSync` now takes a component type
and throws if not all templates are either inlined
are compiled before via `createAsync`.

Closes #9594
This commit is contained in:
Tobias Bosch
2016-06-24 08:46:43 -07:00
parent 24eb8389d2
commit bf598d6b8b
35 changed files with 1093 additions and 700 deletions

View File

@ -10,7 +10,7 @@ import {TestComponentBuilder} from '@angular/compiler/testing';
import {ComponentFixture, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal';
import {isBlank, NumberWrapper,} from '../../src/facade/lang';
import {isBlank, NumberWrapper, ConcreteType,} from '../../src/facade/lang';
import {BaseException} from '../../src/facade/exceptions';
import {StringMapWrapper} from '../../src/facade/collection';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -40,9 +40,9 @@ export function main() {
var renderLog: RenderLog;
var directiveLog: DirectiveLog;
function createCompFixture(
template: string, compType: Type = TestComponent,
_tcb: TestComponentBuilder = null): ComponentFixture<any> {
function createCompFixture<T>(
template: string, compType: ConcreteType<T> = <any>TestComponent,
_tcb: TestComponentBuilder = null): ComponentFixture<T> {
if (isBlank(_tcb)) {
_tcb = tcb;
}
@ -58,18 +58,19 @@ export function main() {
return nodes.map(node => node.injector.get(dirType));
}
function _bindSimpleProp(
bindAttr: string, compType: Type = TestComponent): ComponentFixture<any> {
function _bindSimpleProp<T>(
bindAttr: string, compType: ConcreteType<T> = <any>TestComponent): ComponentFixture<T> {
var template = `<div ${bindAttr}></div>`;
return createCompFixture(template, compType);
}
function _bindSimpleValue(
expression: any, compType: Type = TestComponent): ComponentFixture<any> {
function _bindSimpleValue<T>(
expression: any, compType: ConcreteType<T> = <any>TestComponent): ComponentFixture<T> {
return _bindSimpleProp(`[someProp]='${expression}'`, compType);
}
function _bindAndCheckSimpleValue(expression: any, compType: Type = TestComponent): string[] {
function _bindAndCheckSimpleValue<T>(
expression: any, compType: ConcreteType<T> = <any>TestComponent): string[] {
var ctx = _bindSimpleValue(expression, compType);
ctx.detectChanges(false);
return renderLog.log;

View File

@ -1504,20 +1504,15 @@ function declareTests({useJit}: {useJit: boolean}) {
describe('error handling', () => {
it('should report a meaningful error when a directive is missing annotation',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
tcb = tcb.overrideView(
MyComp,
new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]}));
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb = tcb.overrideView(
MyComp,
new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]}));
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
expect(e.message).toEqual(
`No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`);
async.done();
return null;
});
}));
expect(() => tcb.createAsync(MyComp))
.toThrowError(
`No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`);
}));
it('should report a meaningful error when a component is missing view annotation',
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -1530,19 +1525,13 @@ function declareTests({useJit}: {useJit: boolean}) {
}));
it('should report a meaningful error when a directive is null',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
tcb =
tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''}));
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb = tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''}));
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
expect(e.message).toEqual(
`Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`);
async.done();
return null;
});
}));
expect(() => tcb.createAsync(MyComp))
.toThrowError(
`Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`);
}));
it('should provide an error context when an error happens in DI',
inject(
@ -1642,22 +1631,17 @@ function declareTests({useJit}: {useJit: boolean}) {
if (!IS_DART) {
it('should report a meaningful error when a directive is undefined',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
var undefinedValue: any = void(0);
var undefinedValue: any = void(0);
tcb = tcb.overrideView(
MyComp, new ViewMetadata({directives: [undefinedValue], template: ''}));
tcb = tcb.overrideView(
MyComp, new ViewMetadata({directives: [undefinedValue], template: ''}));
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
expect(e.message).toEqual(
`Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`);
async.done();
return null;
});
}));
expect(() => tcb.createAsync(MyComp))
.toThrowError(
`Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`);
}));
}
it('should specify a location of an error that happened during change detection (text)',

View File

@ -261,18 +261,14 @@ export function main() {
}));
it('should throw with descriptive error when query selectors are not present',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
tcb.overrideTemplate(
MyCompBroken0, '<has-null-query-condition></has-null-query-condition>')
.createAsync(MyCompBroken0)
.catch((e) => {
expect(e.message).toEqual(
`Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`);
async.done();
});
}));
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
expect(
() => tcb.overrideTemplate(
MyCompBroken0, '<has-null-query-condition></has-null-query-condition>')
.createAsync(MyCompBroken0))
.toThrowError(
`Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`);
}));
});
describe('query for TemplateRef', () => {

View File

@ -12,7 +12,7 @@ import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
import {IS_DART} from '../../src/facade/lang';
import {Component, Pipe, PipeTransform, provide, ViewMetadata, OpaqueToken, Injector} from '@angular/core';
import {Component, Pipe, PipeTransform, provide, ViewMetadata, PLATFORM_PIPES, OpaqueToken, Injector, forwardRef} from '@angular/core';
import {NgIf, NgClass} from '@angular/common';
import {CompilerConfig} from '@angular/compiler';
@ -170,8 +170,7 @@ function declareTests({useJit}: {useJit: boolean}) {
it('should support ngClass before a component and content projection inside of an ngIf',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => {
tcb.overrideView(
MyComp1, new ViewMetadata({
template: `A<cmp-content *ngIf="true" [ngClass]="'red'">B</cmp-content>C`,
@ -185,6 +184,15 @@ function declareTests({useJit}: {useJit: boolean}) {
});
}));
it('should handle mutual recursion entered from multiple sides - #7084',
inject(
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => {
tcb.createAsync(FakeRecursiveComp).then((fixture) => {
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('[]');
async.done();
});
}));
});
}
@ -207,3 +215,37 @@ class CustomPipe implements PipeTransform {
@Component({selector: 'cmp-content', template: `<ng-content></ng-content>`})
class CmpWithNgContent {
}
@Component({
selector: 'left',
template: `L<right *ngIf="false"></right>`,
directives: [
NgIf,
forwardRef(() => RightComp),
]
})
class LeftComp {
}
@Component({
selector: 'right',
template: `R<left *ngIf="false"></left>`,
directives: [
NgIf,
forwardRef(() => LeftComp),
]
})
class RightComp {
}
@Component({
selector: 'fakeRecursiveComp',
template: `[<left *ngIf="false"></left><right *ngIf="false"></right>]`,
directives: [
NgIf,
forwardRef(() => LeftComp),
forwardRef(() => RightComp),
]
})
export class FakeRecursiveComp {
}

View File

@ -9,7 +9,7 @@
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, beforeEachProviders, inject,} from '@angular/core/testing/testing_internal';
import {fakeAsync, flushMicrotasks, tick, ComponentFixture} from '@angular/core/testing';
import {TestComponentBuilder} from '@angular/compiler/testing';
import {isBlank} from '../../src/facade/lang';
import {isBlank, ConcreteType} from '../../src/facade/lang';
import {Type, ViewContainerRef, TemplateRef, ElementRef, ChangeDetectorRef, ChangeDetectionStrategy, Directive, Component, DebugElement, forwardRef, Input, PipeTransform, Attribute, ViewMetadata, provide, Optional, Inject, Self, InjectMetadata, Pipe, Host, SkipSelfMetadata} from '@angular/core';
import {NgIf, NgFor} from '@angular/common';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -242,10 +242,11 @@ class TestComp {
export function main() {
var tcb: TestComponentBuilder;
function createCompFixture(
template: string, tcb: TestComponentBuilder, comp: Type = null): ComponentFixture<any> {
function createCompFixture<T>(
template: string, tcb: TestComponentBuilder,
comp: ConcreteType<T> = null): ComponentFixture<T> {
if (isBlank(comp)) {
comp = TestComp;
comp = <any>TestComp;
}
return tcb
.overrideView(
@ -255,7 +256,7 @@ export function main() {
}
function createComp(
template: string, tcb: TestComponentBuilder, comp: Type = null): DebugElement {
template: string, tcb: TestComponentBuilder, comp: ConcreteType<any> = null): DebugElement {
var fixture = createCompFixture(template, tcb, comp);
fixture.detectChanges();
return fixture.debugElement;