refactor(view_compiler): codegen DI and Queries

BREAKING CHANGE:
- Renderer:
  * renderComponent method is removed form `Renderer`, only present on `RootRenderer`
  * Renderer.setDebugInfo is removed. Renderer.createElement / createText / createTemplateAnchor
    now take the DebugInfo directly.
- Query semantics:
  * Queries don't work with dynamically loaded components.
  * e.g. for router-outlet: loaded components can't be queries via @ViewQuery,
    but router-outlet emits an event `activate` now that emits the activated component
- Exception classes and the context inside changed (renamed fields)
- DebugElement.attributes is an Object and not a Map in JS any more
- ChangeDetectorGenConfig was renamed into CompilerConfig
- AppViewManager.createEmbeddedViewInContainer / AppViewManager.createHostViewInContainer
  are removed, use the methods in ViewContainerRef instead
- Change detection order changed:
  * 1. dirty check component inputs
  * 2. dirty check content children
  * 3. update render nodes

Closes #6301
Closes #6567
This commit is contained in:
Tobias Bosch
2016-01-06 14:13:44 -08:00
parent 45f09ba686
commit 2b34c88b69
312 changed files with 14271 additions and 16566 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,148 +0,0 @@
library angular2.test.core.compiler.directive_lifecycle_spec;
import 'package:angular2/testing_internal.dart';
import 'package:angular2/src/core/linker/directive_lifecycle_reflector.dart';
import 'package:angular2/src/core/linker/interfaces.dart';
main() {
describe('Create DirectiveMetadata', () {
describe('lifecycle', () {
describe("ngOnChanges", () {
it("should be true when the directive has the ngOnChanges method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnChanges, DirectiveImplementingOnChanges))
.toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngOnDestroy", () {
it("should be true when the directive has the ngOnDestroy method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnDestroy, DirectiveImplementingOnDestroy))
.toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngOnInit", () {
it("should be true when the directive has the ngOnInit method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnInit, DirectiveImplementingOnInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngDoCheck", () {
it("should be true when the directive has the ngDoCheck method", () {
expect(hasLifecycleHook(
LifecycleHooks.DoCheck, DirectiveImplementingOnCheck)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterContentInit", () {
it("should be true when the directive has the ngAfterContentInit method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit,
DirectiveImplementingAfterContentInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentChecked", () {
it("should be true when the directive has the ngAfterContentChecked method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked,
DirectiveImplementingAfterContentChecked)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterContentChecked, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterViewInit", () {
it("should be true when the directive has the ngAfterViewInit method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit,
DirectiveImplementingAfterViewInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterViewInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterViewChecked", () {
it("should be true when the directive has the ngAfterViewChecked method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked,
DirectiveImplementingAfterViewChecked)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveImplementingOnChanges implements OnChanges {
ngOnChanges(_) {}
}
class DirectiveImplementingOnCheck implements DoCheck {
ngDoCheck() {}
}
class DirectiveImplementingOnInit implements OnInit {
ngOnInit() {}
}
class DirectiveImplementingOnDestroy implements OnDestroy {
ngOnDestroy() {}
}
class DirectiveImplementingAfterContentInit implements AfterContentInit {
ngAfterContentInit() {}
}
class DirectiveImplementingAfterContentChecked implements AfterContentChecked {
ngAfterContentChecked() {}
}
class DirectiveImplementingAfterViewInit implements AfterViewInit {
ngAfterViewInit() {}
}
class DirectiveImplementingAfterViewChecked implements AfterViewChecked {
ngAfterViewChecked() {}
}

View File

@ -1,149 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
xdescribe,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
SpyObject,
proxy
} from 'angular2/testing_internal';
import {hasLifecycleHook} from 'angular2/src/core/linker/directive_lifecycle_reflector';
import {LifecycleHooks} from 'angular2/src/core/linker/interfaces';
export function main() {
describe('Create DirectiveMetadata', () => {
describe('lifecycle', () => {
describe("ngOnChanges", () => {
it("should be true when the directive has the ngOnChanges method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveWithOnChangesMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveNoHooks)).toBe(false);
});
});
describe("ngOnDestroy", () => {
it("should be true when the directive has the ngOnDestroy method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveWithOnDestroyMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveNoHooks)).toBe(false);
});
});
describe("ngOnInit", () => {
it("should be true when the directive has the ngOnInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveWithOnInitMethod)).toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngDoCheck", () => {
it("should be true when the directive has the ngDoCheck method", () => {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveWithOnCheckMethod)).toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentInit", () => {
it("should be true when the directive has the ngAfterContentInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit,
DirectiveWithAfterContentInitMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentChecked", () => {
it("should be true when the directive has the ngAfterContentChecked method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked,
DirectiveWithAfterContentCheckedMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterViewInit", () => {
it("should be true when the directive has the ngAfterViewInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit, DirectiveWithAfterViewInitMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterViewChecked", () => {
it("should be true when the directive has the ngAfterViewChecked method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked,
DirectiveWithAfterViewCheckedMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveWithOnChangesMethod {
ngOnChanges(_) {}
}
class DirectiveWithOnInitMethod {
ngOnInit() {}
}
class DirectiveWithOnCheckMethod {
ngDoCheck() {}
}
class DirectiveWithOnDestroyMethod {
ngOnDestroy() {}
}
class DirectiveWithAfterContentInitMethod {
ngAfterContentInit() {}
}
class DirectiveWithAfterContentCheckedMethod {
ngAfterContentChecked() {}
}
class DirectiveWithAfterViewInitMethod {
ngAfterViewInit() {}
}
class DirectiveWithAfterViewCheckedMethod {
ngAfterViewChecked() {}
}

View File

@ -1,209 +0,0 @@
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/testing_internal';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
import {
DirectiveMetadata,
Directive,
Input,
Output,
HostBinding,
HostListener,
ContentChildren,
ContentChildrenMetadata,
ViewChildren,
ViewChildrenMetadata,
ContentChild,
ContentChildMetadata,
ViewChild,
ViewChildMetadata
} from 'angular2/src/core/metadata';
@Directive({selector: 'someDirective'})
class SomeDirective {
}
@Directive({selector: 'someChildDirective'})
class SomeChildDirective extends SomeDirective {
}
@Directive({selector: 'someDirective', inputs: ['c']})
class SomeDirectiveWithInputs {
@Input() a;
@Input("renamed") b;
c;
}
@Directive({selector: 'someDirective', outputs: ['c']})
class SomeDirectiveWithOutputs {
@Output() a;
@Output("renamed") b;
c;
}
@Directive({selector: 'someDirective', outputs: ['a']})
class SomeDirectiveWithDuplicateOutputs {
@Output() a;
}
@Directive({selector: 'someDirective', properties: ['a']})
class SomeDirectiveWithProperties {
}
@Directive({selector: 'someDirective', events: ['a']})
class SomeDirectiveWithEvents {
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithSetterProps {
@Input("renamed")
set a(value) {
}
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithGetterOutputs {
@Output("renamed")
get a() {
return null;
}
}
@Directive({selector: 'someDirective', host: {'[c]': 'c'}})
class SomeDirectiveWithHostBindings {
@HostBinding() a;
@HostBinding("renamed") b;
c;
}
@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}})
class SomeDirectiveWithHostListeners {
@HostListener('a')
onA() {
}
@HostListener('b', ['$event.value'])
onB(value) {
}
}
@Directive({selector: 'someDirective', queries: {"cs": new ContentChildren("c")}})
class SomeDirectiveWithContentChildren {
@ContentChildren("a") as: any;
c;
}
@Directive({selector: 'someDirective', queries: {"cs": new ViewChildren("c")}})
class SomeDirectiveWithViewChildren {
@ViewChildren("a") as: any;
c;
}
@Directive({selector: 'someDirective', queries: {"c": new ContentChild("c")}})
class SomeDirectiveWithContentChild {
@ContentChild("a") a: any;
c;
}
@Directive({selector: 'someDirective', queries: {"c": new ViewChild("c")}})
class SomeDirectiveWithViewChild {
@ViewChild("a") a: any;
c;
}
class SomeDirectiveWithoutMetadata {}
export function main() {
describe("DirectiveResolver", () => {
var resolver: DirectiveResolver;
beforeEach(() => { resolver = new DirectiveResolver(); });
it('should read out the Directive metadata', () => {
var directiveMetadata = resolver.resolve(SomeDirective);
expect(directiveMetadata)
.toEqual(new DirectiveMetadata(
{selector: 'someDirective', inputs: [], outputs: [], host: {}, queries: {}}));
});
it('should throw if not matching metadata is found', () => {
expect(() => { resolver.resolve(SomeDirectiveWithoutMetadata); })
.toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata');
});
it('should not read parent class Directive metadata', function() {
var directiveMetadata = resolver.resolve(SomeChildDirective);
expect(directiveMetadata)
.toEqual(new DirectiveMetadata(
{selector: 'someChildDirective', inputs: [], outputs: [], host: {}, queries: {}}));
});
describe('inputs', () => {
it('should append directive inputs', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithInputs);
expect(directiveMetadata.inputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps);
expect(directiveMetadata.inputs).toEqual(['a: renamed']);
});
});
describe('outputs', () => {
it('should append directive outputs', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithOutputs);
expect(directiveMetadata.outputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithGetterOutputs);
expect(directiveMetadata.outputs).toEqual(['a: renamed']);
});
it('should throw if duplicate outputs', () => {
expect(() => { resolver.resolve(SomeDirectiveWithDuplicateOutputs); })
.toThrowError(
`Output event 'a' defined multiple times in 'SomeDirectiveWithDuplicateOutputs'`);
});
});
describe('host', () => {
it('should append host bindings', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings);
expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'});
});
it('should append host listeners', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners);
expect(directiveMetadata.host)
.toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'});
});
});
describe('queries', () => {
it('should append ContentChildren', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithContentChildren);
expect(directiveMetadata.queries)
.toEqual({"cs": new ContentChildren("c"), "as": new ContentChildren("a")});
});
it('should append ViewChildren', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithViewChildren);
expect(directiveMetadata.queries)
.toEqual({"cs": new ViewChildren("c"), "as": new ViewChildren("a")});
});
it('should append ContentChild', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild);
expect(directiveMetadata.queries)
.toEqual({"c": new ContentChild("c"), "a": new ContentChild("a")});
});
it('should append ViewChild', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild);
expect(directiveMetadata.queries)
.toEqual({"c": new ViewChild("c"), "a": new ViewChild("a")});
});
});
});
}

View File

@ -16,8 +16,8 @@ import {
ComponentFixture
} from 'angular2/testing_internal';
import {OnDestroy} from 'angular2/core';
import {Injector} from 'angular2/core';
import {Predicate} from 'angular2/src/facade/collection';
import {Injector, OnDestroy, DebugElement, Type} from 'angular2/core';
import {NgIf} from 'angular2/common';
import {Component, ViewMetadata} from 'angular2/src/core/metadata';
import {DynamicComponentLoader} from 'angular2/src/core/linker/dynamic_component_loader';
@ -27,7 +27,6 @@ import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {ComponentFixture_} from "angular2/src/testing/test_component_builder";
import {BaseException} from 'angular2/src/facade/exceptions';
import {PromiseWrapper} from 'angular2/src/facade/promise';
import {stringify} from 'angular2/src/facade/lang';
export function main() {
describe('DynamicComponentLoader', function() {
@ -70,41 +69,38 @@ export function main() {
}));
it('should allow to dispose even if the location has been removed',
inject(
[DynamicComponentLoader, TestComponentBuilder, AsyncTestCompleter],
(loader: DynamicComponentLoader, tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp, new ViewMetadata({
template: '<child-cmp *ngIf="ctxBoolProp"></child-cmp>',
directives: [NgIf, ChildComp]
}))
.overrideView(
ChildComp,
new ViewMetadata(
{template: '<location #loc></location>', directives: [Location]}))
.createAsync(MyComp)
.then((tc) => {
tc.debugElement.componentInstance.ctxBoolProp = true;
tc.detectChanges();
var childCompEl = (<ElementRef_>tc.elementRef).internalElement;
// TODO(juliemr): This is hideous, see if there's a better way to handle
// child element refs now.
var childElementRef =
childCompEl.componentView.appElements[0].nestedViews[0].appElements[0].ref;
loader.loadIntoLocation(DynamicallyLoaded, childElementRef, 'loc')
.then(ref => {
expect(tc.debugElement.nativeElement)
.toHaveText("Location;DynamicallyLoaded;");
inject([DynamicComponentLoader, TestComponentBuilder, AsyncTestCompleter],
(loader: DynamicComponentLoader, tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp, new ViewMetadata({
template: '<child-cmp *ngIf="ctxBoolProp"></child-cmp>',
directives: [NgIf, ChildComp]
}))
.overrideView(
ChildComp,
new ViewMetadata(
{template: '<location #loc></location>', directives: [Location]}))
.createAsync(MyComp)
.then((tc) => {
tc.debugElement.componentInstance.ctxBoolProp = true;
tc.detectChanges();
var childElementRef = tc.debugElement.query(filterByDirective(ChildComp))
.inject(ChildComp)
.elementRef;
loader.loadIntoLocation(DynamicallyLoaded, childElementRef, 'loc')
.then(ref => {
expect(tc.debugElement.nativeElement)
.toHaveText("Location;DynamicallyLoaded;");
tc.debugElement.componentInstance.ctxBoolProp = false;
tc.detectChanges();
expect(tc.debugElement.nativeElement).toHaveText("");
tc.debugElement.componentInstance.ctxBoolProp = false;
tc.detectChanges();
expect(tc.debugElement.nativeElement).toHaveText("");
ref.dispose();
expect(tc.debugElement.nativeElement).toHaveText("");
async.done();
});
});
}));
ref.dispose();
expect(tc.debugElement.nativeElement).toHaveText("");
async.done();
});
});
}));
it('should update host properties',
inject(
@ -138,17 +134,14 @@ export function main() {
}))
.createAsync(MyComp)
.then((tc: ComponentFixture) => {
tc.debugElement
PromiseWrapper.catchError(
loader.loadIntoLocation(DynamicallyLoadedThrows, tc.elementRef,
'loc'),
error => {
expect(error.message).toContain("ThrownInConstructor");
expect(() => tc.detectChanges()).not.toThrow();
async.done();
return null;
});
PromiseWrapper.catchError(
loader.loadIntoLocation(DynamicallyLoadedThrows, tc.elementRef, 'loc'),
(error) => {
expect(error.message).toContain("ThrownInConstructor");
expect(() => tc.detectChanges()).not.toThrow();
async.done();
return null;
});
});
}));
@ -185,25 +178,18 @@ export function main() {
});
}));
it('should throw if not enough projectable nodes are passed in',
inject(
[DynamicComponentLoader, TestComponentBuilder, AsyncTestCompleter],
(loader: DynamicComponentLoader, tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp,
new ViewMetadata({template: '<div #loc></div>', directives: []}))
.createAsync(MyComp)
.then((tc) => {
PromiseWrapper.catchError(
loader.loadIntoLocation(DynamicallyLoadedWithNgContent, tc.elementRef,
'loc', null, []),
(e) => {
expect(e.message).toContain(
`The component ${stringify(DynamicallyLoadedWithNgContent)} has 1 <ng-content> elements, but only 0 slots were provided`);
async.done();
return null;
});
});
}));
it('should not throw if not enough projectable nodes are passed in',
inject([DynamicComponentLoader, TestComponentBuilder, AsyncTestCompleter],
(loader: DynamicComponentLoader, tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp,
new ViewMetadata({template: '<div #loc></div>', directives: []}))
.createAsync(MyComp)
.then((tc) => {
loader.loadIntoLocation(DynamicallyLoadedWithNgContent, tc.elementRef,
'loc', null, [])
.then((_) => { async.done(); });
});
}));
});
@ -311,8 +297,7 @@ export function main() {
describe('loadAsRoot', () => {
it('should allow to create, update and destroy components',
inject([AsyncTestCompleter, DynamicComponentLoader, DOCUMENT, Injector],
(async: AsyncTestCompleter, loader: DynamicComponentLoader, doc,
injector: Injector) => {
(async, loader: DynamicComponentLoader, doc, injector: Injector) => {
var rootEl = createRootElement(doc, 'child-cmp');
DOM.appendChild(doc.body, rootEl);
loader.loadAsRoot(ChildComp, null, injector)
@ -341,8 +326,7 @@ export function main() {
it('should allow to pass projectable nodes',
inject([AsyncTestCompleter, DynamicComponentLoader, DOCUMENT, Injector],
(async: AsyncTestCompleter, loader: DynamicComponentLoader, doc,
injector: Injector) => {
(async, loader: DynamicComponentLoader, doc, injector: Injector) => {
var rootEl = createRootElement(doc, 'dummy');
DOM.appendChild(doc.body, rootEl);
loader.loadAsRoot(DynamicallyLoadedWithNgContent, null, injector, null,
@ -369,10 +353,14 @@ function createRootElement(doc: any, name: string): any {
return rootEl;
}
function filterByDirective(type: Type): Predicate<DebugElement> {
return (debugElement) => { return debugElement.providerTokens.indexOf(type) !== -1; };
}
@Component({selector: 'child-cmp', template: '{{ctxProp}}'})
class ChildComp {
ctxProp: string;
constructor() { this.ctxProp = 'hello'; }
constructor(public elementRef: ElementRef) { this.ctxProp = 'hello'; }
}

View File

@ -1,843 +0,0 @@
// TODO(tbosch): clang-format screws this up, see https://github.com/angular/clang-format/issues/11.
// Enable clang-format here again when this is fixed.
// clang-format off
import {
describe,
ddescribe,
it,
iit,
xit,
xdescribe,
expect,
beforeEach,
beforeEachBindings,
inject,
AsyncTestCompleter,
el,
containsRegexp
} from 'angular2/testing_internal';
import {SpyView, SpyElementRef, SpyDirectiveResolver, SpyProtoView, SpyChangeDetector, SpyAppViewManager} from '../spies';
import {isBlank, isPresent, stringify, Type} from 'angular2/src/facade/lang';
import {ResolvedProvider} from 'angular2/src/core/di';
import {
ListWrapper,
MapWrapper,
StringMapWrapper,
iterateListLike
} from 'angular2/src/facade/collection';
import {
AppProtoElement,
AppElement,
DirectiveProvider
} from 'angular2/src/core/linker/element';
import {ResolvedMetadataCache} from 'angular2/src/core/linker/resolved_metadata_cache';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
import {
Attribute,
Query,
ViewQuery,
ComponentMetadata,
DirectiveMetadata,
ViewEncapsulation
} from 'angular2/src/core/metadata';
import {OnDestroy, Directive} from 'angular2/core';
import {provide, Injector, Provider, Optional, Inject, Injectable, Self, SkipSelf, InjectMetadata, Host, HostMetadata, SkipSelfMetadata} from 'angular2/core';
import {ViewContainerRef, ViewContainerRef_} from 'angular2/src/core/linker/view_container_ref';
import {TemplateRef, TemplateRef_} from 'angular2/src/core/linker/template_ref';
import {ElementRef} from 'angular2/src/core/linker/element_ref';
import {DynamicChangeDetector, ChangeDetectorRef, Parser, Lexer} from 'angular2/src/core/change_detection/change_detection';
import {ChangeDetectorRef_} from 'angular2/src/core/change_detection/change_detector_ref';
import {QueryList} from 'angular2/src/core/linker/query_list';
import {AppView, AppProtoView} from "angular2/src/core/linker/view";
import {ViewType} from "angular2/src/core/linker/view_type";
@Directive({selector: ''})
class SimpleDirective {}
class SimpleService {}
@Directive({selector: ''})
class SomeOtherDirective {}
var _constructionCount;
@Directive({selector: ''})
class CountingDirective {
count: number;
constructor() {
this.count = _constructionCount;
_constructionCount += 1;
}
}
@Directive({selector: ''})
class FancyCountingDirective extends CountingDirective {
constructor() { super(); }
}
@Directive({selector: ''})
class NeedsDirective {
dependency: SimpleDirective;
constructor(@Self() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: ''})
class OptionallyNeedsDirective {
dependency: SimpleDirective;
constructor(@Self() @Optional() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: ''})
class NeeedsDirectiveFromHost {
dependency: SimpleDirective;
constructor(@Host() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: ''})
class NeedsDirectiveFromHostShadowDom {
dependency: SimpleDirective;
constructor(dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: ''})
class NeedsService {
service: any;
constructor(@Inject("service") service) { this.service = service; }
}
@Directive({selector: ''})
class NeedsServiceFromHost {
service: any;
constructor(@Host() @Inject("service") service) { this.service = service; }
}
class HasEventEmitter {
emitter;
constructor() { this.emitter = "emitter"; }
}
@Directive({selector: ''})
class NeedsAttribute {
typeAttribute;
titleAttribute;
fooAttribute;
constructor(@Attribute('type') typeAttribute: String, @Attribute('title') titleAttribute: String,
@Attribute('foo') fooAttribute: String) {
this.typeAttribute = typeAttribute;
this.titleAttribute = titleAttribute;
this.fooAttribute = fooAttribute;
}
}
@Directive({selector: ''})
class NeedsAttributeNoType {
fooAttribute;
constructor(@Attribute('foo') fooAttribute) { this.fooAttribute = fooAttribute; }
}
@Directive({selector: ''})
class NeedsQuery {
query: QueryList<CountingDirective>;
constructor(@Query(CountingDirective) query: QueryList<CountingDirective>) { this.query = query; }
}
@Directive({selector: ''})
class NeedsViewQuery {
query: QueryList<CountingDirective>;
constructor(@ViewQuery(CountingDirective) query: QueryList<CountingDirective>) { this.query = query; }
}
@Directive({selector: ''})
class NeedsQueryByVarBindings {
query: QueryList<any>;
constructor(@Query("one,two") query: QueryList<any>) { this.query = query; }
}
@Directive({selector: ''})
class NeedsTemplateRefQuery {
query: QueryList<TemplateRef>;
constructor(@Query(TemplateRef) query: QueryList<TemplateRef>) { this.query = query; }
}
@Directive({selector: ''})
class NeedsElementRef {
elementRef;
constructor(ref: ElementRef) { this.elementRef = ref; }
}
@Directive({selector: ''})
class NeedsViewContainer {
viewContainer;
constructor(vc: ViewContainerRef) { this.viewContainer = vc; }
}
@Directive({selector: ''})
class NeedsTemplateRef {
templateRef;
constructor(ref: TemplateRef) { this.templateRef = ref; }
}
@Directive({selector: ''})
class OptionallyInjectsTemplateRef {
templateRef;
constructor(@Optional() ref: TemplateRef) { this.templateRef = ref; }
}
@Directive({selector: ''})
class DirectiveNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Directive({selector: ''})
class ComponentNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Injectable()
class PipeNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
class A_Needs_B {
constructor(dep) {}
}
class B_Needs_A {
constructor(dep) {}
}
class DirectiveWithDestroy implements OnDestroy {
ngOnDestroyCounter: number;
constructor() { this.ngOnDestroyCounter = 0; }
ngOnDestroy() { this.ngOnDestroyCounter++; }
}
@Directive({selector: ''})
class D0 {}
@Directive({selector: ''})
class D1 {}
@Directive({selector: ''})
class D2 {}
@Directive({selector: ''})
class D3 {}
@Directive({selector: ''})
class D4 {}
@Directive({selector: ''})
class D5 {}
@Directive({selector: ''})
class D6 {}
@Directive({selector: ''})
class D7 {}
@Directive({selector: ''})
class D8 {}
@Directive({selector: ''})
class D9 {}
@Directive({selector: ''})
class D10 {}
@Directive({selector: ''})
class D11 {}
@Directive({selector: ''})
class D12 {}
@Directive({selector: ''})
class D13 {}
@Directive({selector: ''})
class D14 {}
@Directive({selector: ''})
class D15 {}
@Directive({selector: ''})
class D16 {}
@Directive({selector: ''})
class D17 {}
@Directive({selector: ''})
class D18 {}
@Directive({selector: ''})
class D19 {}
export function main() {
// An injector with more than 10 providers will switch to the dynamic strategy
var dynamicStrategyDirectives = [D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14, D15, D16, D17, D18, D19];
var resolvedMetadataCache:ResolvedMetadataCache;
var mockDirectiveMeta:Map<Type, DirectiveMetadata>;
var directiveResolver:SpyDirectiveResolver;
var dummyView:AppView;
var dummyViewFactory:Function;
function createView(type: ViewType, containerAppElement:AppElement = null, imperativelyCreatedProviders: ResolvedProvider[] = null, rootInjector: Injector = null, pipes: Type[] = null):AppView {
if (isBlank(pipes)) {
pipes = [];
}
var proto = AppProtoView.create(resolvedMetadataCache, type, pipes, {});
var cd = new SpyChangeDetector();
cd.prop('ref', new ChangeDetectorRef_(<any>cd));
var view = new AppView(proto, null, <any>new SpyAppViewManager(), [], containerAppElement, imperativelyCreatedProviders, rootInjector, <any> cd);
view.init([], [], [], []);
return view;
}
function protoAppElement(index, directives: Type[], attributes: {[key:string]:string} = null, dirVariableBindings:{[key:string]:number} = null) {
return AppProtoElement.create(resolvedMetadataCache, index, attributes, directives, dirVariableBindings);
}
function appElement(parent: AppElement, directives: Type[],
view: AppView = null, embeddedViewFactory: Function = null, attributes: {[key:string]:string} = null, dirVariableBindings:{[key:string]:number} = null) {
if (isBlank(view)) {
view = dummyView;
}
var proto = protoAppElement(0, directives, attributes, dirVariableBindings);
var el = new AppElement(proto, view, parent, null, embeddedViewFactory);
view.appElements.push(el);
return el;
}
function parentChildElements(parentDirectives: Type[], childDirectives:Type[], view: AppView = null) {
if (isBlank(view)) {
view = dummyView;
}
var parent = appElement(null, parentDirectives, view);
var child = appElement(parent, childDirectives, view);
return child;
}
function hostShadowElement(hostDirectives: Type[],
viewDirectives: Type[]): AppElement {
var host = appElement(null, hostDirectives);
var view = createView(ViewType.COMPONENT, host);
host.attachComponentView(view);
return appElement(null, viewDirectives, view);
}
function init() {
beforeEachBindings(() => {
var delegateDirectiveResolver = new DirectiveResolver();
directiveResolver = new SpyDirectiveResolver();
directiveResolver.spy('resolve').andCallFake( (directiveType) => {
var result = mockDirectiveMeta.get(directiveType);
if (isBlank(result)) {
result = delegateDirectiveResolver.resolve(directiveType);
}
return result;
});
return [
provide(DirectiveResolver, {useValue: directiveResolver})
];
});
beforeEach(inject([ResolvedMetadataCache], (_metadataCache) => {
mockDirectiveMeta = new Map<Type, DirectiveMetadata>();
resolvedMetadataCache = _metadataCache;
dummyView = createView(ViewType.HOST);
dummyViewFactory = () => {};
_constructionCount = 0;
}));
}
describe("ProtoAppElement", () => {
init();
describe('inline strategy', () => {
it("should allow for direct access using getProviderAtIndex", () => {
var proto = protoAppElement(0, [SimpleDirective]);
expect(proto.getProviderAtIndex(0)).toBeAnInstanceOf(DirectiveProvider);
expect(() => proto.getProviderAtIndex(-1)).toThrowError('Index -1 is out-of-bounds.');
expect(() => proto.getProviderAtIndex(10)).toThrowError('Index 10 is out-of-bounds.');
});
});
describe('dynamic strategy', () => {
it("should allow for direct access using getProviderAtIndex", () => {
var proto = protoAppElement(0, dynamicStrategyDirectives);
expect(proto.getProviderAtIndex(0)).toBeAnInstanceOf(DirectiveProvider);
expect(() => proto.getProviderAtIndex(-1)).toThrowError('Index -1 is out-of-bounds.');
expect(() => proto.getProviderAtIndex(dynamicStrategyDirectives.length - 1)).not.toThrow();
expect(() => proto.getProviderAtIndex(dynamicStrategyDirectives.length))
.toThrowError(`Index ${dynamicStrategyDirectives.length} is out-of-bounds.`);
});
});
describe(".create", () => {
it("should collect providers from all directives", () => {
mockDirectiveMeta.set(SimpleDirective, new DirectiveMetadata({providers: [provide('injectable1', {useValue: 'injectable1'})]}));
mockDirectiveMeta.set(SomeOtherDirective, new DirectiveMetadata({
providers: [provide('injectable2', {useValue: 'injectable2'})]
}));
var pel = protoAppElement( 0, [
SimpleDirective,
SomeOtherDirective
]);
expect(pel.getProviderAtIndex(0).key.token).toBe(SimpleDirective);
expect(pel.getProviderAtIndex(1).key.token).toBe(SomeOtherDirective);
expect(pel.getProviderAtIndex(2).key.token).toEqual("injectable1");
expect(pel.getProviderAtIndex(3).key.token).toEqual("injectable2");
});
it("should collect view providers from the component", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: [provide('injectable1', {useValue: 'injectable1'})]
}));
var pel = protoAppElement(0, [SimpleDirective]);
expect(pel.getProviderAtIndex(0).key.token).toBe(SimpleDirective);
expect(pel.getProviderAtIndex(1).key.token).toEqual("injectable1");
});
it("should flatten nested arrays in viewProviders and providers", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: [[[provide('view', {useValue: 'view'})]]],
providers: [[[provide('host', {useValue: 'host'})]]]
}));
var pel = protoAppElement(0, [SimpleDirective]);
expect(pel.getProviderAtIndex(0).key.token).toBe(SimpleDirective);
expect(pel.getProviderAtIndex(1).key.token).toEqual("view");
expect(pel.getProviderAtIndex(2).key.token).toEqual("host");
});
it('should support an arbitrary number of providers', () => {
var pel = protoAppElement(0, dynamicStrategyDirectives);
expect(pel.getProviderAtIndex(0).key.token).toBe(D0);
expect(pel.getProviderAtIndex(19).key.token).toBe(D19);
});
});
});
describe("AppElement", () => {
init();
[{ strategy: 'inline', directives: [] }, { strategy: 'dynamic',
directives: dynamicStrategyDirectives }].forEach((context) => {
var extraDirectives = context['directives'];
describe(`${context['strategy']} strategy`, () => {
describe("injection", () => {
it("should instantiate directives that have no dependencies", () => {
var directives = ListWrapper.concat([SimpleDirective], extraDirectives);
var el = appElement(null, directives);
expect(el.get(SimpleDirective)).toBeAnInstanceOf(SimpleDirective);
});
it("should instantiate directives that depend on an arbitrary number of directives", () => {
var directives = ListWrapper.concat([SimpleDirective, NeedsDirective], extraDirectives);
var el = appElement(null, directives);
var d = el.get(NeedsDirective);
expect(d).toBeAnInstanceOf(NeedsDirective);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it("should instantiate providers that have dependencies with set visibility",
function() {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
providers: [provide('injectable1', {useValue: 'injectable1'})]
}));
mockDirectiveMeta.set(SomeOtherDirective, new ComponentMetadata({
providers: [
provide('injectable1', {useValue:'new-injectable1'}),
provide('injectable2', {useFactory:
(val) => `${val}-injectable2`,
deps: [[new InjectMetadata('injectable1'), new SkipSelfMetadata()]]})
]
}));
var childInj = parentChildElements(
ListWrapper.concat([SimpleDirective], extraDirectives),
[SomeOtherDirective]
);
expect(childInj.get('injectable2')).toEqual('injectable1-injectable2');
});
it("should instantiate providers that have dependencies", () => {
var providers = [
provide('injectable1', {useValue: 'injectable1'}),
provide('injectable2', {useFactory:
(val) => `${val}-injectable2`,
deps: ['injectable1']})
];
mockDirectiveMeta.set(SimpleDirective, new DirectiveMetadata({providers: providers}));
var el = appElement(null, ListWrapper.concat(
[SimpleDirective], extraDirectives));
expect(el.get('injectable2')).toEqual('injectable1-injectable2');
});
it("should instantiate viewProviders that have dependencies", () => {
var viewProviders = [
provide('injectable1', {useValue: 'injectable1'}),
provide('injectable2', {useFactory:
(val) => `${val}-injectable2`,
deps: ['injectable1']})
];
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: viewProviders}));
var el = appElement(null, ListWrapper.concat(
[SimpleDirective], extraDirectives));
expect(el.get('injectable2')).toEqual('injectable1-injectable2');
});
it("should instantiate components that depend on viewProviders providers", () => {
mockDirectiveMeta.set(NeedsService, new ComponentMetadata({
viewProviders: [provide('service', {useValue: 'service'})]
}));
var el = appElement(null,
ListWrapper.concat([NeedsService], extraDirectives));
expect(el.get(NeedsService).service).toEqual('service');
});
it("should instantiate providers lazily", () => {
var created = false;
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
providers: [provide('service', {useFactory: () => created = true})]
}));
var el = appElement(null,
ListWrapper.concat([SimpleDirective],
extraDirectives));
expect(created).toBe(false);
el.get('service');
expect(created).toBe(true);
});
it("should instantiate view providers lazily", () => {
var created = false;
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: [provide('service', {useFactory: () => created = true})]
}));
var el = appElement(null,
ListWrapper.concat([SimpleDirective],
extraDirectives));
expect(created).toBe(false);
el.get('service');
expect(created).toBe(true);
});
it("should not instantiate other directives that depend on viewProviders providers",
() => {
mockDirectiveMeta.set(SimpleDirective,
new ComponentMetadata({
viewProviders: [provide("service", {useValue: "service"})]
}));
expect(() => { appElement(null, ListWrapper.concat([SimpleDirective, NeedsService], extraDirectives)); })
.toThrowError(containsRegexp(
`No provider for service! (${stringify(NeedsService) } -> service)`));
});
it("should instantiate directives that depend on providers of other directives", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
providers: [provide('service', {useValue: 'hostService'})]})
);
var shadowInj = hostShadowElement(
ListWrapper.concat([SimpleDirective], extraDirectives),
ListWrapper.concat([NeedsService], extraDirectives)
);
expect(shadowInj.get(NeedsService).service).toEqual('hostService');
});
it("should instantiate directives that depend on view providers of a component", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: [provide('service', {useValue: 'hostService'})]})
);
var shadowInj = hostShadowElement(
ListWrapper.concat([SimpleDirective], extraDirectives),
ListWrapper.concat([NeedsService], extraDirectives)
);
expect(shadowInj.get(NeedsService).service).toEqual('hostService');
});
it("should instantiate directives in a root embedded view that depend on view providers of a component", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata({
viewProviders: [provide('service', {useValue: 'hostService'})]})
);
var host = appElement(null, ListWrapper.concat([SimpleDirective], extraDirectives));
var componenetView = createView(ViewType.COMPONENT, host);
host.attachComponentView(componenetView);
var anchor = appElement(null, [], componenetView);
var embeddedView = createView(ViewType.EMBEDDED, anchor);
var rootEmbeddedEl = appElement(null, ListWrapper.concat([NeedsService], extraDirectives), embeddedView);
expect(rootEmbeddedEl.get(NeedsService).service).toEqual('hostService');
});
it("should instantiate directives that depend on imperatively created injector (bootstrap)", () => {
var rootInjector = Injector.resolveAndCreate([
provide("service", {useValue: 'appService'})
]);
var view = createView(ViewType.HOST, null, null, rootInjector);
expect(appElement(null, [NeedsService], view).get(NeedsService).service).toEqual('appService');
expect(() => appElement(null, [NeedsServiceFromHost], view)).toThrowError();
});
it("should instantiate directives that depend on imperatively created providers (root injector)", () => {
var imperativelyCreatedProviders = Injector.resolve([
provide("service", {useValue: 'appService'})
]);
var containerAppElement = appElement(null, []);
var view = createView(ViewType.HOST, containerAppElement, imperativelyCreatedProviders, null);
expect(appElement(null, [NeedsService], view).get(NeedsService).service).toEqual('appService');
expect(appElement(null, [NeedsServiceFromHost], view).get(NeedsServiceFromHost).service).toEqual('appService');
});
it("should not instantiate a directive in a view that has a host dependency on providers"+
" of the component", () => {
mockDirectiveMeta.set(SomeOtherDirective, new DirectiveMetadata({
providers: [provide('service', {useValue: 'hostService'})]})
);
expect(() => {
hostShadowElement(
ListWrapper.concat([SomeOtherDirective], extraDirectives),
ListWrapper.concat([NeedsServiceFromHost], extraDirectives)
);
}).toThrowError(new RegExp("No provider for service!"));
});
it("should not instantiate a directive in a view that has a host dependency on providers"+
" of a decorator directive", () => {
mockDirectiveMeta.set(SomeOtherDirective, new DirectiveMetadata({
providers: [provide('service', {useValue: 'hostService'})]}));
expect(() => {
hostShadowElement(
ListWrapper.concat([SimpleDirective, SomeOtherDirective], extraDirectives),
ListWrapper.concat([NeedsServiceFromHost], extraDirectives)
);
}).toThrowError(new RegExp("No provider for service!"));
});
it("should get directives", () => {
var child = hostShadowElement(
ListWrapper.concat([SomeOtherDirective, SimpleDirective], extraDirectives),
[NeedsDirectiveFromHostShadowDom]);
var d = child.get(NeedsDirectiveFromHostShadowDom);
expect(d).toBeAnInstanceOf(NeedsDirectiveFromHostShadowDom);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it("should get directives from the host", () => {
var child = parentChildElements(ListWrapper.concat([SimpleDirective], extraDirectives),
[NeeedsDirectiveFromHost]);
var d = child.get(NeeedsDirectiveFromHost);
expect(d).toBeAnInstanceOf(NeeedsDirectiveFromHost);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it("should throw when a dependency cannot be resolved", () => {
expect(() => appElement(null, ListWrapper.concat([NeeedsDirectiveFromHost], extraDirectives)))
.toThrowError(containsRegexp(
`No provider for ${stringify(SimpleDirective) }! (${stringify(NeeedsDirectiveFromHost) } -> ${stringify(SimpleDirective) })`));
});
it("should inject null when an optional dependency cannot be resolved", () => {
var el = appElement(null, ListWrapper.concat([OptionallyNeedsDirective], extraDirectives));
var d = el.get(OptionallyNeedsDirective);
expect(d.dependency).toEqual(null);
});
it("should allow for direct access using getDirectiveAtIndex", () => {
var providers =
ListWrapper.concat([SimpleDirective], extraDirectives);
var el = appElement(null, providers);
var firsIndexOut = providers.length > 10 ? providers.length : 10;
expect(el.getDirectiveAtIndex(0)).toBeAnInstanceOf(SimpleDirective);
expect(() => el.getDirectiveAtIndex(-1)).toThrowError('Index -1 is out-of-bounds.');
expect(() => el.getDirectiveAtIndex(firsIndexOut))
.toThrowError(`Index ${firsIndexOut} is out-of-bounds.`);
});
it("should instantiate directives that depend on the containing component", () => {
mockDirectiveMeta.set(SimpleDirective, new ComponentMetadata());
var shadow = hostShadowElement(ListWrapper.concat([SimpleDirective], extraDirectives),
[NeeedsDirectiveFromHost]);
var d = shadow.get(NeeedsDirectiveFromHost);
expect(d).toBeAnInstanceOf(NeeedsDirectiveFromHost);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it("should not instantiate directives that depend on other directives in the containing component's ElementInjector",
() => {
mockDirectiveMeta.set(SomeOtherDirective, new ComponentMetadata());
expect(() =>
{
hostShadowElement(
ListWrapper.concat([SomeOtherDirective, SimpleDirective], extraDirectives),
[NeedsDirective]);
})
.toThrowError(containsRegexp(
`No provider for ${stringify(SimpleDirective) }! (${stringify(NeedsDirective) } -> ${stringify(SimpleDirective) })`));
});
});
describe('static attributes', () => {
it('should be injectable', () => {
var el = appElement(null, ListWrapper.concat([NeedsAttribute], extraDirectives), null, null, {
'type': 'text',
'title': ''
});
var needsAttribute = el.get(NeedsAttribute);
expect(needsAttribute.typeAttribute).toEqual('text');
expect(needsAttribute.titleAttribute).toEqual('');
expect(needsAttribute.fooAttribute).toEqual(null);
});
it('should be injectable without type annotation', () => {
var el = appElement(null, ListWrapper.concat([NeedsAttributeNoType], extraDirectives), null,
null, {'foo': 'bar'});
var needsAttribute = el.get(NeedsAttributeNoType);
expect(needsAttribute.fooAttribute).toEqual('bar');
});
});
describe("refs", () => {
it("should inject ElementRef", () => {
var el = appElement(null, ListWrapper.concat([NeedsElementRef], extraDirectives));
expect(el.get(NeedsElementRef).elementRef).toBe(el.ref);
});
it("should inject ChangeDetectorRef of the component's view into the component via a proxy", () => {
mockDirectiveMeta.set(ComponentNeedsChangeDetectorRef, new ComponentMetadata());
var host = appElement(null, ListWrapper.concat([ComponentNeedsChangeDetectorRef], extraDirectives));
var view = createView(ViewType.COMPONENT, host);
host.attachComponentView(view);
host.get(ComponentNeedsChangeDetectorRef).changeDetectorRef.markForCheck();
expect((<any>view.changeDetector).spy('markPathToRootAsCheckOnce')).toHaveBeenCalled();
});
it("should inject ChangeDetectorRef of the containing component into directives", () => {
mockDirectiveMeta.set(DirectiveNeedsChangeDetectorRef, new DirectiveMetadata());
var view = createView(ViewType.HOST);
var el = appElement(null, ListWrapper.concat([DirectiveNeedsChangeDetectorRef], extraDirectives), view);
expect(el.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef).toBe(view.changeDetector.ref);
});
it('should inject ViewContainerRef', () => {
var el = appElement(null, ListWrapper.concat([NeedsViewContainer], extraDirectives));
expect(el.get(NeedsViewContainer).viewContainer).toBeAnInstanceOf(ViewContainerRef_);
});
it("should inject TemplateRef", () => {
var el = appElement(null, ListWrapper.concat([NeedsTemplateRef], extraDirectives), null, dummyViewFactory);
expect(el.get(NeedsTemplateRef).templateRef.elementRef).toBe(el.ref);
});
it("should throw if there is no TemplateRef", () => {
expect(() => appElement(null, ListWrapper.concat([NeedsTemplateRef], extraDirectives)))
.toThrowError(
`No provider for TemplateRef! (${stringify(NeedsTemplateRef) } -> TemplateRef)`);
});
it('should inject null if there is no TemplateRef when the dependency is optional', () => {
var el = appElement(null, ListWrapper.concat([OptionallyInjectsTemplateRef], extraDirectives));
var instance = el.get(OptionallyInjectsTemplateRef);
expect(instance.templateRef).toBeNull();
});
});
describe('queries', () => {
function expectDirectives(query: QueryList<any>, type, expectedIndex) {
var currentCount = 0;
expect(query.length).toEqual(expectedIndex.length);
iterateListLike(query, (i) => {
expect(i).toBeAnInstanceOf(type);
expect(i.count).toBe(expectedIndex[currentCount]);
currentCount += 1;
});
}
it('should be injectable', () => {
var el =
appElement(null, ListWrapper.concat([NeedsQuery], extraDirectives));
expect(el.get(NeedsQuery).query).toBeAnInstanceOf(QueryList);
});
it('should contain directives on the same injector', () => {
var el = appElement(null, ListWrapper.concat([
NeedsQuery,
CountingDirective
], extraDirectives));
el.ngAfterContentChecked();
expectDirectives(el.get(NeedsQuery).query, CountingDirective, [0]);
});
it('should contain TemplateRefs on the same injector', () => {
var el = appElement(null, ListWrapper.concat([
NeedsTemplateRefQuery
], extraDirectives), null, dummyViewFactory);
el.ngAfterContentChecked();
expect(el.get(NeedsTemplateRefQuery).query.first).toBeAnInstanceOf(TemplateRef_);
});
it('should contain the element when no directives are bound to the var provider', () => {
var dirs:Type[] = [NeedsQueryByVarBindings];
var dirVariableBindings:{[key:string]:number} = {
"one": null // element
};
var el = appElement(null, dirs.concat(extraDirectives), null, null, null, dirVariableBindings);
el.ngAfterContentChecked();
expect(el.get(NeedsQueryByVarBindings).query.first).toBe(el.ref);
});
it('should contain directives on the same injector when querying by variable providers' +
'in the order of var providers specified in the query', () => {
var dirs:Type[] = [NeedsQueryByVarBindings, NeedsDirective, SimpleDirective];
var dirVariableBindings:{[key:string]:number} = {
"one": 2, // 2 is the index of SimpleDirective
"two": 1 // 1 is the index of NeedsDirective
};
var el = appElement(null, dirs.concat(extraDirectives), null, null, null, dirVariableBindings);
el.ngAfterContentChecked();
// NeedsQueryByVarBindings queries "one,two", so SimpleDirective should be before NeedsDirective
expect(el.get(NeedsQueryByVarBindings).query.first).toBeAnInstanceOf(SimpleDirective);
expect(el.get(NeedsQueryByVarBindings).query.last).toBeAnInstanceOf(NeedsDirective);
});
it('should contain directives on the same and a child injector in construction order', () => {
var parent = appElement(null, [NeedsQuery, CountingDirective]);
appElement(parent, ListWrapper.concat([CountingDirective], extraDirectives));
parent.ngAfterContentChecked();
expectDirectives(parent.get(NeedsQuery).query, CountingDirective, [0, 1]);
});
});
});
});
});
}
class ContextWithHandler {
handler;
constructor(handler) { this.handler = handler; }
}

View File

@ -64,10 +64,11 @@ import {AsyncPipe} from 'angular2/common';
import {
PipeTransform,
ChangeDetectorRef,
ChangeDetectionStrategy,
ChangeDetectorGenConfig
ChangeDetectionStrategy
} from 'angular2/src/core/change_detection/change_detection';
import {CompilerConfig} from 'angular2/compiler';
import {
Directive,
Component,
@ -97,27 +98,23 @@ const ANCHOR_ELEMENT = CONST_EXPR(new OpaqueToken('AnchorElement'));
export function main() {
if (IS_DART) {
declareTests();
declareTests(false);
} else {
describe('no jit', () => {
beforeEachProviders(() => [
provide(ChangeDetectorGenConfig,
{useValue: new ChangeDetectorGenConfig(true, false, false)})
]);
declareTests();
describe('jit', () => {
beforeEachProviders(
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, true)})]);
declareTests(true);
});
describe('jit', () => {
beforeEachProviders(() => [
provide(ChangeDetectorGenConfig,
{useValue: new ChangeDetectorGenConfig(true, false, true)})
]);
declareTests();
describe('no jit', () => {
beforeEachProviders(
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, false)})]);
declareTests(false);
});
}
}
function declareTests() {
function declareTests(isJit: boolean) {
describe('integration tests', function() {
beforeEachProviders(() => [provide(ANCHOR_ELEMENT, {useValue: el('<div></div>')})]);
@ -530,7 +527,7 @@ function declareTests() {
});
}));
it('should allow to transplant embedded ProtoViews into other ViewContainers',
it('should allow to transplant TemplateRefs into other ViewContainers',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(
MyComp, new ViewMetadata({
@ -585,13 +582,13 @@ function declareTests() {
});
}));
it('should make the assigned component accessible in property bindings',
it('should make the assigned component accessible in property bindings, even if they were declared before the component',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
tcb.overrideView(
MyComp, new ViewMetadata({
template: '<p><child-cmp var-alice></child-cmp>{{alice.ctxProp}}</p>',
template: '<p>{{alice.ctxProp}}<child-cmp var-alice></child-cmp></p>',
directives: [ChildComp]
}))
@ -703,6 +700,7 @@ function declareTests() {
});
describe("OnPush components", () => {
it("should use ChangeDetectorRef to manually request a check",
inject([TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
@ -778,6 +776,30 @@ function declareTests() {
})));
}
it("should be checked when an event is fired",
inject([TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp, new ViewMetadata({
template: '<push-cmp [prop]="ctxProp" #cmp></push-cmp>',
directives: [[[PushCmp]]]
}))
.createAsync(MyComp)
.then((fixture) => {
var cmp = fixture.debugElement.children[0].getLocal('cmp');
fixture.debugElement.componentInstance.ctxProp = "one";
fixture.detectChanges();
expect(cmp.numberOfChecks).toEqual(1);
fixture.debugElement.componentInstance.ctxProp = "two";
fixture.detectChanges();
expect(cmp.numberOfChecks).toEqual(2);
async.done();
})}));
it('should not affect updating properties on the component',
inject([TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
@ -1386,9 +1408,8 @@ function declareTests() {
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
var c = e.context;
expect(DOM.nodeName(c.element).toUpperCase()).toEqual("DIRECTIVE-THROWING-ERROR");
expect(DOM.nodeName(c.componentElement).toUpperCase()).toEqual("DIV");
expect(c.injector).toBeAnInstanceOf(Injector);
expect(DOM.nodeName(c.componentRenderElement).toUpperCase()).toEqual("DIV");
expect(c.injector.getOptional).toBeTruthy();
async.done();
return null;
});
@ -1406,10 +1427,10 @@ function declareTests() {
throw "Should throw";
} catch (e) {
var c = e.context;
expect(DOM.nodeName(c.element).toUpperCase()).toEqual("INPUT");
expect(DOM.nodeName(c.componentElement).toUpperCase()).toEqual("DIV");
expect(c.injector).toBeAnInstanceOf(Injector);
expect(c.expression).toContain("one.two.three");
expect(DOM.nodeName(c.renderNode).toUpperCase()).toEqual("INPUT");
expect(DOM.nodeName(c.componentRenderElement).toUpperCase()).toEqual("DIV");
expect(c.injector.getOptional).toBeTruthy();
expect(c.source).toContain(":0:7");
expect(c.context).toBe(fixture.debugElement.componentInstance);
expect(c.locals["local"]).toBeDefined();
}
@ -1421,7 +1442,8 @@ function declareTests() {
it('should provide an error context when an error happens in change detection (text node)',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb = tcb.overrideView(MyComp, new ViewMetadata({template: `{{one.two.three}}`}));
tcb = tcb.overrideView(MyComp,
new ViewMetadata({template: `<div>{{one.two.three}}</div>`}));
tcb.createAsync(MyComp).then(fixture => {
try {
@ -1429,8 +1451,8 @@ function declareTests() {
throw "Should throw";
} catch (e) {
var c = e.context;
expect(c.element).toBeNull();
expect(c.injector).toBeNull();
expect(c.renderNode).toBeTruthy();
expect(c.source).toContain(':0:5');
}
async.done();
@ -1461,9 +1483,9 @@ function declareTests() {
clearPendingTimers();
var c = e.context;
expect(DOM.nodeName(c.element).toUpperCase()).toEqual("SPAN");
expect(DOM.nodeName(c.componentElement).toUpperCase()).toEqual("DIV");
expect(c.injector).toBeAnInstanceOf(Injector);
expect(DOM.nodeName(c.renderNode).toUpperCase()).toEqual("SPAN");
expect(DOM.nodeName(c.componentRenderElement).toUpperCase()).toEqual("DIV");
expect(c.injector.getOptional).toBeTruthy();
expect(c.context).toBe(fixture.debugElement.componentInstance);
expect(c.locals["local"]).toBeDefined();
}
@ -1493,12 +1515,12 @@ function declareTests() {
inject([TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp, new ViewMetadata({template: '{{a.b}}'}))
tcb.overrideView(MyComp, new ViewMetadata({template: '<div>{{a.b}}</div>'}))
.createAsync(MyComp)
.then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(containsRegexp(`{{a.b}} in ${stringify(MyComp)}`));
.toThrowError(containsRegexp(`:0:5`));
async.done();
})}));
@ -1511,8 +1533,7 @@ function declareTests() {
.createAsync(MyComp)
.then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(containsRegexp(`a.b in ${stringify(MyComp)}`));
expect(() => fixture.detectChanges()).toThrowError(containsRegexp(`:0:5`));
async.done();
})}));
@ -1528,7 +1549,7 @@ function declareTests() {
.createAsync(MyComp)
.then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(containsRegexp(`a.b in ${stringify(MyComp)}`));
.toThrowError(containsRegexp(`:0:11`));
async.done();
})}));
});
@ -1640,10 +1661,8 @@ function declareTests() {
});
describe('logging property updates', () => {
beforeEachProviders(() => [
provide(ChangeDetectorGenConfig,
{useValue: new ChangeDetectorGenConfig(true, true, false)})
]);
beforeEachProviders(
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, true, isJit)})]);
it('should reflect property values as attributes',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
@ -1680,28 +1699,6 @@ function declareTests() {
}));
});
describe('different proto view storages', () => {
function runWithMode(mode: string) {
return inject(
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(MyComp,
new ViewMetadata({template: `<!--${mode}--><div>{{ctxProp}}</div>`}))
.createAsync(MyComp)
.then((fixture) => {
fixture.debugElement.componentInstance.ctxProp = 'Hello World!';
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('Hello World!');
async.done();
});
});
}
it('should work with storing DOM nodes', runWithMode('cache'));
it('should work with serializing the DOM nodes', runWithMode('nocache'));
});
// Disabled until a solution is found, refs:
// - https://github.com/angular/angular/issues/776
// - https://github.com/angular/angular/commit/81f3f32

View File

@ -436,6 +436,43 @@ export function main() {
});
}));
// Note: This does not use a ng-content element, but
// is still important as we are merging proto views independent of
// the presence of ng-content elements!
it('should still allow to implement a recursive trees via multiple components',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(MainComp,
new ViewMetadata({template: '<tree></tree>', directives: [Tree]}))
.overrideView(Tree, new ViewMetadata({
template: 'TREE({{depth}}:<tree2 *manual [depth]="depth+1"></tree2>)',
directives: [Tree2, ManualViewportDirective]
}))
.createAsync(MainComp)
.then((main) => {
main.detectChanges();
expect(main.debugElement.nativeElement).toHaveText('TREE(0:)');
var tree = main.debugElement.query(By.directive(Tree));
var manualDirective: ManualViewportDirective = tree.queryAllNodes(By.directive(
ManualViewportDirective))[0].inject(ManualViewportDirective);
manualDirective.show();
main.detectChanges();
expect(main.debugElement.nativeElement).toHaveText('TREE(0:TREE2(1:))');
var tree2 = main.debugElement.query(By.directive(Tree2));
manualDirective =
tree2.queryAllNodes(By.directive(ManualViewportDirective))[0].inject(
ManualViewportDirective);
manualDirective.show();
main.detectChanges();
expect(main.debugElement.nativeElement).toHaveText('TREE(0:TREE2(1:TREE(2:)))');
async.done();
});
}));
if (DOM.supportsNativeShadowDOM()) {
it('should support native content projection and isolate styles per component',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
@ -456,6 +493,27 @@ export function main() {
}
if (DOM.supportsDOMEvents()) {
it('should support non emulated styles',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(MainComp, new ViewMetadata({
template: '<div class="redStyle"></div>',
styles: ['.redStyle { color: red}'],
encapsulation: ViewEncapsulation.None,
directives: [OtherComp]
}))
.createAsync(MainComp)
.then((main) => {
var mainEl = main.debugElement.nativeElement;
var div1 = DOM.firstChild(mainEl);
var div2 = DOM.createElement('div');
DOM.setAttribute(div2, 'class', 'redStyle');
DOM.appendChild(mainEl, div2);
expect(DOM.getComputedStyle(div1).color).toEqual('rgb(255, 0, 0)');
expect(DOM.getComputedStyle(div2).color).toEqual('rgb(255, 0, 0)');
async.done();
});
}));
it('should support emulated style encapsulation',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
tcb.overrideView(MainComp, new ViewMetadata({
@ -705,11 +763,21 @@ class ConditionalTextComponent {
class Tab {
}
@Component({
selector: 'tree2',
inputs: ['depth'],
template: 'TREE2({{depth}}:<tree *manual [depth]="depth+1"></tree>)',
directives: [ManualViewportDirective, forwardRef(() => Tree)]
})
class Tree2 {
depth = 0;
}
@Component({
selector: 'tree',
inputs: ['depth'],
template: 'TREE({{depth}}:<tree *manual [depth]="depth+1"></tree>)',
directives: [ManualViewportDirective, Tree]
directives: [ManualViewportDirective, Tree, forwardRef(() => Tree)]
})
class Tree {
depth = 0;

View File

@ -97,8 +97,6 @@ export function main() {
view.debugElement.componentInstance.shouldShow = false;
view.detectChanges();
// TODO: this fails right now!
// -> queries are not dirtied!
expect(q.log).toEqual([
["setter", "foo"],
["init", "foo"],
@ -144,7 +142,7 @@ export function main() {
tcb.overrideTemplate(MyComp, template)
.overrideTemplate(
NeedsViewChild,
'<div *ngIf="true"><div *ngIf="shouldShow" text="foo"></div></div>')
'<div *ngIf="true"><div *ngIf="shouldShow" text="foo"></div></div><div *ngIf="shouldShow2" text="bar"></div>')
.createAsync(MyComp)
.then((view) => {
view.detectChanges();
@ -153,15 +151,18 @@ export function main() {
expect(q.log).toEqual([["setter", "foo"], ["init", "foo"], ["check", "foo"]]);
q.shouldShow = false;
q.shouldShow2 = true;
q.log = [];
view.detectChanges();
expect(q.log).toEqual([
["setter", "foo"],
["init", "foo"],
["check", "foo"],
["setter", null],
["check", null]
]);
expect(q.log).toEqual([["setter", "bar"], ["check", "bar"]]);
q.shouldShow = false;
q.shouldShow2 = false;
q.log = [];
view.detectChanges();
expect(q.log).toEqual([["setter", null], ["check", null]]);
async.done();
});
@ -408,7 +409,7 @@ export function main() {
});
}));
it('should reflect dynamically inserted directives',
it('should support dynamically inserted directives',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var template = '<needs-query-by-var-binding #q>' +
'<div *ngFor="#item of list" [text]="item" #textLabel="textDir"></div>' +
@ -739,6 +740,7 @@ class NeedsContentChild implements AfterContentInit, AfterContentChecked {
class NeedsViewChild implements AfterViewInit,
AfterViewChecked {
shouldShow: boolean = true;
shouldShow2: boolean = false;
_child: TextDirective;
@ViewChild(TextDirective)
@ -956,4 +958,4 @@ class MyComp {
this.shouldShow = false;
this.list = ['1d', '2d', '3d'];
}
}
}

View File

@ -0,0 +1,661 @@
import {
describe,
ddescribe,
it,
iit,
xit,
xdescribe,
expect,
beforeEach,
beforeEachProviders,
inject,
AsyncTestCompleter,
el,
containsRegexp,
ComponentFixture,
TestComponentBuilder,
fakeAsync,
tick
} from 'angular2/testing_internal';
import {isBlank, isPresent, stringify, Type, CONST_EXPR} from 'angular2/src/facade/lang';
import {
ViewContainerRef,
TemplateRef,
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
Directive,
Component,
DebugElement,
forwardRef,
Input,
PipeTransform,
Attribute,
ViewMetadata,
provide,
Injector,
Provider,
Optional,
Inject,
Injectable,
Self,
SkipSelf,
InjectMetadata,
Pipe,
Host,
HostMetadata,
SkipSelfMetadata
} from 'angular2/core';
import {NgIf} from 'angular2/common';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
const ALL_DIRECTIVES = CONST_EXPR([
forwardRef(() => SimpleDirective),
forwardRef(() => CycleDirective),
forwardRef(() => SimpleComponent),
forwardRef(() => SomeOtherDirective),
forwardRef(() => NeedsDirectiveFromSelf),
forwardRef(() => NeedsServiceComponent),
forwardRef(() => OptionallyNeedsDirective),
forwardRef(() => NeedsComponentFromHost),
forwardRef(() => NeedsDirectiveFromHost),
forwardRef(() => NeedsDirective),
forwardRef(() => NeedsService),
forwardRef(() => NeedsAppService),
forwardRef(() => NeedsAttribute),
forwardRef(() => NeedsAttributeNoType),
forwardRef(() => NeedsElementRef),
forwardRef(() => NeedsViewContainerRef),
forwardRef(() => NeedsTemplateRef),
forwardRef(() => OptionallyNeedsTemplateRef),
forwardRef(() => DirectiveNeedsChangeDetectorRef),
forwardRef(() => PushComponentNeedsChangeDetectorRef),
forwardRef(() => NeedsServiceFromHost),
forwardRef(() => NeedsAttribute),
forwardRef(() => NeedsAttributeNoType),
forwardRef(() => NeedsElementRef),
forwardRef(() => NeedsViewContainerRef),
forwardRef(() => NeedsTemplateRef),
forwardRef(() => OptionallyNeedsTemplateRef),
forwardRef(() => DirectiveNeedsChangeDetectorRef),
forwardRef(() => PushComponentNeedsChangeDetectorRef),
forwardRef(() => NeedsHostAppService),
NgIf
]);
const ALL_PIPES = CONST_EXPR([
forwardRef(() => PipeNeedsChangeDetectorRef),
forwardRef(() => PipeNeedsService),
forwardRef(() => PurePipe),
forwardRef(() => ImpurePipe),
]);
@Directive({selector: '[simpleDirective]'})
class SimpleDirective {
@Input('simpleDirective') value: any = null;
}
@Component({selector: '[simpleComponent]', template: '', directives: ALL_DIRECTIVES})
class SimpleComponent {
}
class SimpleService {}
@Directive({selector: '[someOtherDirective]'})
class SomeOtherDirective {
}
@Directive({selector: '[cycleDirective]'})
class CycleDirective {
constructor(self: CycleDirective) {}
}
@Directive({selector: '[needsDirectiveFromSelf]'})
class NeedsDirectiveFromSelf {
dependency: SimpleDirective;
constructor(@Self() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: '[optionallyNeedsDirective]'})
class OptionallyNeedsDirective {
dependency: SimpleDirective;
constructor(@Self() @Optional() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: '[needsComponentFromHost]'})
class NeedsComponentFromHost {
dependency: SimpleComponent;
constructor(@Host() dependency: SimpleComponent) { this.dependency = dependency; }
}
@Directive({selector: '[needsDirectiveFromHost]'})
class NeedsDirectiveFromHost {
dependency: SimpleDirective;
constructor(@Host() dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: '[needsDirective]'})
class NeedsDirective {
dependency: SimpleDirective;
constructor(dependency: SimpleDirective) { this.dependency = dependency; }
}
@Directive({selector: '[needsService]'})
class NeedsService {
service: any;
constructor(@Inject("service") service) { this.service = service; }
}
@Directive({selector: '[needsAppService]'})
class NeedsAppService {
service: any;
constructor(@Inject("appService") service) { this.service = service; }
}
@Component({selector: '[needsHostAppService]', template: '', directives: ALL_DIRECTIVES})
class NeedsHostAppService {
service: any;
constructor(@Host() @Inject("appService") service) { this.service = service; }
}
@Component({selector: '[needsServiceComponent]', template: ''})
class NeedsServiceComponent {
service: any;
constructor(@Inject("service") service) { this.service = service; }
}
@Directive({selector: '[needsServiceFromHost]'})
class NeedsServiceFromHost {
service: any;
constructor(@Host() @Inject("service") service) { this.service = service; }
}
@Directive({selector: '[needsAttribute]'})
class NeedsAttribute {
typeAttribute;
titleAttribute;
fooAttribute;
constructor(@Attribute('type') typeAttribute: String, @Attribute('title') titleAttribute: String,
@Attribute('foo') fooAttribute: String) {
this.typeAttribute = typeAttribute;
this.titleAttribute = titleAttribute;
this.fooAttribute = fooAttribute;
}
}
@Directive({selector: '[needsAttributeNoType]'})
class NeedsAttributeNoType {
fooAttribute;
constructor(@Attribute('foo') fooAttribute) { this.fooAttribute = fooAttribute; }
}
@Directive({selector: '[needsElementRef]'})
class NeedsElementRef {
elementRef;
constructor(ref: ElementRef) { this.elementRef = ref; }
}
@Directive({selector: '[needsViewContainerRef]'})
class NeedsViewContainerRef {
viewContainer;
constructor(vc: ViewContainerRef) { this.viewContainer = vc; }
}
@Directive({selector: '[needsTemplateRef]'})
class NeedsTemplateRef {
templateRef;
constructor(ref: TemplateRef) { this.templateRef = ref; }
}
@Directive({selector: '[optionallyNeedsTemplateRef]'})
class OptionallyNeedsTemplateRef {
templateRef;
constructor(@Optional() ref: TemplateRef) { this.templateRef = ref; }
}
@Directive({selector: '[directiveNeedsChangeDetectorRef]'})
class DirectiveNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({
selector: '[componentNeedsChangeDetectorRef]',
template: '{{counter}}',
directives: ALL_DIRECTIVES,
changeDetection: ChangeDetectionStrategy.OnPush
})
class PushComponentNeedsChangeDetectorRef {
counter: number = 0;
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Pipe({name: 'purePipe', pure: true})
class PurePipe {
constructor() {}
transform(value: any, args: any[] = null): any { return this; }
}
@Pipe({name: 'impurePipe', pure: false})
class ImpurePipe {
constructor() {}
transform(value: any, args: any[] = null): any { return this; }
}
@Pipe({name: 'pipeNeedsChangeDetectorRef'})
class PipeNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
transform(value: any, args: any[] = null): any { return this; }
}
@Pipe({name: 'pipeNeedsService'})
export class PipeNeedsService implements PipeTransform {
service: any;
constructor(@Inject("service") service) { this.service = service; }
transform(value: any, args: any[] = null): any { return this; }
}
@Component({selector: 'root'})
class TestComp {
}
export function main() {
var tcb: TestComponentBuilder;
function createCompFixture(template: string, tcb: TestComponentBuilder,
comp: Type = null): ComponentFixture {
if (isBlank(comp)) {
comp = TestComp;
}
return tcb.overrideView(comp,
new ViewMetadata(
{template: template, directives: ALL_DIRECTIVES, pipes: ALL_PIPES}))
.createFakeAsync(comp);
}
function createComp(template: string, tcb: TestComponentBuilder,
comp: Type = null): DebugElement {
var fixture = createCompFixture(template, tcb, comp);
fixture.detectChanges();
return fixture.debugElement;
}
describe("View Injector", () => {
// On CJS fakeAsync is not supported...
if (!DOM.supportsDOMEvents()) return;
beforeEachProviders(() => [provide("appService", {useValue: 'appService'})]);
beforeEach(inject([TestComponentBuilder], (_tcb) => { tcb = _tcb; }));
describe("injection", () => {
it("should instantiate directives that have no dependencies", fakeAsync(() => {
var el = createComp('<div simpleDirective>', tcb);
expect(el.children[0].inject(SimpleDirective)).toBeAnInstanceOf(SimpleDirective);
}));
it("should instantiate directives that depend on another directive", fakeAsync(() => {
var el = createComp('<div simpleDirective needsDirective>', tcb);
var d = el.children[0].inject(NeedsDirective);
expect(d).toBeAnInstanceOf(NeedsDirective);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
}));
it("should instantiate providers that have dependencies with SkipSelf", fakeAsync(() => {
var el = createComp('<div simpleDirective><span someOtherDirective></span></div>',
tcb.overrideProviders(
SimpleDirective,
[provide('injectable1', {useValue: 'injectable1'})])
.overrideProviders(SomeOtherDirective, [
provide('injectable1', {useValue: 'new-injectable1'}),
provide('injectable2',
{
useFactory: (val) => `${val}-injectable2`,
deps: [
[
new InjectMetadata('injectable1'),
new SkipSelfMetadata()
]
]
})
]));
expect(el.children[0].children[0].inject('injectable2'))
.toEqual('injectable1-injectable2');
}));
it("should instantiate providers that have dependencies", fakeAsync(() => {
var providers = [
provide('injectable1', {useValue: 'injectable1'}),
provide('injectable2',
{useFactory: (val) => `${val}-injectable2`, deps: ['injectable1']})
];
var el = createComp('<div simpleDirective></div>',
tcb.overrideProviders(SimpleDirective, providers));
expect(el.children[0].inject('injectable2')).toEqual('injectable1-injectable2');
}));
it("should instantiate viewProviders that have dependencies", fakeAsync(() => {
var viewProviders = [
provide('injectable1', {useValue: 'injectable1'}),
provide('injectable2',
{useFactory: (val) => `${val}-injectable2`, deps: ['injectable1']})
];
var el = createComp('<div simpleComponent></div>',
tcb.overrideViewProviders(SimpleComponent, viewProviders));
expect(el.children[0].inject('injectable2')).toEqual('injectable1-injectable2');
}));
it("should instantiate components that depend on viewProviders providers", fakeAsync(() => {
var el =
createComp('<div needsServiceComponent></div>',
tcb.overrideViewProviders(NeedsServiceComponent,
[provide('service', {useValue: 'service'})]));
expect(el.children[0].inject(NeedsServiceComponent).service).toEqual('service');
}));
it("should instantiate multi providers", fakeAsync(() => {
var providers = [
provide('injectable1', {useValue: 'injectable11', multi: true}),
provide('injectable1', {useValue: 'injectable12', multi: true})
];
var el = createComp('<div simpleDirective></div>',
tcb.overrideProviders(SimpleDirective, providers));
expect(el.children[0].inject('injectable1')).toEqual(['injectable11', 'injectable12']);
}));
it("should instantiate providers lazily", fakeAsync(() => {
var created = false;
var el = createComp(
'<div simpleDirective></div>',
tcb.overrideProviders(SimpleDirective,
[provide('service', {useFactory: () => created = true})]));
expect(created).toBe(false);
el.children[0].inject('service');
expect(created).toBe(true);
}));
it("should instantiate view providers lazily", fakeAsync(() => {
var created = false;
var el = createComp(
'<div simpleComponent></div>',
tcb.overrideViewProviders(SimpleComponent,
[provide('service', {useFactory: () => created = true})]));
expect(created).toBe(false);
el.children[0].inject('service');
expect(created).toBe(true);
}));
it("should not instantiate other directives that depend on viewProviders providers",
fakeAsync(() => {
expect(() =>
createComp('<div simpleComponent needsService></div>',
tcb.overrideViewProviders(
SimpleComponent, [provide("service", {useValue: "service"})])))
.toThrowError(containsRegexp(`No provider for service!`));
}));
it("should instantiate directives that depend on providers of other directives",
fakeAsync(() => {
var el =
createComp('<div simpleDirective><div needsService></div></div>',
tcb.overrideProviders(SimpleDirective,
[provide('service', {useValue: 'parentService'})]));
expect(el.children[0].children[0].inject(NeedsService).service).toEqual('parentService');
}));
it("should instantiate directives that depend on providers in a parent view",
fakeAsync(() => {
var el = createComp(
'<div simpleDirective><template [ngIf]="true"><div *ngIf="true" needsService></div></template></div>',
tcb.overrideProviders(SimpleDirective,
[provide('service', {useValue: 'parentService'})]));
expect(el.children[0].children[0].inject(NeedsService).service).toEqual('parentService');
}));
it("should instantiate directives that depend on providers of a component", fakeAsync(() => {
var el =
createComp('<div simpleComponent></div>',
tcb.overrideTemplate(SimpleComponent, '<div needsService></div>')
.overrideProviders(SimpleComponent,
[provide('service', {useValue: 'hostService'})]));
expect(el.children[0].children[0].inject(NeedsService).service).toEqual('hostService');
}));
it("should instantiate directives that depend on view providers of a component",
fakeAsync(() => {
var el = createComp(
'<div simpleComponent></div>',
tcb.overrideTemplate(SimpleComponent, '<div needsService></div>')
.overrideViewProviders(SimpleComponent,
[provide('service', {useValue: 'hostService'})]));
expect(el.children[0].children[0].inject(NeedsService).service).toEqual('hostService');
}));
it("should instantiate directives in a root embedded view that depend on view providers of a component",
fakeAsync(() => {
var el = createComp(
'<div simpleComponent></div>',
tcb.overrideTemplate(SimpleComponent, '<div *ngIf="true" needsService></div>')
.overrideViewProviders(SimpleComponent,
[provide('service', {useValue: 'hostService'})]));
expect(el.children[0].children[0].inject(NeedsService).service).toEqual('hostService');
}));
it("should instantiate directives that depend on instances in the app injector",
fakeAsync(() => {
var el = createComp('<div needsAppService></div>', tcb);
expect(el.children[0].inject(NeedsAppService).service).toEqual('appService');
}));
it("should not instantiate a directive with cyclic dependencies", fakeAsync(() => {
expect(() => createComp('<div cycleDirective></div>', tcb))
.toThrowError(
'Template parse errors:\nCannot instantiate cyclic dependency! CycleDirective ("[ERROR ->]<div cycleDirective></div>"): TestComp@0:0');
}));
it("should not instantiate a directive in a view that has a host dependency on providers" +
" of the component",
fakeAsync(() => {
expect(() => createComp(
'<div simpleComponent></div>',
tcb.overrideProviders(SimpleComponent,
[provide('service', {useValue: 'hostService'})])
.overrideTemplate(SimpleComponent, '<div needsServiceFromHost><div>')))
.toThrowError(
`Template parse errors:\nNo provider for service ("[ERROR ->]<div needsServiceFromHost><div>"): SimpleComponent@0:0`);
}));
it("should not instantiate a directive in a view that has a host dependency on providers" +
" of a decorator directive",
fakeAsync(() => {
expect(() => createComp(
'<div simpleComponent someOtherDirective></div>',
tcb.overrideProviders(SomeOtherDirective,
[provide('service', {useValue: 'hostService'})])
.overrideTemplate(SimpleComponent, '<div needsServiceFromHost><div>')))
.toThrowError(
`Template parse errors:\nNo provider for service ("[ERROR ->]<div needsServiceFromHost><div>"): SimpleComponent@0:0`);
}));
it("should not instantiate a directive in a view that has a self dependency on a parent directive",
fakeAsync(() => {
expect(() => createComp('<div simpleDirective><div needsDirectiveFromSelf></div></div>',
tcb))
.toThrowError(
`Template parse errors:\nNo provider for SimpleDirective ("<div simpleDirective>[ERROR ->]<div needsDirectiveFromSelf></div></div>"): TestComp@0:21`);
}));
it("should instantiate directives that depend on other directives", fakeAsync(() => {
var el = createComp('<div simpleDirective><div needsDirective></div></div>', tcb);
var d = el.children[0].children[0].inject(NeedsDirective);
expect(d).toBeAnInstanceOf(NeedsDirective);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
}));
it("should throw when a dependency cannot be resolved", fakeAsync(() => {
expect(() => createComp('<div needsService></div>', tcb))
.toThrowError(containsRegexp(`No provider for service!`));
}));
it("should inject null when an optional dependency cannot be resolved", fakeAsync(() => {
var el = createComp('<div optionallyNeedsDirective></div>', tcb);
var d = el.children[0].inject(OptionallyNeedsDirective);
expect(d.dependency).toEqual(null);
}));
it("should instantiate directives that depends on the host component", fakeAsync(() => {
var el = createComp(
'<div simpleComponent></div>',
tcb.overrideTemplate(SimpleComponent, '<div needsComponentFromHost></div>'));
var d = el.children[0].children[0].inject(NeedsComponentFromHost);
expect(d.dependency).toBeAnInstanceOf(SimpleComponent);
}));
it("should instantiate host views for components that have a @Host dependency ",
fakeAsync(() => {
var el = createComp('', tcb, NeedsHostAppService);
expect(el.componentInstance.service).toEqual('appService');
}));
it("should not instantiate directives that depend on other directives on the host element",
fakeAsync(() => {
expect(() => createComp(
'<div simpleComponent simpleDirective></div>',
tcb.overrideTemplate(SimpleComponent, '<div needsDirectiveFromHost></div>')))
.toThrowError(
`Template parse errors:\nNo provider for SimpleDirective ("[ERROR ->]<div needsDirectiveFromHost></div>"): SimpleComponent@0:0`);
}));
});
describe('static attributes', () => {
it('should be injectable', fakeAsync(() => {
var el = createComp('<div needsAttribute type="text" title></div>', tcb);
var needsAttribute = el.children[0].inject(NeedsAttribute);
expect(needsAttribute.typeAttribute).toEqual('text');
expect(needsAttribute.titleAttribute).toEqual('');
expect(needsAttribute.fooAttribute).toEqual(null);
}));
it('should be injectable without type annotation', fakeAsync(() => {
var el = createComp('<div needsAttributeNoType foo="bar"></div>', tcb);
var needsAttribute = el.children[0].inject(NeedsAttributeNoType);
expect(needsAttribute.fooAttribute).toEqual('bar');
}));
});
describe("refs", () => {
it("should inject ElementRef", fakeAsync(() => {
var el = createComp('<div needsElementRef></div>', tcb);
expect(el.children[0].inject(NeedsElementRef).elementRef.nativeElement)
.toBe(el.children[0].nativeElement);
}));
it("should inject ChangeDetectorRef of the component's view into the component via a proxy",
fakeAsync(() => {
var cf = createCompFixture('<div componentNeedsChangeDetectorRef></div>', tcb);
cf.detectChanges();
var compEl = cf.debugElement.children[0];
var comp = compEl.inject(PushComponentNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
comp.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
}));
it("should inject ChangeDetectorRef of the containing component into directives",
fakeAsync(() => {
var cf = createCompFixture(
'<div componentNeedsChangeDetectorRef></div>',
tcb.overrideTemplate(PushComponentNeedsChangeDetectorRef,
'{{counter}}<div directiveNeedsChangeDetectorRef></div>'));
cf.detectChanges();
var compEl = cf.debugElement.children[0];
var comp = compEl.inject(PushComponentNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
compEl.children[0]
.inject(DirectiveNeedsChangeDetectorRef)
.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
}));
it('should inject ViewContainerRef', fakeAsync(() => {
var el = createComp('<div needsViewContainerRef></div>', tcb);
expect(el.children[0].inject(NeedsViewContainerRef).viewContainer.element.nativeElement)
.toBe(el.children[0].nativeElement);
}));
it("should inject TemplateRef", fakeAsync(() => {
var el = createComp('<template needsViewContainerRef needsTemplateRef></template>', tcb);
expect(el.childNodes[0].inject(NeedsTemplateRef).templateRef.elementRef)
.toBe(el.childNodes[0].inject(NeedsViewContainerRef).viewContainer.element);
}));
it("should throw if there is no TemplateRef", fakeAsync(() => {
expect(() => createComp('<div needsTemplateRef></div>', tcb))
.toThrowError(containsRegexp(`No provider for TemplateRef!`));
}));
it('should inject null if there is no TemplateRef when the dependency is optional',
fakeAsync(() => {
var el = createComp('<div optionallyNeedsTemplateRef></div>', tcb);
var instance = el.children[0].inject(OptionallyNeedsTemplateRef);
expect(instance.templateRef).toBeNull();
}));
});
describe('pipes', () => {
it('should instantiate pipes that have dependencies', fakeAsync(() => {
var el = createComp(
'<div [simpleDirective]="true | pipeNeedsService"></div>',
tcb.overrideProviders(TestComp, [provide('service', {useValue: 'pipeService'})]));
expect(el.children[0].inject(SimpleDirective).value.service).toEqual('pipeService');
}));
it('should inject ChangeDetectorRef into pipes', fakeAsync(() => {
var el = createComp(
'<div [simpleDirective]="true | pipeNeedsChangeDetectorRef" directiveNeedsChangeDetectorRef></div>',
tcb);
var cdRef = el.children[0].inject(DirectiveNeedsChangeDetectorRef).changeDetectorRef;
expect(el.children[0].inject(SimpleDirective).value.changeDetectorRef).toBe(cdRef);
}));
it('should cache pure pipes', fakeAsync(() => {
var el = createComp(
'<div [simpleDirective]="true | purePipe"></div><div [simpleDirective]="true | purePipe"></div>',
tcb);
var purePipe1 = el.children[0].inject(SimpleDirective).value;
var purePipe2 = el.children[1].inject(SimpleDirective).value;
expect(purePipe1).toBeAnInstanceOf(PurePipe);
expect(purePipe1).toBe(purePipe2);
}));
it('should not cache pure pipes', fakeAsync(() => {
var el = createComp(
'<div [simpleDirective]="true | impurePipe"></div><div [simpleDirective]="true | impurePipe"></div>',
tcb);
var purePipe1 = el.children[0].inject(SimpleDirective).value;
var purePipe2 = el.children[1].inject(SimpleDirective).value;
expect(purePipe1).toBeAnInstanceOf(ImpurePipe);
expect(purePipe2).toBeAnInstanceOf(ImpurePipe);
expect(purePipe1).not.toBe(purePipe2);
}));
});
});
}

View File

@ -1,71 +0,0 @@
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/testing_internal';
import {ViewResolver} from 'angular2/src/core/linker/view_resolver';
import {Component, ViewMetadata} from 'angular2/src/core/metadata';
class SomeDir {}
class SomePipe {}
@Component({
selector: 'sample',
template: "some template",
directives: [SomeDir],
pipes: [SomePipe],
styles: ["some styles"]
})
class ComponentWithView {
}
@Component({
selector: 'sample',
template: "some template",
directives: [SomeDir],
pipes: [SomePipe],
styles: ["some styles"]
})
class ComponentWithTemplate {
}
@Component({selector: 'sample', template: "some template"})
class ComponentWithViewTemplate {
}
@Component({selector: 'sample', templateUrl: "some template url", template: "some template"})
class ComponentWithViewTemplateUrl {
}
@Component({selector: 'sample'})
class ComponentWithoutView {
}
class SimpleClass {}
export function main() {
describe("ViewResolver", () => {
var resolver: ViewResolver;
beforeEach(() => { resolver = new ViewResolver(); });
it('should read out the View metadata from the Component metadata', () => {
var viewMetadata = resolver.resolve(ComponentWithTemplate);
expect(viewMetadata)
.toEqual(new ViewMetadata({
template: "some template",
directives: [SomeDir],
pipes: [SomePipe],
styles: ["some styles"]
}));
});
it('should throw when Component has no View decorator and no template is set', () => {
expect(() => resolver.resolve(ComponentWithoutView))
.toThrowErrorWith(
"Component 'ComponentWithoutView' must have either 'template' or 'templateUrl' set");
});
it('should throw when simple class has no View decorator and no template is set', () => {
expect(() => resolver.resolve(SimpleClass))
.toThrowErrorWith("Could not compile 'SimpleClass' because it is not a component.");
});
});
}