refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,598 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core';
import {ɵDomRendererFactory2} from '@angular/platform-browser';
import {AnimationDriver, BrowserAnimationsModule, ɵAnimationEngine} from '@angular/platform-browser/animations';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/platform-browser/animations/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {TestBed} from '../../testing';
export function main() {
// these tests are only mean't to be run within the DOM (for now)
if (typeof Element == 'undefined') return;
describe('animation tests', function() {
function getLog(): MockAnimationPlayer[] {
return MockAnimationDriver.log as MockAnimationPlayer[];
}
function resetLog() { MockAnimationDriver.log = []; }
beforeEach(() => {
resetLog();
TestBed.configureTestingModule({
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
imports: [BrowserAnimationsModule]
});
});
describe('animation triggers', () => {
it('should trigger a state change animation from void => state', () => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'void => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
expect(getLog().pop().keyframes).toEqual([
{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
]);
});
it('should not throw an error if a trigger with the same name exists in separate components',
() => {
@Component({selector: 'cmp1', template: '...', animations: [trigger('trig', [])]})
class Cmp1 {
}
@Component({selector: 'cmp2', template: '...', animations: [trigger('trig', [])]})
class Cmp2 {
}
TestBed.configureTestingModule({declarations: [Cmp1, Cmp2]});
const cmp1 = TestBed.createComponent(Cmp1);
const cmp2 = TestBed.createComponent(Cmp2);
});
it('should trigger a state change animation from void => state on the component host element',
() => {
@Component({
selector: 'my-cmp',
template: '...',
animations: [trigger(
'myAnimation',
[transition(
'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
@HostBinding('@myAnimation')
get binding() { return this.exp ? 'b' : 'a'; }
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = false;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
cmp.exp = true;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
const data = getLog().pop();
expect(data.element).toEqual(fixture.elementRef.nativeElement);
expect(data.keyframes).toEqual([{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}]);
});
it('should cancel and merge in mid-animation styles into the follow-up animation', () => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[
transition(
'a => b',
[
style({'opacity': '0'}),
animate(500, style({'opacity': '1'})),
]),
transition(
'b => c',
[
style({'width': '0'}),
animate(500, style({'width': '100px'})),
]),
])],
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
resetLog();
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
resetLog();
cmp.exp = 'c';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
const data = getLog().pop();
expect(data.previousStyles).toEqual({opacity: AUTO_STYLE});
});
it('should invoke an animation trigger that is state-less', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngFor="let item of items" @myAnimation></div>
`,
animations: [trigger(
'myAnimation',
[transition(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
})
class Cmp {
items: number[] = [];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.items = [1, 2, 3, 4, 5];
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(5);
for (let i = 0; i < 5; i++) {
const item = getLog()[i];
expect(item.duration).toEqual(1000);
expect(item.keyframes).toEqual([{opacity: '0', offset: 0}, {opacity: '1', offset: 1}]);
}
});
it('should retain styles on the element once the animation is complete', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #green @green></div>
`,
animations: [trigger('green', [state('*', style({backgroundColor: 'green'}))])]
})
class Cmp {
@ViewChild('green') public element: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
engine.flush();
const player = engine.activePlayers.pop();
player.finish();
expect(getDOM().hasStyle(cmp.element.nativeElement, 'background-color', 'green'))
.toBeTruthy();
});
it('should animate removals of nodes to the `void` state for each animation trigger', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngIf="exp" class="ng-if" [@trig1]="exp2" @trig2></div>
`,
animations: [
trigger('trig1', [transition('state => void', [animate(1000, style({opacity: 0}))])]),
trigger('trig2', [transition(':leave', [animate(1000, style({width: '0px'}))])])
]
})
class Cmp {
public exp = true;
public exp2 = 'state';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
engine.flush();
resetLog();
const element = getDOM().querySelector(fixture.nativeElement, '.ng-if');
assertHasParent(element, true);
cmp.exp = false;
fixture.detectChanges();
engine.flush();
assertHasParent(element, true);
expect(getLog().length).toEqual(2);
const player2 = getLog().pop();
const player1 = getLog().pop();
expect(player2.keyframes).toEqual([
{width: AUTO_STYLE, offset: 0},
{width: '0px', offset: 1},
]);
expect(player1.keyframes).toEqual([
{opacity: AUTO_STYLE, offset: 0}, {opacity: '0', offset: 1}
]);
player2.finish();
player1.finish();
assertHasParent(element, false);
});
it('should not run inner child animations when a parent is set to be removed', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngIf="exp" class="parent" >
<div [@myAnimation]="exp2"></div>
</div>
`,
animations: [trigger(
'myAnimation', [transition('a => b', [animate(1000, style({width: '0px'}))])])]
})
class Cmp {
public exp = true;
public exp2 = '0';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
cmp.exp2 = 'a';
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp = false;
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
resetLog();
});
});
describe('animation listeners', () => {
it('should trigger a `start` state change listener for when the animation changes state from void => state',
() => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation]="exp" (@myAnimation.start)="callback($event)"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'void => *',
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
event: AnimationEvent;
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'true';
fixture.detectChanges();
engine.flush();
expect(cmp.event.triggerName).toEqual('myAnimation');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(500);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('true');
});
it('should trigger a `done` state change listener for when the animation changes state from a => b',
() => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation123]="exp" (@myAnimation123.done)="callback($event)"></div>
`,
animations: [trigger(
'myAnimation123',
[transition(
'* => b', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
event: AnimationEvent;
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event).toBeFalsy();
const player = engine.activePlayers.pop();
player.finish();
expect(cmp.event.triggerName).toEqual('myAnimation123');
expect(cmp.event.phaseName).toEqual('done');
expect(cmp.event.totalTime).toEqual(999);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('b');
});
it('should handle callbacks for multiple triggers running simultaneously', () => {
@Component({
selector: 'if-cmp',
template: `
<div [@ani1]="exp1" (@ani1.done)="callback1($event)"></div>
<div [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
`,
animations: [
trigger(
'ani1',
[
transition(
'* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b', [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2).toBeFalsy();
player2.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
it('should handle callbacks for multiple triggers running simultaneously on the same element',
() => {
@Component({
selector: 'if-cmp',
template: `
<div [@ani1]="exp1" (@ani1.done)="callback1($event)" [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
`,
animations: [
trigger(
'ani1',
[
transition(
'* => a',
[style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b',
[style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2).toBeFalsy();
player2.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
it('should trigger a state change listener for when the animation changes state from void => state on the host element',
() => {
@Component({
selector: 'my-cmp',
template: `...`,
animations: [trigger(
'myAnimation2',
[transition(
'void => *',
[style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])])],
})
class Cmp {
event: AnimationEvent;
@HostBinding('@myAnimation2')
exp: any = false;
@HostListener('@myAnimation2.start', ['$event'])
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'TRUE';
fixture.detectChanges();
engine.flush();
expect(cmp.event.triggerName).toEqual('myAnimation2');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(1000);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('TRUE');
});
});
describe('errors for not using the animation module', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{provide: RendererFactory2, useExisting: ɵDomRendererFactory2}],
});
});
it('should throw when using an @prop binding without the animation module', () => {
@Component({template: `<div [@myAnimation]="true"></div>`})
class Cmp {
}
TestBed.configureTestingModule({declarations: [Cmp]});
const comp = TestBed.createComponent(Cmp);
expect(() => comp.detectChanges())
.toThrowError(
'Found the synthetic property @myAnimation. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.');
});
it('should throw when using an @prop listener without the animation module', () => {
@Component({template: `<div (@myAnimation.start)="a = true"></div>`})
class Cmp {
a: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
expect(() => TestBed.createComponent(Cmp))
.toThrowError(
'Found the synthetic listener @myAnimation.start. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.');
});
});
});
}
function assertHasParent(element: any, yes: boolean) {
const parent = getDOM().parentElement(element);
if (yes) {
expect(parent).toBeTruthy();
} else {
expect(parent).toBeFalsy();
}
}

View File

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {APP_INITIALIZER, ApplicationInitStatus} from '../src/application_init';
import {TestBed, async, inject} from '../testing';
export function main() {
describe('ApplicationInitStatus', () => {
describe('no initializers', () => {
it('should return true for `done`',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
expect(status.done).toBe(true);
})));
it('should return a promise that resolves immediately for `donePromise`',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
status.donePromise.then(() => { expect(status.done).toBe(true); });
})));
});
describe('with async initializers', () => {
let resolve: (result: any) => void;
let promise: Promise<any>;
beforeEach(() => {
promise = new Promise((res) => { resolve = res; });
TestBed.configureTestingModule(
{providers: [{provide: APP_INITIALIZER, multi: true, useValue: () => promise}]});
});
it('should update the status once all async initializers are done',
async(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
let completerResolver = false;
setTimeout(() => {
completerResolver = true;
resolve(null);
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(completerResolver).toBe(true);
});
})));
});
});
}

View File

@ -0,0 +1,17 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {LOCALE_ID} from '@angular/core';
import {describe, expect, inject, it} from '../testing/testing_internal';
export function main() {
describe('Application module', () => {
it('should set the default locale to "en-US"',
inject([LOCALE_ID], (defaultLocale: string) => { expect(defaultLocale).toEqual('en-US'); }));
});
}

View File

@ -0,0 +1,515 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
import {TestComponentRenderer} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
import {expect} from '@angular/platform-browser/testing/matchers';
import {ServerModule} from '@angular/platform-server';
import {ComponentFixture, ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
@Component({selector: 'bootstrap-app', template: 'hello'})
class SomeComponent {
}
export function main() {
describe('bootstrap', () => {
let mockConsole: MockConsole;
beforeEach(() => { mockConsole = new MockConsole(); });
function createRootEl() {
const doc = TestBed.get(DOCUMENT);
const rootEl = <HTMLElement>getDOM().firstChild(
getDOM().content(getDOM().createTemplate(`<bootstrap-app></bootstrap-app>`)));
const oldRoots = getDOM().querySelectorAll(doc, 'bootstrap-app');
for (let i = 0; i < oldRoots.length; i++) {
getDOM().remove(oldRoots[i]);
}
getDOM().appendChild(doc.body, rootEl);
}
type CreateModuleOptions = {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[]};
function createModule(providers?: any[]): Type<any>;
function createModule(options: CreateModuleOptions): Type<any>;
function createModule(providersOrOptions: any[] | CreateModuleOptions): Type<any> {
let options: CreateModuleOptions = {};
if (providersOrOptions instanceof Array) {
options = {providers: providersOrOptions};
} else {
options = providersOrOptions || {};
}
const errorHandler = new ErrorHandler(false);
errorHandler._console = mockConsole as any;
const platformModule = getDOM().supportsDOMEvents() ? BrowserModule : ServerModule;
@NgModule({
providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
imports: [platformModule],
declarations: [SomeComponent],
entryComponents: [SomeComponent],
bootstrap: options.bootstrap || []
})
class MyModule {
}
if (options.ngDoBootstrap !== false) {
(<any>MyModule.prototype).ngDoBootstrap = options.ngDoBootstrap || (() => {});
}
return MyModule;
}
describe('ApplicationRef', () => {
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => {
const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']);
const viewRef = jasmine.createSpyObj(
'viewRef', ['detectChanges', 'detachFromAppRef', 'attachToAppRef']);
viewRef.internalView = view;
view.ref = viewRef;
try {
ref.attachView(viewRef);
viewRef.detectChanges.and.callFake(() => ref.tick());
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
} finally {
ref.detachView(viewRef);
}
}));
describe('APP_BOOTSTRAP_LISTENER', () => {
let capturedCompRefs: ComponentRef<any>[];
beforeEach(() => {
capturedCompRefs = [];
TestBed.configureTestingModule({
providers: [{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useValue: (compRef: any) => { capturedCompRefs.push(compRef); }
}]
});
});
it('should be called when a component is bootstrapped',
inject([ApplicationRef], (ref: ApplicationRef_) => {
createRootEl();
const compRef = ref.bootstrap(SomeComponent);
expect(capturedCompRefs).toEqual([compRef]);
}));
});
describe('bootstrap', () => {
it('should throw if an APP_INITIIALIZER is not yet resolved',
withModule(
{
providers: [
{provide: APP_INITIALIZER, useValue: () => new Promise(() => {}), multi: true}
]
},
inject([ApplicationRef], (ref: ApplicationRef_) => {
createRootEl();
expect(() => ref.bootstrap(SomeComponent))
.toThrowError(
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
})));
});
});
describe('bootstrapModule', () => {
let defaultPlatform: PlatformRef;
beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
createRootEl();
defaultPlatform = _platform;
}));
it('should wait for asynchronous app initializers', async(() => {
let resolve: (result: any) => void;
const promise: Promise<any> = new Promise((res) => { resolve = res; });
let initializerDone = false;
setTimeout(() => {
resolve(true);
initializerDone = true;
}, 1);
defaultPlatform
.bootstrapModule(
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]))
.then(_ => { expect(initializerDone).toBe(true); });
}));
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
defaultPlatform
.bootstrapModule(createModule(
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
// Note: if the modules throws an error during construction,
// we don't have an injector and therefore no way of
// getting the exception handler. So
// the error is only rethrown but not logged via the exception handler.
expect(mockConsole.res).toEqual([]);
});
}));
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
async(() => {
defaultPlatform
.bootstrapModule(createModule([
{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}
]))
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
expect(mockConsole.res).toEqual(['EXCEPTION: Test']);
});
}));
it('should throw useful error when ApplicationRef is not configured', async(() => {
@NgModule()
class EmptyModule {
}
return defaultPlatform.bootstrapModule(EmptyModule)
.then(() => fail('expecting error'), (error) => {
expect(error.message)
.toEqual('No ErrorHandler. Is platform module (BrowserModule) included?');
});
}));
it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module',
async(() => {
const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap}))
.then((moduleRef) => {
const appRef = moduleRef.injector.get(ApplicationRef);
expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
});
}));
it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => {
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
.then((moduleRef) => {
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
expect(appRef.componentTypes).toEqual([SomeComponent]);
});
}));
it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified',
async(() => {
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false}))
.then(() => expect(false).toBe(true), (e) => {
const expectedErrMsg =
`The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`;
expect(e.message).toEqual(expectedErrMsg);
expect(mockConsole.res[0]).toEqual('EXCEPTION: ' + expectedErrMsg);
});
}));
it('should add bootstrapped module into platform modules list', async(() => {
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
.then(module => expect((<any>defaultPlatform)._modules).toContain(module));
}));
});
describe('bootstrapModuleFactory', () => {
let defaultPlatform: PlatformRef;
beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
createRootEl();
defaultPlatform = _platform;
}));
it('should wait for asynchronous app initializers', async(() => {
let resolve: (result: any) => void;
const promise: Promise<any> = new Promise((res) => { resolve = res; });
let initializerDone = false;
setTimeout(() => {
resolve(true);
initializerDone = true;
}, 1);
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]));
defaultPlatform.bootstrapModuleFactory(moduleFactory).then(_ => {
expect(initializerDone).toBe(true);
});
}));
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
// Note: if the modules throws an error during construction,
// we don't have an injector and therefore no way of
// getting the exception handler. So
// the error is only rethrown but not logged via the exception handler.
expect(mockConsole.res).toEqual([]);
}));
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
async(() => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null);
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}]));
defaultPlatform.bootstrapModuleFactory(moduleFactory)
.then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test');
expect(mockConsole.res).toEqual(['EXCEPTION: Test']);
});
}));
});
describe('attachView / detachView', () => {
@Component({template: '{{name}}'})
class MyComp {
name = 'Initial';
}
@Component({template: '<ng-container #vc></ng-container>'})
class ContainerComp {
@ViewChild('vc', {read: ViewContainerRef})
vc: ViewContainerRef;
}
@Component({template: '<ng-template #t>Dynamic content</ng-template>'})
class EmbeddedViewComp {
@ViewChild(TemplateRef)
tplRef: TemplateRef<Object>;
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComp, ContainerComp, EmbeddedViewComp],
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
});
});
it('should dirty check attached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
expect(appRef.viewCount).toBe(0);
appRef.tick();
expect(comp.nativeElement).toHaveText('');
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(appRef.viewCount).toBe(1);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should not dirty check detached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(comp.nativeElement).toHaveText('Initial');
appRef.detachView(comp.componentRef.hostView);
comp.componentInstance.name = 'New';
appRef.tick();
expect(appRef.viewCount).toBe(0);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should detach attached views if they are destroyed', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
comp.destroy();
expect(appRef.viewCount).toBe(0);
});
it('should detach attached embedded views if they are destroyed', () => {
const comp = TestBed.createComponent(EmbeddedViewComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({});
appRef.attachView(embeddedViewRef);
embeddedViewRef.destroy();
expect(appRef.viewCount).toBe(0);
});
it('should not allow to attach a view to both, a view container and the ApplicationRef',
() => {
const comp = TestBed.createComponent(MyComp);
let hostView = comp.componentRef.hostView;
const containerComp = TestBed.createComponent(ContainerComp);
containerComp.detectChanges();
const vc = containerComp.componentInstance.vc;
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
vc.insert(hostView);
expect(() => appRef.attachView(hostView))
.toThrowError('This view is already attached to a ViewContainer!');
hostView = vc.detach(0);
appRef.attachView(hostView);
expect(() => vc.insert(hostView))
.toThrowError('This view is already attached directly to the ApplicationRef!');
});
});
});
describe('AppRef', () => {
@Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
class SyncComp {
text: string = '1';
}
@Component({selector: 'click-comp', template: `<span (click)="onClick()">{{text}}</span>`})
class ClickComp {
text: string = '1';
onClick() { this.text += '1'; }
}
@Component({selector: 'micro-task-comp', template: `<span>{{text}}</span>`})
class MicroTaskComp {
text: string = '1';
ngOnInit() {
Promise.resolve(null).then((_) => { this.text += '1'; });
}
}
@Component({selector: 'macro-task-comp', template: `<span>{{text}}</span>`})
class MacroTaskComp {
text: string = '1';
ngOnInit() {
setTimeout(() => { this.text += '1'; }, 10);
}
}
@Component({selector: 'micro-macro-task-comp', template: `<span>{{text}}</span>`})
class MicroMacroTaskComp {
text: string = '1';
ngOnInit() {
Promise.resolve(null).then((_) => {
this.text += '1';
setTimeout(() => { this.text += '1'; }, 10);
});
}
}
@Component({selector: 'macro-micro-task-comp', template: `<span>{{text}}</span>`})
class MacroMicroTaskComp {
text: string = '1';
ngOnInit() {
setTimeout(() => {
this.text += '1';
Promise.resolve(null).then((_: any) => { this.text += '1'; });
}, 10);
}
}
let stableCalled = false;
beforeEach(() => {
stableCalled = false;
TestBed.configureTestingModule({
declarations: [
SyncComp, MicroTaskComp, MacroTaskComp, MicroMacroTaskComp, MacroMicroTaskComp, ClickComp
],
});
});
afterEach(() => { expect(stableCalled).toBe(true, 'isStable did not emit true on stable'); });
function expectStableTexts(component: Type<any>, expected: string[]) {
const fixture = TestBed.createComponent(component);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
const zone: NgZone = TestBed.get(NgZone);
appRef.attachView(fixture.componentRef.hostView);
zone.run(() => appRef.tick());
let i = 0;
appRef.isStable.subscribe({
next: (stable: boolean) => {
if (stable) {
expect(i).toBeLessThan(expected.length);
expect(fixture.nativeElement).toHaveText(expected[i++]);
stableCalled = true;
}
}
});
}
it('isStable should fire on synchronous component loading',
async(() => { expectStableTexts(SyncComp, ['1']); }));
it('isStable should fire after a microtask on init is completed',
async(() => { expectStableTexts(MicroTaskComp, ['11']); }));
it('isStable should fire after a macrotask on init is completed',
async(() => { expectStableTexts(MacroTaskComp, ['11']); }));
it('isStable should fire only after chain of micro and macrotasks on init are completed',
async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); }));
it('isStable should fire only after chain of macro and microtasks on init are completed',
async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); }));
describe('unstable', () => {
let unstableCalled = false;
afterEach(
() => { expect(unstableCalled).toBe(true, 'isStable did not emit false on unstable'); });
function expectUnstable(appRef: ApplicationRef) {
appRef.isStable.subscribe({
next: (stable: boolean) => {
if (stable) {
stableCalled = true;
}
if (!stable) {
unstableCalled = true;
}
}
});
}
it('should be fired after app becomes unstable', async(() => {
const fixture = TestBed.createComponent(ClickComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
const zone: NgZone = TestBed.get(NgZone);
appRef.attachView(fixture.componentRef.hostView);
zone.run(() => appRef.tick());
fixture.whenStable().then(() => {
expectUnstable(appRef);
const element = fixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
});
}));
});
});
}
class MockConsole {
res: any[] = [];
log(s: any): void { this.res.push(s); }
error(s: any): void { this.res.push(s); }
}

View File

@ -0,0 +1,57 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {devModeEqual} from '@angular/core/src/change_detection/change_detection_util';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('ChangeDetectionUtil', () => {
describe('devModeEqual', () => {
it('should do the deep comparison of iterables', () => {
expect(devModeEqual([['one']], [['one']])).toBe(true);
expect(devModeEqual(['one'], ['one', 'two'])).toBe(false);
expect(devModeEqual(['one', 'two'], ['one'])).toBe(false);
expect(devModeEqual(['one'], 'one')).toBe(false);
expect(devModeEqual(['one'], new Object())).toBe(false);
expect(devModeEqual('one', ['one'])).toBe(false);
expect(devModeEqual(new Object(), ['one'])).toBe(false);
});
it('should compare primitive numbers', () => {
expect(devModeEqual(1, 1)).toBe(true);
expect(devModeEqual(1, 2)).toBe(false);
expect(devModeEqual(new Object(), 2)).toBe(false);
expect(devModeEqual(1, new Object())).toBe(false);
});
it('should compare primitive strings', () => {
expect(devModeEqual('one', 'one')).toBe(true);
expect(devModeEqual('one', 'two')).toBe(false);
expect(devModeEqual(new Object(), 'one')).toBe(false);
expect(devModeEqual('one', new Object())).toBe(false);
});
it('should compare primitive booleans', () => {
expect(devModeEqual(true, true)).toBe(true);
expect(devModeEqual(true, false)).toBe(false);
expect(devModeEqual(new Object(), true)).toBe(false);
expect(devModeEqual(true, new Object())).toBe(false);
});
it('should compare null', () => {
expect(devModeEqual(null, null)).toBe(true);
expect(devModeEqual(null, 1)).toBe(false);
expect(devModeEqual(new Object(), null)).toBe(false);
expect(devModeEqual(null, new Object())).toBe(false);
});
it('should return true for other objects',
() => { expect(devModeEqual(new Object(), new Object())).toBe(true); });
});
});
}

View File

@ -0,0 +1,592 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DefaultIterableDiffer, DefaultIterableDifferFactory} from '@angular/core/src/change_detection/differs/default_iterable_differ';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {TestIterable} from '../../change_detection/iterable';
import {iterableChangesAsString} from '../../change_detection/util';
class ItemWithId {
constructor(private id: string) {}
toString() { return `{id: ${this.id}}`; }
}
class ComplexItem {
constructor(private id: string, private color: string) {}
toString() { return `{id: ${this.id}, color: ${this.color}}`; }
}
// todo(vicb): UnmodifiableListView / frozen object when implemented
export function main() {
describe('iterable differ', function() {
describe('DefaultIterableDiffer', function() {
let differ: any /** TODO #9100 */;
beforeEach(() => { differ = new DefaultIterableDiffer(); });
it('should support list and iterables', () => {
const f = new DefaultIterableDifferFactory();
expect(f.supports([])).toBeTruthy();
expect(f.supports(new TestIterable())).toBeTruthy();
expect(f.supports(new Map())).toBeFalsy();
expect(f.supports(null)).toBeFalsy();
});
it('should support iterables', () => {
const l = new TestIterable();
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
l.list = [1];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['1[null->0]'],
additions: ['1[null->0]']
}));
l.list = [2, 1];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['2[null->0]', '1[0->1]'],
previous: ['1[0->1]'],
additions: ['2[null->0]'],
moves: ['1[0->1]']
}));
});
it('should detect additions', () => {
const l: any[] /** TODO #9100 */ = [];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
l.push('a');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a[null->0]'],
additions: ['a[null->0]']
}));
l.push('b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString(
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
});
it('should support changing the reference', () => {
let l = [0];
differ.check(l);
l = [1, 0];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['1[null->0]', '0[0->1]'],
previous: ['0[0->1]'],
additions: ['1[null->0]'],
moves: ['0[0->1]']
}));
l = [2, 1, 0];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
previous: ['1[0->1]', '0[1->2]'],
additions: ['2[null->0]'],
moves: ['1[0->1]', '0[1->2]']
}));
});
it('should handle swapping element', () => {
const l = [1, 2];
differ.check(l);
l.length = 0;
l.push(2);
l.push(1);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['2[1->0]', '1[0->1]'],
previous: ['1[0->1]', '2[1->0]'],
moves: ['2[1->0]', '1[0->1]']
}));
});
it('should handle incremental swapping element', () => {
const l = ['a', 'b', 'c'];
differ.check(l);
l.splice(1, 1);
l.splice(0, 0, 'b');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['b[1->0]', 'a[0->1]', 'c'],
previous: ['a[0->1]', 'b[1->0]', 'c'],
moves: ['b[1->0]', 'a[0->1]']
}));
l.splice(1, 1);
l.push('a');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['b', 'c[2->1]', 'a[1->2]'],
previous: ['b', 'a[1->2]', 'c[2->1]'],
moves: ['c[2->1]', 'a[1->2]']
}));
});
it('should detect changes in list', () => {
const l: any[] /** TODO #9100 */ = [];
differ.check(l);
l.push('a');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a[null->0]'],
additions: ['a[null->0]']
}));
l.push('b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString(
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
l.push('c');
l.push('d');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
previous: ['a', 'b'],
additions: ['c[null->2]', 'd[null->3]']
}));
l.splice(2, 1);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a', 'b', 'd[3->2]'],
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
moves: ['d[3->2]'],
removals: ['c[2->null]']
}));
l.length = 0;
l.push('d');
l.push('c');
l.push('b');
l.push('a');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'],
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
additions: ['c[null->1]'],
moves: ['d[2->0]', 'b[1->2]', 'a[0->3]']
}));
});
it('should test string by value rather than by reference (Dart)', () => {
const l = ['a', 'boo'];
differ.check(l);
const b = 'b';
const oo = 'oo';
l[1] = b + oo;
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({collection: ['a', 'boo'], previous: ['a', 'boo']}));
});
it('should ignore [NaN] != [NaN] (JS)', () => {
const l = [NaN];
differ.check(l);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({collection: [NaN], previous: [NaN]}));
});
it('should detect [NaN] moves', () => {
const l: any[] = [NaN, NaN];
differ.check(l);
l.unshift('foo');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
previous: ['NaN[0->1]', 'NaN[1->2]'],
additions: ['foo[null->0]'],
moves: ['NaN[0->1]', 'NaN[1->2]']
}));
});
it('should remove and add same item', () => {
const l = ['a', 'b', 'c'];
differ.check(l);
l.splice(1, 1);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a', 'c[2->1]'],
previous: ['a', 'b[1->null]', 'c[2->1]'],
moves: ['c[2->1]'],
removals: ['b[1->null]']
}));
l.splice(1, 0, 'b');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a', 'b[null->1]', 'c[1->2]'],
previous: ['a', 'c[1->2]'],
additions: ['b[null->1]'],
moves: ['c[1->2]']
}));
});
it('should support duplicates', () => {
const l = ['a', 'a', 'a', 'b', 'b'];
differ.check(l);
l.splice(0, 1);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'],
moves: ['b[3->2]', 'b[4->3]'],
removals: ['a[2->null]']
}));
});
it('should support insertions/moves', () => {
const l = ['a', 'a', 'b', 'b'];
differ.check(l);
l.splice(0, 0, 'b');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'],
previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'],
additions: ['b[null->4]'],
moves: ['b[2->0]', 'a[0->1]', 'a[1->2]']
}));
});
it('should not report unnecessary moves', () => {
const l = ['a', 'b', 'c'];
differ.check(l);
l.length = 0;
l.push('b');
l.push('a');
l.push('c');
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['b[1->0]', 'a[0->1]', 'c'],
previous: ['a[0->1]', 'b[1->0]', 'c'],
moves: ['b[1->0]', 'a[0->1]']
}));
});
describe('forEachOperation', () => {
function stringifyItemChange(record: any, p: number, c: number, originalIndex: number) {
const suffix = originalIndex == null ? '' : ' [o=' + originalIndex + ']';
const value = record.item;
if (record.currentIndex == null) {
return `REMOVE ${value} (${p} -> VOID)${suffix}`;
} else if (record.previousIndex == null) {
return `INSERT ${value} (VOID -> ${c})${suffix}`;
} else {
return `MOVE ${value} (${p} -> ${c})${suffix}`;
}
}
function modifyArrayUsingOperation(
arr: number[], endData: any[], prev: number, next: number) {
let value: number = null;
if (prev == null) {
value = endData[next];
arr.splice(next, 0, value);
} else if (next == null) {
value = arr[prev];
arr.splice(prev, 1);
} else {
value = arr[prev];
arr.splice(prev, 1);
arr.splice(next, 0, value);
}
return value;
}
it('should trigger a series of insert/move/remove changes for inputs that have been diffed',
() => {
const startData = [0, 1, 2, 3, 4, 5];
const endData = [6, 2, 7, 0, 4, 8];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([
'INSERT 6 (VOID -> 0)', 'MOVE 2 (3 -> 1) [o=2]', 'INSERT 7 (VOID -> 2)',
'REMOVE 1 (4 -> VOID) [o=1]', 'REMOVE 3 (4 -> VOID) [o=3]',
'REMOVE 5 (5 -> VOID) [o=5]', 'INSERT 8 (VOID -> 5)'
]);
expect(startData).toEqual(endData);
});
it('should consider inserting/removing/moving items with respect to items that have not moved at all',
() => {
const startData = [0, 1, 2, 3];
const endData = [2, 1];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([
'REMOVE 0 (0 -> VOID) [o=0]', 'MOVE 2 (1 -> 0) [o=2]', 'REMOVE 3 (2 -> VOID) [o=3]'
]);
expect(startData).toEqual(endData);
});
it('should be able to manage operations within a criss/cross of move operations', () => {
const startData = [1, 2, 3, 4, 5, 6];
const endData = [3, 6, 4, 9, 1, 2];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([
'MOVE 3 (2 -> 0) [o=2]', 'MOVE 6 (5 -> 1) [o=5]', 'MOVE 4 (4 -> 2) [o=3]',
'INSERT 9 (VOID -> 3)', 'REMOVE 5 (6 -> VOID) [o=4]'
]);
expect(startData).toEqual(endData);
});
it('should skip moves for multiple nodes that have not moved', () => {
const startData = [0, 1, 2, 3, 4];
const endData = [4, 1, 2, 3, 0, 5];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([
'MOVE 4 (4 -> 0) [o=4]', 'MOVE 1 (2 -> 1) [o=1]', 'MOVE 2 (3 -> 2) [o=2]',
'MOVE 3 (4 -> 3) [o=3]', 'INSERT 5 (VOID -> 5)'
]);
expect(startData).toEqual(endData);
});
it('should not fail', () => {
const startData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const endData = [10, 11, 1, 5, 7, 8, 0, 5, 3, 6];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([
'MOVE 10 (10 -> 0) [o=10]', 'MOVE 11 (11 -> 1) [o=11]', 'MOVE 1 (3 -> 2) [o=1]',
'MOVE 5 (7 -> 3) [o=5]', 'MOVE 7 (9 -> 4) [o=7]', 'MOVE 8 (10 -> 5) [o=8]',
'REMOVE 2 (7 -> VOID) [o=2]', 'INSERT 5 (VOID -> 7)', 'REMOVE 4 (9 -> VOID) [o=4]',
'REMOVE 9 (10 -> VOID) [o=9]'
]);
expect(startData).toEqual(endData);
});
it('should trigger nothing when the list is completely full of replaced items that are tracked by the index',
() => {
differ = new DefaultIterableDiffer((index: number) => index);
const startData = [1, 2, 3, 4];
const endData = [5, 6, 7, 8];
differ = differ.diff(startData);
differ = differ.diff(endData);
const operations: string[] = [];
differ.forEachOperation((item: any, prev: number, next: number) => {
const value = modifyArrayUsingOperation(startData, endData, prev, next);
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
});
expect(operations).toEqual([]);
});
});
describe('diff', () => {
it('should return self when there is a change', () => {
expect(differ.diff(['a', 'b'])).toBe(differ);
});
it('should return null when there is no change', () => {
differ.diff(['a', 'b']);
expect(differ.diff(['a', 'b'])).toEqual(null);
});
it('should treat null as an empty list', () => {
differ.diff(['a', 'b']);
expect(differ.diff(null).toString()).toEqual(iterableChangesAsString({
previous: ['a[0->null]', 'b[1->null]'],
removals: ['a[0->null]', 'b[1->null]']
}));
});
it('should throw when given an invalid collection', () => {
expect(() => differ.diff('invalid')).toThrowError('Error trying to diff \'invalid\'');
});
});
});
describe('trackBy function by id', function() {
let differ: any /** TODO #9100 */;
const trackByItemId = (index: number, item: any): any => item.id;
const buildItemList = (list: string[]) => list.map((val) => new ItemWithId(val));
beforeEach(() => { differ = new DefaultIterableDiffer(trackByItemId); });
it('should treat the collection as dirty if identity changes', () => {
differ.diff(buildItemList(['a']));
expect(differ.diff(buildItemList(['a']))).toBe(differ);
});
it('should treat seen records as identity changes, not additions', () => {
let l = buildItemList(['a', 'b', 'c']);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`],
additions: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`]
}));
l = buildItemList(['a', 'b', 'c']);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: [`{id: a}`, `{id: b}`, `{id: c}`],
identityChanges: [`{id: a}`, `{id: b}`, `{id: c}`],
previous: [`{id: a}`, `{id: b}`, `{id: c}`]
}));
});
it('should have updated properties in identity change collection', () => {
let l = [new ComplexItem('a', 'blue'), new ComplexItem('b', 'yellow')];
differ.check(l);
l = [new ComplexItem('a', 'orange'), new ComplexItem('b', 'red')];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: [`{id: a, color: orange}`, `{id: b, color: red}`],
identityChanges: [`{id: a, color: orange}`, `{id: b, color: red}`],
previous: [`{id: a, color: orange}`, `{id: b, color: red}`]
}));
});
it('should track moves normally', () => {
let l = buildItemList(['a', 'b', 'c']);
differ.check(l);
l = buildItemList(['b', 'a', 'c']);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['{id: b}[1->0]', '{id: a}[0->1]', '{id: c}'],
identityChanges: ['{id: b}[1->0]', '{id: a}[0->1]', '{id: c}'],
previous: ['{id: a}[0->1]', '{id: b}[1->0]', '{id: c}'],
moves: ['{id: b}[1->0]', '{id: a}[0->1]']
}));
});
it('should track duplicate reinsertion normally', () => {
let l = buildItemList(['a', 'a']);
differ.check(l);
l = buildItemList(['b', 'a', 'a']);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['{id: b}[null->0]', '{id: a}[0->1]', '{id: a}[1->2]'],
identityChanges: ['{id: a}[0->1]', '{id: a}[1->2]'],
previous: ['{id: a}[0->1]', '{id: a}[1->2]'],
moves: ['{id: a}[0->1]', '{id: a}[1->2]'],
additions: ['{id: b}[null->0]']
}));
});
it('should track removals normally', () => {
const l = buildItemList(['a', 'b', 'c']);
differ.check(l);
l.splice(2, 1);
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['{id: a}', '{id: b}'],
previous: ['{id: a}', '{id: b}', '{id: c}[2->null]'],
removals: ['{id: c}[2->null]']
}));
});
});
describe('trackBy function by index', function() {
let differ: any /** TODO #9100 */;
const trackByIndex = (index: number, item: any): number => index;
beforeEach(() => { differ = new DefaultIterableDiffer(trackByIndex); });
it('should track removals normally', () => {
differ.check(['a', 'b', 'c', 'd']);
differ.check(['e', 'f', 'g', 'h']);
differ.check(['e', 'f', 'h']);
expect(differ.toString()).toEqual(iterableChangesAsString({
collection: ['e', 'f', 'h'],
previous: ['e', 'f', 'h', 'h[3->null]'],
removals: ['h[3->null]'],
identityChanges: ['h']
}));
});
});
});
}

View File

@ -0,0 +1,229 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DefaultKeyValueDiffer, DefaultKeyValueDifferFactory} from '@angular/core/src/change_detection/differs/default_keyvalue_differ';
import {afterEach, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {kvChangesAsString} from '../../change_detection/util';
// todo(vicb): Update the code & tests for object equality
export function main() {
describe('keyvalue differ', function() {
describe('DefaultKeyValueDiffer', function() {
let differ: DefaultKeyValueDiffer<any, any>;
let m: Map<any, any>;
beforeEach(() => {
differ = new DefaultKeyValueDiffer<string, any>();
m = new Map();
});
afterEach(() => { differ = null; });
it('should detect additions', () => {
differ.check(m);
m.set('a', 1);
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->1]'], additions: ['a[null->1]']}));
m.set('b', 2);
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->2]'], previous: ['a'], additions: ['b[null->2]']}));
});
it('should handle changing key/values correctly', () => {
m.set(1, 10);
m.set(2, 20);
differ.check(m);
m.set(2, 10);
m.set(1, 20);
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['1[10->20]', '2[20->10]'],
previous: ['1[10->20]', '2[20->10]'],
changes: ['1[10->20]', '2[20->10]']
}));
});
it('should expose previous and current value', () => {
let previous: any /** TODO #9100 */, current: any /** TODO #9100 */;
m.set(1, 10);
differ.check(m);
m.set(1, 20);
differ.check(m);
differ.forEachChangedItem((record: any /** TODO #9100 */) => {
previous = record.previousValue;
current = record.currentValue;
});
expect(previous).toEqual(10);
expect(current).toEqual(20);
});
it('should do basic map watching', () => {
differ.check(m);
m.set('a', 'A');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
m.set('b', 'B');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
m.set('b', 'BB');
m.set('d', 'D');
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['a', 'b[B->BB]', 'd[null->D]'],
previous: ['a', 'b[B->BB]'],
additions: ['d[null->D]'],
changes: ['b[B->BB]']
}));
m.delete('b');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'd'], previous: ['a', 'b[BB->null]', 'd'], removals: ['b[BB->null]']}));
m.clear();
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
previous: ['a[A->null]', 'd[D->null]'],
removals: ['a[A->null]', 'd[D->null]']
}));
});
it('should not see a NaN value as a change', () => {
m.set('foo', Number.NaN);
differ.check(m);
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({map: ['foo'], previous: ['foo']}));
});
it('should work regardless key order', () => {
m.set('a', 0);
m.set('b', 0);
differ.check(m);
m = new Map();
m.set('b', 1);
m.set('a', 1);
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['b[0->1]', 'a[0->1]'],
previous: ['a[0->1]', 'b[0->1]'],
changes: ['b[0->1]', 'a[0->1]']
}));
});
describe('JsObject changes', () => {
it('should support JS Object', () => {
const f = new DefaultKeyValueDifferFactory();
expect(f.supports({})).toBeTruthy();
expect(f.supports('not supported')).toBeFalsy();
expect(f.supports(0)).toBeFalsy();
expect(f.supports(null)).toBeFalsy();
});
it('should do basic object watching', () => {
let m: {[k: string]: string} = {};
differ.check(m);
m['a'] = 'A';
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
m['b'] = 'B';
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
m['b'] = 'BB';
m['d'] = 'D';
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['a', 'b[B->BB]', 'd[null->D]'],
previous: ['a', 'b[B->BB]'],
additions: ['d[null->D]'],
changes: ['b[B->BB]']
}));
m = {};
m['a'] = 'A';
m['d'] = 'D';
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['a', 'd'],
previous: ['a', 'b[BB->null]', 'd'],
removals: ['b[BB->null]']
}));
m = {};
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({
previous: ['a[A->null]', 'd[D->null]'],
removals: ['a[A->null]', 'd[D->null]']
}));
});
it('should work regardless key order', () => {
differ.check({a: 0, b: 0});
differ.check({b: 1, a: 1});
expect(differ.toString()).toEqual(kvChangesAsString({
map: ['b[0->1]', 'a[0->1]'],
previous: ['a[0->1]', 'b[0->1]'],
changes: ['b[0->1]', 'a[0->1]']
}));
});
});
describe('diff', () => {
it('should return self when there is a change', () => {
m.set('a', 'A');
expect(differ.diff(m)).toBe(differ);
});
it('should return null when there is no change', () => {
m.set('a', 'A');
differ.diff(m);
expect(differ.diff(m)).toEqual(null);
});
it('should treat null as an empty list', () => {
m.set('a', 'A');
differ.diff(m);
expect(differ.diff(null).toString())
.toEqual(kvChangesAsString({previous: ['a[A->null]'], removals: ['a[A->null]']}));
});
it('should throw when given an invalid collection', () => {
expect(() => differ.diff(<any>'invalid'))
.toThrowError('Error trying to diff \'invalid\'');
});
});
});
});
}

View File

@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ReflectiveInjector} from '@angular/core';
import {IterableDiffers} from '@angular/core/src/change_detection/differs/iterable_differs';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {SpyIterableDifferFactory} from '../../spies';
export function main() {
describe('IterableDiffers', function() {
let factory1: any /** TODO #9100 */;
let factory2: any /** TODO #9100 */;
let factory3: any /** TODO #9100 */;
beforeEach(() => {
factory1 = new SpyIterableDifferFactory();
factory2 = new SpyIterableDifferFactory();
factory3 = new SpyIterableDifferFactory();
});
it('should throw when no suitable implementation found', () => {
const differs = new IterableDiffers([]);
expect(() => differs.find('some object'))
.toThrowError(/Cannot find a differ supporting object 'some object'/);
});
it('should return the first suitable implementation', () => {
factory1.spy('supports').and.returnValue(false);
factory2.spy('supports').and.returnValue(true);
factory3.spy('supports').and.returnValue(true);
const differs = IterableDiffers.create(<any>[factory1, factory2, factory3]);
expect(differs.find('some object')).toBe(factory2);
});
it('should copy over differs from the parent repo', () => {
factory1.spy('supports').and.returnValue(true);
factory2.spy('supports').and.returnValue(false);
const parent = IterableDiffers.create(<any>[factory1]);
const child = IterableDiffers.create(<any>[factory2], parent);
expect(child.factories).toEqual([factory2, factory1]);
});
describe('.extend()', () => {
it('should throw if calling extend when creating root injector', () => {
const injector = ReflectiveInjector.resolveAndCreate([IterableDiffers.extend([])]);
expect(() => injector.get(IterableDiffers))
.toThrowError(/Cannot extend IterableDiffers without a parent injector/);
});
it('should extend di-inherited diffesr', () => {
const parent = new IterableDiffers([factory1]);
const injector =
ReflectiveInjector.resolveAndCreate([{provide: IterableDiffers, useValue: parent}]);
const childInjector = injector.resolveAndCreateChild([IterableDiffers.extend([factory2])]);
expect(injector.get(IterableDiffers).factories).toEqual([factory1]);
expect(childInjector.get(IterableDiffers).factories).toEqual([factory2, factory1]);
});
});
});
}

View File

@ -0,0 +1,16 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getSymbolIterator} from '@angular/core/src/util';
export class TestIterable {
list: number[];
constructor() { this.list = []; }
[getSymbolIterator()]() { return (this.list as any)[getSymbolIterator()](); }
}

View File

@ -0,0 +1,37 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export function iterableChangesAsString(
{collection = [] as any, previous = [] as any, additions = [] as any, moves = [] as any,
removals = [] as any, identityChanges = [] as any}): string {
return 'collection: ' + collection.join(', ') + '\n' +
'previous: ' + previous.join(', ') + '\n' +
'additions: ' + additions.join(', ') + '\n' +
'moves: ' + moves.join(', ') + '\n' +
'removals: ' + removals.join(', ') + '\n' +
'identityChanges: ' + identityChanges.join(', ') + '\n';
}
export function kvChangesAsString(
{map, previous, additions, changes, removals}:
{map?: any[], previous?: any[], additions?: any[], changes?: any[], removals?: any[]}):
string {
if (!map) map = [];
if (!previous) previous = [];
if (!additions) additions = [];
if (!changes) changes = [];
if (!removals) removals = [];
return 'map: ' + map.join(', ') + '\n' +
'previous: ' + previous.join(', ') + '\n' +
'additions: ' + additions.join(', ') + '\n' +
'changes: ' + changes.join(', ') + '\n' +
'removals: ' + removals.join(', ') + '\n';
}

View File

@ -0,0 +1,324 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Injectable, Input} from '@angular/core';
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestBed, async, withModule} from '@angular/core/testing';
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
import {expect} from '@angular/platform-browser/testing/matchers';
@Component({selector: 'simple-comp', template: `<span>Original {{simpleBinding}}</span>`})
@Injectable()
class SimpleComp {
simpleBinding: string;
constructor() { this.simpleBinding = 'Simple'; }
}
@Component({
selector: 'my-if-comp',
template: `MyIf(<span *ngIf="showMore">More</span>)`,
})
@Injectable()
class MyIfComp {
showMore: boolean = false;
}
@Component({selector: 'autodetect-comp', template: `<span (click)='click()'>{{text}}</span>`})
class AutoDetectComp {
text: string = '1';
click() { this.text += '1'; }
}
@Component({selector: 'async-comp', template: `<span (click)='click()'>{{text}}</span>`})
class AsyncComp {
text: string = '1';
click() {
Promise.resolve(null).then((_) => { this.text += '1'; });
}
}
@Component({selector: 'async-child-comp', template: '<span>{{localText}}</span>'})
class AsyncChildComp {
localText: string = '';
@Input()
set text(value: string) {
Promise.resolve(null).then((_) => { this.localText = value; });
}
}
@Component({
selector: 'async-change-comp',
template: `<async-child-comp (click)='click()' [text]="text"></async-child-comp>`
})
class AsyncChangeComp {
text: string = '1';
click() { this.text += '1'; }
}
@Component({selector: 'async-timeout-comp', template: `<span (click)='click()'>{{text}}</span>`})
class AsyncTimeoutComp {
text: string = '1';
click() {
setTimeout(() => { this.text += '1'; }, 10);
}
}
@Component(
{selector: 'nested-async-timeout-comp', template: `<span (click)='click()'>{{text}}</span>`})
class NestedAsyncTimeoutComp {
text: string = '1';
click() {
setTimeout(() => { setTimeout(() => { this.text += '1'; }, 10); }, 10);
}
}
export function main() {
describe('ComponentFixture', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AutoDetectComp, AsyncComp, AsyncTimeoutComp, NestedAsyncTimeoutComp, AsyncChangeComp,
MyIfComp, SimpleComp, AsyncChildComp
]
});
}));
it('should auto detect changes if autoDetectChanges is called', () => {
const componentFixture = TestBed.createComponent(AutoDetectComp);
expect(componentFixture.ngZone).not.toBeNull();
componentFixture.autoDetectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.isStable()).toBe(true);
expect(componentFixture.nativeElement).toHaveText('11');
});
it('should auto detect changes if ComponentFixtureAutoDetect is provided as true',
withModule({providers: [{provide: ComponentFixtureAutoDetect, useValue: true}]}, () => {
const componentFixture = TestBed.createComponent(AutoDetectComp);
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('11');
}));
it('should signal through whenStable when the fixture is stable (autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(AsyncComp);
componentFixture.autoDetectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become stable
// before checking for new value.
expect(componentFixture.isStable()).toBe(false);
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should signal through isStable when the fixture is stable (no autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(AsyncComp);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become stable
// before checking.
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should wait for macroTask(setTimeout) while checking for whenStable ' +
'(autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(AsyncTimeoutComp);
componentFixture.autoDetectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become
// stable before checking for new value.
expect(componentFixture.isStable()).toBe(false);
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should wait for macroTask(setTimeout) while checking for whenStable ' +
'(no autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(AsyncTimeoutComp);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become
// stable before checking for new value.
expect(componentFixture.isStable()).toBe(false);
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should wait for nested macroTasks(setTimeout) while checking for whenStable ' +
'(autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(NestedAsyncTimeoutComp);
componentFixture.autoDetectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become
// stable before checking for new value.
expect(componentFixture.isStable()).toBe(false);
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should wait for nested macroTasks(setTimeout) while checking for whenStable ' +
'(no autoDetectChanges)',
async(() => {
const componentFixture = TestBed.createComponent(NestedAsyncTimeoutComp);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
expect(componentFixture.nativeElement).toHaveText('1');
// Component is updated asynchronously. Wait for the fixture to become
// stable before checking for new value.
expect(componentFixture.isStable()).toBe(false);
componentFixture.whenStable().then((waited) => {
expect(waited).toBe(true);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('11');
});
}));
it('should stabilize after async task in change detection (autoDetectChanges)', async(() => {
const componentFixture = TestBed.createComponent(AsyncChangeComp);
componentFixture.autoDetectChanges();
componentFixture.whenStable().then((_) => {
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
componentFixture.whenStable().then(
(_) => { expect(componentFixture.nativeElement).toHaveText('11'); });
});
}));
it('should stabilize after async task in change detection(no autoDetectChanges)', async(() => {
const componentFixture = TestBed.createComponent(AsyncChangeComp);
componentFixture.detectChanges();
componentFixture.whenStable().then((_) => {
// Run detectChanges again so that stabilized value is reflected in the
// DOM.
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('1');
const element = componentFixture.debugElement.children[0];
dispatchEvent(element.nativeElement, 'click');
componentFixture.detectChanges();
componentFixture.whenStable().then((_) => {
// Run detectChanges again so that stabilized value is reflected in
// the DOM.
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('11');
});
});
}));
describe('No NgZone', () => {
beforeEach(() => {
TestBed.configureTestingModule(
{providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]});
});
it('calling autoDetectChanges raises an error', () => {
const componentFixture = TestBed.createComponent(SimpleComp);
expect(() => {
componentFixture.autoDetectChanges();
}).toThrowError(/Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set/);
});
it('should instantiate a component with valid DOM', async(() => {
const componentFixture = TestBed.createComponent(SimpleComp);
expect(componentFixture.ngZone).toBeNull();
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('Original Simple');
}));
it('should allow changing members of the component', async(() => {
const componentFixture = TestBed.createComponent(MyIfComp);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('MyIf()');
componentFixture.componentInstance.showMore = true;
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('MyIf(More)');
}));
});
});
}

View File

@ -0,0 +1,357 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {EventEmitter, Injectable, NO_ERRORS_SCHEMA} from '@angular/core';
import {Component, Directive, Input} from '@angular/core/src/metadata';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
@Injectable()
class Logger {
logs: string[];
constructor() { this.logs = []; }
add(thing: string) { this.logs.push(thing); }
}
@Directive({selector: '[message]', inputs: ['message']})
class MessageDir {
logger: Logger;
constructor(logger: Logger) { this.logger = logger; }
set message(newMessage: string) { this.logger.add(newMessage); }
}
@Component({
selector: 'child-comp',
template: `<div class="child" message="child">
<span class="childnested" message="nestedchild">Child</span>
</div>
<span class="child" [innerHtml]="childBinding"></span>`,
})
class ChildComp {
childBinding: string;
constructor() { this.childBinding = 'Original'; }
}
@Component({
selector: 'parent-comp',
viewProviders: [Logger],
template: `<div class="parent" message="parent">
<span class="parentnested" message="nestedparent">Parent</span>
</div>
<span class="parent" [innerHtml]="parentBinding"></span>
<child-comp class="child-comp-class"></child-comp>`,
})
class ParentComp {
parentBinding: string;
constructor() { this.parentBinding = 'OriginalParent'; }
}
@Directive({selector: 'custom-emitter', outputs: ['myevent']})
class CustomEmitter {
myevent: EventEmitter<any>;
constructor() { this.myevent = new EventEmitter(); }
}
@Component({
selector: 'events-comp',
template: `<button (click)="handleClick()"></button>
<custom-emitter (myevent)="handleCustom()"></custom-emitter>`,
})
class EventsComp {
clicked: boolean;
customed: boolean;
constructor() {
this.clicked = false;
this.customed = false;
}
handleClick() { this.clicked = true; }
handleCustom() { this.customed = true; }
}
@Component({
selector: 'cond-content-comp',
viewProviders: [Logger],
template: `<div class="child" message="child" *ngIf="myBool"><ng-content></ng-content></div>`,
})
class ConditionalContentComp {
myBool: boolean = false;
}
@Component({
selector: 'conditional-parent-comp',
viewProviders: [Logger],
template: `<span class="parent" [innerHtml]="parentBinding"></span>
<cond-content-comp class="cond-content-comp-class">
<span class="from-parent"></span>
</cond-content-comp>`,
})
class ConditionalParentComp {
parentBinding: string;
constructor() { this.parentBinding = 'OriginalParent'; }
}
@Component({
selector: 'using-for',
viewProviders: [Logger],
template: `<span *ngFor="let thing of stuff" [innerHtml]="thing"></span>
<ul message="list">
<li *ngFor="let item of stuff" [innerHtml]="item"></li>
</ul>`,
})
class UsingFor {
stuff: string[];
constructor() { this.stuff = ['one', 'two', 'three']; }
}
@Directive({selector: '[mydir]', exportAs: 'mydir'})
class MyDir {
}
@Component({
selector: 'locals-comp',
template: `
<div mydir #alice="mydir"></div>
`,
})
class LocalsComp {
}
@Component({
selector: 'bank-account',
template: `
Bank Name: {{bank}}
Account Id: {{id}}
`
})
class BankAccount {
@Input() bank: string;
@Input('account') id: string;
normalizedBankName: string;
}
@Component({
selector: 'test-app',
template: `
<bank-account bank="RBC"
account="4747"
[style.width.px]="width"
[style.color]="color"
[class.closed]="isClosed"
[class.open]="!isClosed"></bank-account>
`,
})
class TestApp {
width = 200;
color = 'red';
isClosed = true;
}
export function main() {
describe('debug element', () => {
let fixture: ComponentFixture<any>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ChildComp,
ConditionalContentComp,
ConditionalParentComp,
CustomEmitter,
EventsComp,
LocalsComp,
MessageDir,
MyDir,
ParentComp,
TestApp,
UsingFor,
],
providers: [Logger],
schemas: [NO_ERRORS_SCHEMA],
});
}));
it('should list all child nodes', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
// The root component has 3 elements and 2 text node children.
expect(fixture.debugElement.childNodes.length).toEqual(5);
});
it('should list all component child elements', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
const childEls = fixture.debugElement.children;
// The root component has 3 elements in its view.
expect(childEls.length).toEqual(3);
expect(getDOM().hasClass(childEls[0].nativeElement, 'parent')).toBe(true);
expect(getDOM().hasClass(childEls[1].nativeElement, 'parent')).toBe(true);
expect(getDOM().hasClass(childEls[2].nativeElement, 'child-comp-class')).toBe(true);
const nested = childEls[0].children;
expect(nested.length).toEqual(1);
expect(getDOM().hasClass(nested[0].nativeElement, 'parentnested')).toBe(true);
const childComponent = childEls[2];
const childCompChildren = childComponent.children;
expect(childCompChildren.length).toEqual(2);
expect(getDOM().hasClass(childCompChildren[0].nativeElement, 'child')).toBe(true);
expect(getDOM().hasClass(childCompChildren[1].nativeElement, 'child')).toBe(true);
const childNested = childCompChildren[0].children;
expect(childNested.length).toEqual(1);
expect(getDOM().hasClass(childNested[0].nativeElement, 'childnested')).toBe(true);
});
it('should list conditional component child elements', () => {
fixture = TestBed.createComponent(ConditionalParentComp);
fixture.detectChanges();
const childEls = fixture.debugElement.children;
// The root component has 2 elements in its view.
expect(childEls.length).toEqual(2);
expect(getDOM().hasClass(childEls[0].nativeElement, 'parent')).toBe(true);
expect(getDOM().hasClass(childEls[1].nativeElement, 'cond-content-comp-class')).toBe(true);
const conditionalContentComp = childEls[1];
expect(conditionalContentComp.children.length).toEqual(0);
conditionalContentComp.componentInstance.myBool = true;
fixture.detectChanges();
expect(conditionalContentComp.children.length).toEqual(1);
});
it('should list child elements within viewports', () => {
fixture = TestBed.createComponent(UsingFor);
fixture.detectChanges();
const childEls = fixture.debugElement.children;
expect(childEls.length).toEqual(4);
// The 4th child is the <ul>
const list = childEls[3];
expect(list.children.length).toEqual(3);
});
it('should list element attributes', () => {
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
const bankElem = fixture.debugElement.children[0];
expect(bankElem.attributes['bank']).toEqual('RBC');
expect(bankElem.attributes['account']).toEqual('4747');
});
it('should list element classes', () => {
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
const bankElem = fixture.debugElement.children[0];
expect(bankElem.classes['closed']).toBe(true);
expect(bankElem.classes['open']).toBe(false);
});
it('should list element styles', () => {
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
const bankElem = fixture.debugElement.children[0];
expect(bankElem.styles['width']).toEqual('200px');
expect(bankElem.styles['color']).toEqual('red');
});
it('should query child elements by css', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
const childTestEls = fixture.debugElement.queryAll(By.css('child-comp'));
expect(childTestEls.length).toBe(1);
expect(getDOM().hasClass(childTestEls[0].nativeElement, 'child-comp-class')).toBe(true);
});
it('should query child elements by directive', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
const childTestEls = fixture.debugElement.queryAll(By.directive(MessageDir));
expect(childTestEls.length).toBe(4);
expect(getDOM().hasClass(childTestEls[0].nativeElement, 'parent')).toBe(true);
expect(getDOM().hasClass(childTestEls[1].nativeElement, 'parentnested')).toBe(true);
expect(getDOM().hasClass(childTestEls[2].nativeElement, 'child')).toBe(true);
expect(getDOM().hasClass(childTestEls[3].nativeElement, 'childnested')).toBe(true);
});
it('should list providerTokens', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
expect(fixture.debugElement.providerTokens).toContain(Logger);
});
it('should list locals', () => {
fixture = TestBed.createComponent(LocalsComp);
fixture.detectChanges();
expect(fixture.debugElement.children[0].references['alice']).toBeAnInstanceOf(MyDir);
});
it('should allow injecting from the element injector', () => {
fixture = TestBed.createComponent(ParentComp);
fixture.detectChanges();
expect((<Logger>(fixture.debugElement.children[0].injector.get(Logger))).logs).toEqual([
'parent', 'nestedparent', 'child', 'nestedchild'
]);
});
it('should list event listeners', () => {
fixture = TestBed.createComponent(EventsComp);
fixture.detectChanges();
expect(fixture.debugElement.children[0].listeners.length).toEqual(1);
expect(fixture.debugElement.children[1].listeners.length).toEqual(1);
});
it('should trigger event handlers', () => {
fixture = TestBed.createComponent(EventsComp);
fixture.detectChanges();
expect(fixture.componentInstance.clicked).toBe(false);
expect(fixture.componentInstance.customed).toBe(false);
fixture.debugElement.children[0].triggerEventHandler('click', <Event>{});
expect(fixture.componentInstance.clicked).toBe(true);
fixture.debugElement.children[1].triggerEventHandler('myevent', <Event>{});
expect(fixture.componentInstance.customed).toBe(true);
});
});
}

View File

@ -0,0 +1,15 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '@angular/core';
export function main() {
describe('dev mode', () => {
it('is enabled in our tests by default', () => { expect(isDevMode()).toBe(true); });
});
}

View File

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '@angular/core';
import {forwardRef, resolveForwardRef} from '@angular/core/src/di';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('forwardRef', function() {
it('should wrap and unwrap the reference', () => {
const ref = forwardRef(() => String);
expect(ref instanceof Type).toBe(true);
expect(resolveForwardRef(ref)).toBe(String);
});
});
}

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('Injector.NULL', () => {
it('should throw if no arg is given', () => {
expect(() => Injector.NULL.get('someToken')).toThrowError('No provider for someToken!');
});
it('should throw if THROW_IF_NOT_FOUND is given', () => {
expect(() => Injector.NULL.get('someToken', Injector.THROW_IF_NOT_FOUND))
.toThrowError('No provider for someToken!');
});
it('should return the default value',
() => { expect(Injector.NULL.get('someToken', 'notFound')).toEqual('notFound'); });
});
}

View File

@ -0,0 +1,529 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, Injectable, InjectionToken, Injector, Optional, Provider, ReflectiveInjector, ReflectiveKey, Self, forwardRef} from '@angular/core';
import {ReflectiveInjector_} from '@angular/core/src/di/reflective_injector';
import {ResolvedReflectiveProvider_} from '@angular/core/src/di/reflective_provider';
import {getOriginalError} from '@angular/core/src/errors';
import {expect} from '@angular/platform-browser/testing/matchers';
import {stringify} from '../../src/util';
class Engine {}
class BrokenEngine {
constructor() { throw new Error('Broken Engine'); }
}
class DashboardSoftware {}
@Injectable()
class Dashboard {
constructor(software: DashboardSoftware) {}
}
class TurboEngine extends Engine {}
@Injectable()
class Car {
constructor(public engine: Engine) {}
}
@Injectable()
class CarWithOptionalEngine {
constructor(@Optional() public engine: Engine) {}
}
@Injectable()
class CarWithDashboard {
engine: Engine;
dashboard: Dashboard;
constructor(engine: Engine, dashboard: Dashboard) {
this.engine = engine;
this.dashboard = dashboard;
}
}
@Injectable()
class SportsCar extends Car {
}
@Injectable()
class CarWithInject {
constructor(@Inject(TurboEngine) public engine: Engine) {}
}
@Injectable()
class CyclicEngine {
constructor(car: Car) {}
}
class NoAnnotations {
constructor(secretDependency: any) {}
}
function factoryFn(a: any) {}
export function main() {
const dynamicProviders = [
{provide: 'provider0', useValue: 1}, {provide: 'provider1', useValue: 1},
{provide: 'provider2', useValue: 1}, {provide: 'provider3', useValue: 1},
{provide: 'provider4', useValue: 1}, {provide: 'provider5', useValue: 1},
{provide: 'provider6', useValue: 1}, {provide: 'provider7', useValue: 1},
{provide: 'provider8', useValue: 1}, {provide: 'provider9', useValue: 1},
{provide: 'provider10', useValue: 1}
];
function createInjector(
providers: Provider[], parent: ReflectiveInjector = null): ReflectiveInjector_ {
const resolvedProviders = ReflectiveInjector.resolve(providers.concat(dynamicProviders));
if (parent != null) {
return <ReflectiveInjector_>parent.createChildFromResolved(resolvedProviders);
} else {
return <ReflectiveInjector_>ReflectiveInjector.fromResolvedProviders(resolvedProviders);
}
}
describe(`injector`, () => {
it('should instantiate a class without dependencies', () => {
const injector = createInjector([Engine]);
const engine = injector.get(Engine);
expect(engine).toBeAnInstanceOf(Engine);
});
it('should resolve dependencies based on type information', () => {
const injector = createInjector([Engine, Car]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(Car);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should resolve dependencies based on @Inject annotation', () => {
const injector = createInjector([TurboEngine, Engine, CarWithInject]);
const car = injector.get(CarWithInject);
expect(car).toBeAnInstanceOf(CarWithInject);
expect(car.engine).toBeAnInstanceOf(TurboEngine);
});
it('should throw when no type and not @Inject (class case)', () => {
expect(() => createInjector([NoAnnotations]))
.toThrowError(
'Cannot resolve all parameters for \'NoAnnotations\'(?). ' +
'Make sure that all the parameters are decorated with Inject or have valid type annotations ' +
'and that \'NoAnnotations\' is decorated with Injectable.');
});
it('should throw when no type and not @Inject (factory case)', () => {
expect(() => createInjector([{provide: 'someToken', useFactory: factoryFn}]))
.toThrowError(
'Cannot resolve all parameters for \'factoryFn\'(?). ' +
'Make sure that all the parameters are decorated with Inject or have valid type annotations ' +
'and that \'factoryFn\' is decorated with Injectable.');
});
it('should cache instances', () => {
const injector = createInjector([Engine]);
const e1 = injector.get(Engine);
const e2 = injector.get(Engine);
expect(e1).toBe(e2);
});
it('should provide to a value', () => {
const injector = createInjector([{provide: Engine, useValue: 'fake engine'}]);
const engine = injector.get(Engine);
expect(engine).toEqual('fake engine');
});
it('should inject dependencies instance of InjectionToken', () => {
const TOKEN = new InjectionToken<string>('token');
const injector = createInjector([
{provide: TOKEN, useValue: 'by token'},
{provide: Engine, useFactory: (v: string) => v, deps: [[TOKEN]]},
]);
const engine = injector.get(Engine);
expect(engine).toEqual('by token');
});
it('should provide to a factory', () => {
function sportsCarFactory(e: any) { return new SportsCar(e); }
const injector =
createInjector([Engine, {provide: Car, useFactory: sportsCarFactory, deps: [Engine]}]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should supporting provider to null', () => {
const injector = createInjector([{provide: Engine, useValue: null}]);
const engine = injector.get(Engine);
expect(engine).toBeNull();
});
it('should provide to an alias', () => {
const injector = createInjector([
Engine, {provide: SportsCar, useClass: SportsCar}, {provide: Car, useExisting: SportsCar}
]);
const car = injector.get(Car);
const sportsCar = injector.get(SportsCar);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car).toBe(sportsCar);
});
it('should support multiProviders', () => {
const injector = createInjector([
Engine, {provide: Car, useClass: SportsCar, multi: true},
{provide: Car, useClass: CarWithOptionalEngine, multi: true}
]);
const cars = injector.get(Car);
expect(cars.length).toEqual(2);
expect(cars[0]).toBeAnInstanceOf(SportsCar);
expect(cars[1]).toBeAnInstanceOf(CarWithOptionalEngine);
});
it('should support multiProviders that are created using useExisting', () => {
const injector =
createInjector([Engine, SportsCar, {provide: Car, useExisting: SportsCar, multi: true}]);
const cars = injector.get(Car);
expect(cars.length).toEqual(1);
expect(cars[0]).toBe(injector.get(SportsCar));
});
it('should throw when the aliased provider does not exist', () => {
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
const e = `No provider for ${stringify(SportsCar)}! (car -> ${stringify(SportsCar)})`;
expect(() => injector.get('car')).toThrowError(e);
});
it('should handle forwardRef in useExisting', () => {
const injector = createInjector([
{provide: 'originalEngine', useClass: forwardRef(() => Engine)},
{provide: 'aliasedEngine', useExisting: <any>forwardRef(() => 'originalEngine')}
]);
expect(injector.get('aliasedEngine')).toBeAnInstanceOf(Engine);
});
it('should support overriding factory dependencies', () => {
const injector = createInjector(
[Engine, {provide: Car, useFactory: (e: Engine) => new SportsCar(e), deps: [Engine]}]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should support optional dependencies', () => {
const injector = createInjector([CarWithOptionalEngine]);
const car = injector.get(CarWithOptionalEngine);
expect(car.engine).toEqual(null);
});
it('should flatten passed-in providers', () => {
const injector = createInjector([[[Engine, Car]]]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(Car);
});
it('should use the last provider when there are multiple providers for same token', () => {
const injector = createInjector(
[{provide: Engine, useClass: Engine}, {provide: Engine, useClass: TurboEngine}]);
expect(injector.get(Engine)).toBeAnInstanceOf(TurboEngine);
});
it('should use non-type tokens', () => {
const injector = createInjector([{provide: 'token', useValue: 'value'}]);
expect(injector.get('token')).toEqual('value');
});
it('should throw when given invalid providers', () => {
expect(() => createInjector(<any>['blah']))
.toThrowError(
'Invalid provider - only instances of Provider and Type are allowed, got: blah');
});
it('should provide itself', () => {
const parent = createInjector([]);
const child = parent.resolveAndCreateChild([]);
expect(child.get(Injector)).toBe(child);
});
it('should throw when no provider defined', () => {
const injector = createInjector([]);
expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!');
});
it('should show the full path when no provider', () => {
const injector = createInjector([CarWithDashboard, Engine, Dashboard]);
expect(() => injector.get(CarWithDashboard))
.toThrowError(
`No provider for DashboardSoftware! (${stringify(CarWithDashboard)} -> ${stringify(Dashboard)} -> DashboardSoftware)`);
});
it('should throw when trying to instantiate a cyclic dependency', () => {
const injector = createInjector([Car, {provide: Engine, useClass: CyclicEngine}]);
expect(() => injector.get(Car))
.toThrowError(
`Cannot instantiate cyclic dependency! (${stringify(Car)} -> ${stringify(Engine)} -> ${stringify(Car)})`);
});
it('should show the full path when error happens in a constructor', () => {
const providers =
ReflectiveInjector.resolve([Car, {provide: Engine, useClass: BrokenEngine}]);
const injector = new ReflectiveInjector_(providers);
try {
injector.get(Car);
throw 'Must throw';
} catch (e) {
expect(e.message).toContain(
`Error during instantiation of Engine! (${stringify(Car)} -> Engine)`);
expect(getOriginalError(e) instanceof Error).toBeTruthy();
expect(e.keys[0].token).toEqual(Engine);
}
});
it('should instantiate an object after a failed attempt', () => {
let isBroken = true;
const injector = createInjector([
Car, {provide: Engine, useFactory: (() => isBroken ? new BrokenEngine() : new Engine())}
]);
expect(() => injector.get(Car))
.toThrowError('Broken Engine: Error during instantiation of Engine! (Car -> Engine).');
isBroken = false;
expect(injector.get(Car)).toBeAnInstanceOf(Car);
});
it('should support null values', () => {
const injector = createInjector([{provide: 'null', useValue: null}]);
expect(injector.get('null')).toBe(null);
});
});
describe('child', () => {
it('should load instances from parent injector', () => {
const parent = ReflectiveInjector.resolveAndCreate([Engine]);
const child = parent.resolveAndCreateChild([]);
const engineFromParent = parent.get(Engine);
const engineFromChild = child.get(Engine);
expect(engineFromChild).toBe(engineFromParent);
});
it('should not use the child providers when resolving the dependencies of a parent provider',
() => {
const parent = ReflectiveInjector.resolveAndCreate([Car, Engine]);
const child = parent.resolveAndCreateChild([{provide: Engine, useClass: TurboEngine}]);
const carFromChild = child.get(Car);
expect(carFromChild.engine).toBeAnInstanceOf(Engine);
});
it('should create new instance in a child injector', () => {
const parent = ReflectiveInjector.resolveAndCreate([Engine]);
const child = parent.resolveAndCreateChild([{provide: Engine, useClass: TurboEngine}]);
const engineFromParent = parent.get(Engine);
const engineFromChild = child.get(Engine);
expect(engineFromParent).not.toBe(engineFromChild);
expect(engineFromChild).toBeAnInstanceOf(TurboEngine);
});
it('should give access to parent', () => {
const parent = ReflectiveInjector.resolveAndCreate([]);
const child = parent.resolveAndCreateChild([]);
expect(child.parent).toBe(parent);
});
});
describe('resolveAndInstantiate', () => {
it('should instantiate an object in the context of the injector', () => {
const inj = ReflectiveInjector.resolveAndCreate([Engine]);
const car = inj.resolveAndInstantiate(Car);
expect(car).toBeAnInstanceOf(Car);
expect(car.engine).toBe(inj.get(Engine));
});
it('should not store the instantiated object in the injector', () => {
const inj = ReflectiveInjector.resolveAndCreate([Engine]);
inj.resolveAndInstantiate(Car);
expect(() => inj.get(Car)).toThrowError();
});
});
describe('instantiate', () => {
it('should instantiate an object in the context of the injector', () => {
const inj = ReflectiveInjector.resolveAndCreate([Engine]);
const car = inj.instantiateResolved(ReflectiveInjector.resolve([Car])[0]);
expect(car).toBeAnInstanceOf(Car);
expect(car.engine).toBe(inj.get(Engine));
});
});
describe('depedency resolution', () => {
describe('@Self()', () => {
it('should return a dependency from self', () => {
const inj = ReflectiveInjector.resolveAndCreate([
Engine,
{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]}
]);
expect(inj.get(Car)).toBeAnInstanceOf(Car);
});
it('should throw when not requested provider on self', () => {
const parent = ReflectiveInjector.resolveAndCreate([Engine]);
const child = parent.resolveAndCreateChild(
[{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]}]);
expect(() => child.get(Car))
.toThrowError(`No provider for Engine! (${stringify(Car)} -> ${stringify(Engine)})`);
});
});
describe('default', () => {
it('should not skip self', () => {
const parent = ReflectiveInjector.resolveAndCreate([Engine]);
const child = parent.resolveAndCreateChild([
{provide: Engine, useClass: TurboEngine},
{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [Engine]}
]);
expect(child.get(Car).engine).toBeAnInstanceOf(TurboEngine);
});
});
});
describe('resolve', () => {
it('should resolve and flatten', () => {
const providers = ReflectiveInjector.resolve([Engine, [BrokenEngine]]);
providers.forEach(function(b) {
if (!b) return; // the result is a sparse array
expect(b instanceof ResolvedReflectiveProvider_).toBe(true);
});
});
it('should support multi providers', () => {
const provider = ReflectiveInjector.resolve([
{provide: Engine, useClass: BrokenEngine, multi: true},
{provide: Engine, useClass: TurboEngine, multi: true}
])[0];
expect(provider.key.token).toBe(Engine);
expect(provider.multiProvider).toEqual(true);
expect(provider.resolvedFactories.length).toEqual(2);
});
it('should support providers as hash', () => {
const provider = ReflectiveInjector.resolve([
{provide: Engine, useClass: BrokenEngine, multi: true},
{provide: Engine, useClass: TurboEngine, multi: true}
])[0];
expect(provider.key.token).toBe(Engine);
expect(provider.multiProvider).toEqual(true);
expect(provider.resolvedFactories.length).toEqual(2);
});
it('should support multi providers with only one provider', () => {
const provider =
ReflectiveInjector.resolve([{provide: Engine, useClass: BrokenEngine, multi: true}])[0];
expect(provider.key.token).toBe(Engine);
expect(provider.multiProvider).toEqual(true);
expect(provider.resolvedFactories.length).toEqual(1);
});
it('should throw when mixing multi providers with regular providers', () => {
expect(() => {
ReflectiveInjector.resolve(
[{provide: Engine, useClass: BrokenEngine, multi: true}, Engine]);
}).toThrowError(/Cannot mix multi providers and regular providers/);
expect(() => {
ReflectiveInjector.resolve(
[Engine, {provide: Engine, useClass: BrokenEngine, multi: true}]);
}).toThrowError(/Cannot mix multi providers and regular providers/);
});
it('should resolve forward references', () => {
const providers = ReflectiveInjector.resolve([
forwardRef(() => Engine),
[{provide: forwardRef(() => BrokenEngine), useClass: forwardRef(() => Engine)}], {
provide: forwardRef(() => String),
useFactory: () => 'OK',
deps: [forwardRef(() => Engine)]
}
]);
const engineProvider = providers[0];
const brokenEngineProvider = providers[1];
const stringProvider = providers[2];
expect(engineProvider.resolvedFactories[0].factory() instanceof Engine).toBe(true);
expect(brokenEngineProvider.resolvedFactories[0].factory() instanceof Engine).toBe(true);
expect(stringProvider.resolvedFactories[0].dependencies[0].key)
.toEqual(ReflectiveKey.get(Engine));
});
it('should support overriding factory dependencies with dependency annotations', () => {
const providers = ReflectiveInjector.resolve([{
provide: 'token',
useFactory: (e: any /** TODO #9100 */) => 'result',
deps: [[new Inject('dep')]]
}]);
const provider = providers[0];
expect(provider.resolvedFactories[0].dependencies[0].key.token).toEqual('dep');
});
it('should allow declaring dependencies with flat arrays', () => {
const resolved = ReflectiveInjector.resolve(
[{provide: 'token', useFactory: (e: any) => e, deps: [new Inject('dep')]}]);
const nestedResolved = ReflectiveInjector.resolve(
[{provide: 'token', useFactory: (e: any) => e, deps: [[new Inject('dep')]]}]);
expect(resolved[0].resolvedFactories[0].dependencies[0].key.token)
.toEqual(nestedResolved[0].resolvedFactories[0].dependencies[0].key.token);
});
});
describe('displayName', () => {
it('should work', () => {
expect((<ReflectiveInjector_>ReflectiveInjector.resolveAndCreate([Engine, BrokenEngine]))
.displayName)
.toEqual('ReflectiveInjector(providers: [ "Engine" , "BrokenEngine" ])');
});
});
}

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {KeyRegistry} from '@angular/core/src/di/reflective_key';
export function main() {
describe('key', function() {
let registry: KeyRegistry;
beforeEach(function() { registry = new KeyRegistry(); });
it('should be equal to another key if type is the same',
function() { expect(registry.get('car')).toBe(registry.get('car')); });
it('should not be equal to another key if types are different',
function() { expect(registry.get('car')).not.toBe(registry.get('porsche')); });
it('should return the passed in key',
function() { expect(registry.get(registry.get('car'))).toBe(registry.get('car')); });
});
}

View File

@ -0,0 +1,86 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnInit} from '@angular/core';
import {Component, Directive} from '@angular/core/src/metadata';
import {TestBed, inject} from '@angular/core/testing';
import {Log} from '@angular/core/testing/testing_internal';
export function main() {
describe('directive lifecycle integration spec', () => {
let log: Log;
beforeEach(() => {
TestBed
.configureTestingModule({
declarations: [
LifecycleCmp,
LifecycleDir,
MyComp5,
],
providers: [Log]
})
.overrideComponent(MyComp5, {set: {template: '<div [field]="123" lifecycle></div>'}});
});
beforeEach(inject([Log], (_log: any) => { log = _log; }));
it('should invoke lifecycle methods ngOnChanges > ngOnInit > ngDoCheck > ngAfterContentChecked',
() => {
const fixture = TestBed.createComponent(MyComp5);
fixture.detectChanges();
expect(log.result())
.toEqual(
'ngOnChanges; ngOnInit; ngDoCheck; ngAfterContentInit; ngAfterContentChecked; child_ngDoCheck; ' +
'ngAfterViewInit; ngAfterViewChecked');
log.clear();
fixture.detectChanges();
expect(log.result())
.toEqual('ngDoCheck; ngAfterContentChecked; child_ngDoCheck; ngAfterViewChecked');
});
});
}
@Directive({selector: '[lifecycle-dir]'})
class LifecycleDir implements DoCheck {
constructor(private _log: Log) {}
ngDoCheck() { this._log.add('child_ngDoCheck'); }
}
@Component({
selector: '[lifecycle]',
inputs: ['field'],
template: `<div lifecycle-dir></div>`,
})
class LifecycleCmp implements OnChanges,
OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked {
field: any /** TODO #9100 */;
constructor(private _log: Log) {}
ngOnChanges(_: any /** TODO #9100 */) { this._log.add('ngOnChanges'); }
ngOnInit() { this._log.add('ngOnInit'); }
ngDoCheck() { this._log.add('ngDoCheck'); }
ngAfterContentInit() { this._log.add('ngAfterContentInit'); }
ngAfterContentChecked() { this._log.add('ngAfterContentChecked'); }
ngAfterViewInit() { this._log.add('ngAfterViewInit'); }
ngAfterViewChecked() { this._log.add('ngAfterViewChecked'); }
}
@Component({selector: 'my-comp'})
class MyComp5 {
}

View File

@ -0,0 +1,108 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el, stringifyElement} from '@angular/platform-browser/testing/browser_util';
export function main() {
describe('dom adapter', () => {
let defaultDoc: any;
beforeEach(() => {
defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
});
it('should not coalesque text nodes', () => {
const el1 = el('<div>a</div>');
const el2 = el('<div>b</div>');
getDOM().appendChild(el2, getDOM().firstChild(el1));
expect(getDOM().childNodes(el2).length).toBe(2);
const el2Clone = getDOM().clone(el2);
expect(getDOM().childNodes(el2Clone).length).toBe(2);
});
it('should clone correctly', () => {
const el1 = el('<div x="y">a<span>b</span></div>');
const clone = getDOM().clone(el1);
expect(clone).not.toBe(el1);
getDOM().setAttribute(clone, 'test', '1');
expect(stringifyElement(clone)).toEqual('<div test="1" x="y">a<span>b</span></div>');
expect(getDOM().getAttribute(el1, 'test')).toBeFalsy();
const cNodes = getDOM().childNodes(clone);
const firstChild = cNodes[0];
const secondChild = cNodes[1];
expect(getDOM().parentElement(firstChild)).toBe(clone);
expect(getDOM().nextSibling(firstChild)).toBe(secondChild);
expect(getDOM().isTextNode(firstChild)).toBe(true);
expect(getDOM().parentElement(secondChild)).toBe(clone);
expect(getDOM().nextSibling(secondChild)).toBeFalsy();
expect(getDOM().isElementNode(secondChild)).toBe(true);
});
it('should be able to create text nodes and use them with the other APIs', () => {
const t = getDOM().createTextNode('hello');
expect(getDOM().isTextNode(t)).toBe(true);
const d = getDOM().createElement('div');
getDOM().appendChild(d, t);
expect(getDOM().getInnerHTML(d)).toEqual('hello');
});
it('should set className via the class attribute', () => {
const d = getDOM().createElement('div');
getDOM().setAttribute(d, 'class', 'class1');
expect(d.className).toEqual('class1');
});
it('should allow to remove nodes without parents', () => {
const d = getDOM().createElement('div');
expect(() => getDOM().remove(d)).not.toThrow();
});
if (getDOM().supportsDOMEvents()) {
describe('getBaseHref', () => {
beforeEach(() => getDOM().resetBaseElement());
it('should return null if base element is absent',
() => { expect(getDOM().getBaseHref(defaultDoc)).toBeNull(); });
it('should return the value of the base element', () => {
const baseEl = getDOM().createElement('base');
getDOM().setAttribute(baseEl, 'href', '/drop/bass/connon/');
const headEl = defaultDoc.head;
getDOM().appendChild(headEl, baseEl);
const baseHref = getDOM().getBaseHref(defaultDoc);
getDOM().removeChild(headEl, baseEl);
getDOM().resetBaseElement();
expect(baseHref).toEqual('/drop/bass/connon/');
});
it('should return a relative url', () => {
const baseEl = getDOM().createElement('base');
getDOM().setAttribute(baseEl, 'href', 'base');
const headEl = defaultDoc.head;
getDOM().appendChild(headEl, baseEl);
const baseHref = getDOM().getBaseHref(defaultDoc);
getDOM().removeChild(headEl, baseEl);
getDOM().resetBaseElement();
expect(baseHref.endsWith('/base')).toBe(true);
});
});
}
});
}

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('Shim', () => {
it('should provide correct function.name ', () => {
const functionWithoutName = identity(() => function(_: any /** TODO #9100 */) {});
function foo(_: any /** TODO #9100 */){};
expect((<any>functionWithoutName).name).toBeFalsy();
expect((<any>foo).name).toEqual('foo');
});
});
}
function identity(a: any /** TODO #9100 */) {
return a;
}

View File

@ -0,0 +1,106 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ERROR_DEBUG_CONTEXT, ERROR_TYPE} from '@angular/core/src/errors';
import {ErrorHandler, wrappedError} from '../src/error_handler';
class MockConsole {
res: any[] = [];
error(s: any): void { this.res.push(s); }
}
export function main() {
function errorToString(error: any) {
const logger = new MockConsole();
const errorHandler = new ErrorHandler(false);
errorHandler._console = logger as any;
errorHandler.handleError(error);
return logger.res.join('\n');
}
function getStack(error: Error): string {
try {
throw error;
} catch (e) {
return e.stack;
}
}
describe('ErrorHandler', () => {
it('should output exception', () => {
const e = errorToString(new Error('message!'));
expect(e).toContain('message!');
});
it('should output stackTrace', () => {
const error = new Error('message!');
const stack = getStack(error);
if (stack) {
const e = errorToString(error);
expect(e).toContain(stack);
}
});
describe('context', () => {
it('should print nested context', () => {
const cause = new Error('message!');
const stack = getStack(cause);
const context = { source: 'context!', toString() { return 'Context'; } } as any;
const original = viewWrappedError(cause, context);
const e = errorToString(wrappedError('message', original));
expect(e).toEqual(
stack ? `EXCEPTION: message caused by: Error in context! caused by: message!
ORIGINAL EXCEPTION: message!
ORIGINAL STACKTRACE:
${stack}
ERROR CONTEXT:
Context` :
`EXCEPTION: message caused by: Error in context! caused by: message!
ORIGINAL EXCEPTION: message!
ERROR CONTEXT:
Context`);
});
});
describe('original exception', () => {
it('should print original exception message if available (original is Error)', () => {
const realOriginal = new Error('inner');
const original = wrappedError('wrapped', realOriginal);
const e = errorToString(wrappedError('wrappedwrapped', original));
expect(e).toContain('inner');
});
it('should print original exception message if available (original is not Error)', () => {
const realOriginal = new Error('custom');
const original = wrappedError('wrapped', realOriginal);
const e = errorToString(wrappedError('wrappedwrapped', original));
expect(e).toContain('custom');
});
});
describe('original stack', () => {
it('should print original stack if available', () => {
const realOriginal = new Error('inner');
const stack = getStack(realOriginal);
if (stack) {
const original = wrappedError('wrapped', realOriginal);
const e = errorToString(wrappedError('wrappedwrapped', original));
expect(e).toContain(stack);
}
});
});
});
}
function viewWrappedError(originalError: any, context: any): Error {
const error = wrappedError(`Error in ${context.source}`, originalError);
(error as any)[ERROR_DEBUG_CONTEXT] = context;
(error as any)[ERROR_TYPE] = viewWrappedError;
return error;
}

View File

@ -0,0 +1,133 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {EventEmitter} from '../src/event_emitter';
export function main() {
describe('EventEmitter', () => {
let emitter: EventEmitter<any>;
beforeEach(() => { emitter = new EventEmitter(); });
it('should call the next callback',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
emitter.subscribe({
next: (value: any) => {
expect(value).toEqual(99);
async.done();
}
});
emitter.emit(99);
}));
it('should call the throw callback',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
emitter.subscribe({
next: () => {},
error: (error: any) => {
expect(error).toEqual('Boom');
async.done();
}
});
emitter.error('Boom');
}));
it('should work when no throw callback is provided',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
emitter.subscribe({next: () => {}, error: (_: any) => { async.done(); }});
emitter.error('Boom');
}));
it('should call the return callback',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
emitter.subscribe(
{next: () => {}, error: (_: any) => {}, complete: () => { async.done(); }});
emitter.complete();
}));
it('should subscribe to the wrapper synchronously', () => {
let called = false;
emitter.subscribe({next: (value: any) => { called = true; }});
emitter.emit(99);
expect(called).toBe(true);
});
it('delivers next and error events synchronously',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const log: any[] /** TODO #9100 */ = [];
emitter.subscribe({
next: (x: any) => {
log.push(x);
expect(log).toEqual([1, 2]);
},
error: (err: any) => {
log.push(err);
expect(log).toEqual([1, 2, 3, 4]);
async.done();
}
});
log.push(1);
emitter.emit(2);
log.push(3);
emitter.error(4);
log.push(5);
}));
it('delivers next and complete events synchronously', () => {
const log: any[] /** TODO #9100 */ = [];
emitter.subscribe({
next: (x: any) => {
log.push(x);
expect(log).toEqual([1, 2]);
},
error: null,
complete: () => {
log.push(4);
expect(log).toEqual([1, 2, 3, 4]);
}
});
log.push(1);
emitter.emit(2);
log.push(3);
emitter.complete();
log.push(5);
expect(log).toEqual([1, 2, 3, 4, 5]);
});
it('delivers events asynchronously when forced to async mode',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const e = new EventEmitter(true);
const log: any[] /** TODO #9100 */ = [];
e.subscribe((x: any) => {
log.push(x);
expect(log).toEqual([1, 3, 2]);
async.done();
});
log.push(1);
e.emit(2);
log.push(3);
}));
it('reports whether it has subscribers', () => {
const e = new EventEmitter(false);
expect(e.observers.length > 0).toBe(false);
e.subscribe({next: () => {}});
expect(e.observers.length > 0).toBe(true);
});
// TODO: vsavkin: add tests cases
// should call dispose on the subscription if generator returns {done:true}
// should call dispose on the subscription on throw
// should call dispose on the subscription on return
});
}

View File

@ -0,0 +1,318 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {discardPeriodicTasks, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {Log, beforeEach, describe, inject, it} from '@angular/core/testing/testing_internal';
import {expect} from '@angular/platform-browser/testing/matchers';
import {Parser} from '../../compiler/src/expression_parser/parser';
const resolvedPromise = Promise.resolve(null);
const ProxyZoneSpec: {assertPresent: () => void} = (Zone as any)['ProxyZoneSpec'];
export function main() {
describe('fake async', () => {
it('should run synchronous code', () => {
let ran = false;
fakeAsync(() => { ran = true; })();
expect(ran).toEqual(true);
});
it('should pass arguments to the wrapped function', () => {
fakeAsync((foo: any /** TODO #9100 */, bar: any /** TODO #9100 */) => {
expect(foo).toEqual('foo');
expect(bar).toEqual('bar');
})('foo', 'bar');
});
it('should work with inject()', fakeAsync(inject([Parser], (parser: any /** TODO #9100 */) => {
expect(parser).toBeAnInstanceOf(Parser);
})));
it('should throw on nested calls', () => {
expect(() => {
fakeAsync(() => { fakeAsync((): any /** TODO #9100 */ => null)(); })();
}).toThrowError('fakeAsync() calls can not be nested');
});
it('should flush microtasks before returning', () => {
let thenRan = false;
fakeAsync(() => { resolvedPromise.then(_ => { thenRan = true; }); })();
expect(thenRan).toEqual(true);
});
it('should propagate the return value',
() => { expect(fakeAsync(() => 'foo')()).toEqual('foo'); });
describe('Promise', () => {
it('should run asynchronous code', fakeAsync(() => {
let thenRan = false;
resolvedPromise.then((_) => { thenRan = true; });
expect(thenRan).toEqual(false);
flushMicrotasks();
expect(thenRan).toEqual(true);
}));
it('should run chained thens', fakeAsync(() => {
const log = new Log();
resolvedPromise.then((_) => log.add(1)).then((_) => log.add(2));
expect(log.result()).toEqual('');
flushMicrotasks();
expect(log.result()).toEqual('1; 2');
}));
it('should run Promise created in Promise', fakeAsync(() => {
const log = new Log();
resolvedPromise.then((_) => {
log.add(1);
resolvedPromise.then((_) => log.add(2));
});
expect(log.result()).toEqual('');
flushMicrotasks();
expect(log.result()).toEqual('1; 2');
}));
it('should complain if the test throws an exception during async calls', () => {
expect(() => {
fakeAsync(() => {
resolvedPromise.then((_) => { throw new Error('async'); });
flushMicrotasks();
})();
}).toThrowError(/Uncaught \(in promise\): Error: async/);
});
it('should complain if a test throws an exception', () => {
expect(() => { fakeAsync(() => { throw new Error('sync'); })(); }).toThrowError('sync');
});
});
describe('timers', () => {
it('should run queued zero duration timer on zero tick', fakeAsync(() => {
let ran = false;
setTimeout(() => { ran = true; }, 0);
expect(ran).toEqual(false);
tick();
expect(ran).toEqual(true);
}));
it('should run queued timer after sufficient clock ticks', fakeAsync(() => {
let ran = false;
setTimeout(() => { ran = true; }, 10);
tick(6);
expect(ran).toEqual(false);
tick(6);
expect(ran).toEqual(true);
}));
it('should run queued timer only once', fakeAsync(() => {
let cycles = 0;
setTimeout(() => { cycles++; }, 10);
tick(10);
expect(cycles).toEqual(1);
tick(10);
expect(cycles).toEqual(1);
tick(10);
expect(cycles).toEqual(1);
}));
it('should not run cancelled timer', fakeAsync(() => {
let ran = false;
const id = setTimeout(() => { ran = true; }, 10);
clearTimeout(id);
tick(10);
expect(ran).toEqual(false);
}));
it('should throw an error on dangling timers', () => {
expect(() => {
fakeAsync(() => { setTimeout(() => {}, 10); })();
}).toThrowError('1 timer(s) still in the queue.');
});
it('should throw an error on dangling periodic timers', () => {
expect(() => {
fakeAsync(() => { setInterval(() => {}, 10); })();
}).toThrowError('1 periodic timer(s) still in the queue.');
});
it('should run periodic timers', fakeAsync(() => {
let cycles = 0;
const id = setInterval(() => { cycles++; }, 10);
tick(10);
expect(cycles).toEqual(1);
tick(10);
expect(cycles).toEqual(2);
tick(10);
expect(cycles).toEqual(3);
clearInterval(id);
}));
it('should not run cancelled periodic timer', fakeAsync(() => {
let ran = false;
const id = setInterval(() => { ran = true; }, 10);
clearInterval(id);
tick(10);
expect(ran).toEqual(false);
}));
it('should be able to cancel periodic timers from a callback', fakeAsync(() => {
let cycles = 0;
let id: any /** TODO #9100 */;
id = setInterval(() => {
cycles++;
clearInterval(id);
}, 10);
tick(10);
expect(cycles).toEqual(1);
tick(10);
expect(cycles).toEqual(1);
}));
it('should clear periodic timers', fakeAsync(() => {
let cycles = 0;
const id = setInterval(() => { cycles++; }, 10);
tick(10);
expect(cycles).toEqual(1);
discardPeriodicTasks();
// Tick once to clear out the timer which already started.
tick(10);
expect(cycles).toEqual(2);
tick(10);
// Nothing should change
expect(cycles).toEqual(2);
}));
it('should process microtasks before timers', fakeAsync(() => {
const log = new Log();
resolvedPromise.then((_) => log.add('microtask'));
setTimeout(() => log.add('timer'), 9);
const id = setInterval(() => log.add('periodic timer'), 10);
expect(log.result()).toEqual('');
tick(10);
expect(log.result()).toEqual('microtask; timer; periodic timer');
clearInterval(id);
}));
it('should process micro-tasks created in timers before next timers', fakeAsync(() => {
const log = new Log();
resolvedPromise.then((_) => log.add('microtask'));
setTimeout(() => {
log.add('timer');
resolvedPromise.then((_) => log.add('t microtask'));
}, 9);
const id = setInterval(() => {
log.add('periodic timer');
resolvedPromise.then((_) => log.add('pt microtask'));
}, 10);
tick(10);
expect(log.result())
.toEqual('microtask; timer; t microtask; periodic timer; pt microtask');
tick(10);
expect(log.result())
.toEqual(
'microtask; timer; t microtask; periodic timer; pt microtask; periodic timer; pt microtask');
clearInterval(id);
}));
});
describe('outside of the fakeAsync zone', () => {
it('calling flushMicrotasks should throw', () => {
expect(() => {
flushMicrotasks();
}).toThrowError('The code should be running in the fakeAsync zone to call this function');
});
it('calling tick should throw', () => {
expect(() => {
tick();
}).toThrowError('The code should be running in the fakeAsync zone to call this function');
});
it('calling discardPeriodicTasks should throw', () => {
expect(() => {
discardPeriodicTasks();
}).toThrowError('The code should be running in the fakeAsync zone to call this function');
});
});
describe('only one `fakeAsync` zone per test', () => {
let zoneInBeforeEach: Zone;
let zoneInTest1: Zone;
beforeEach(fakeAsync(() => { zoneInBeforeEach = Zone.current; }));
it('should use the same zone as in beforeEach', fakeAsync(() => {
zoneInTest1 = Zone.current;
expect(zoneInTest1).toBe(zoneInBeforeEach);
}));
});
});
describe('ProxyZone', () => {
beforeEach(() => { ProxyZoneSpec.assertPresent(); });
afterEach(() => { ProxyZoneSpec.assertPresent(); });
it('should allow fakeAsync zone to retroactively set a zoneSpec outside of fakeAsync', () => {
ProxyZoneSpec.assertPresent();
let state: string = 'not run';
const testZone = Zone.current.fork({name: 'test-zone'});
(fakeAsync(() => {
testZone.run(() => {
Promise.resolve('works').then((v) => state = v);
expect(state).toEqual('not run');
flushMicrotasks();
expect(state).toEqual('works');
});
}))();
expect(state).toEqual('works');
});
});
}

View File

@ -0,0 +1,66 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule} from '@angular/common';
import {Component, ContentChildren, Directive, Inject, NO_ERRORS_SCHEMA, NgModule, QueryList, asNativeElements, forwardRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('forwardRef integration', function() {
beforeEach(() => { TestBed.configureTestingModule({imports: [Module], declarations: [App]}); });
it('should instantiate components which are declared using forwardRef', () => {
const a = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}).createComponent(App);
a.detectChanges();
expect(asNativeElements(a.debugElement.children)).toHaveText('frame(lock)');
expect(TestBed.get(ModuleFrame)).toBeDefined();
});
});
}
@NgModule({
imports: [CommonModule],
providers: [forwardRef(() => ModuleFrame)],
declarations: [forwardRef(() => Door), forwardRef(() => Lock)],
exports: [forwardRef(() => Door), forwardRef(() => Lock)]
})
class Module {
}
@Component({
selector: 'app',
viewProviders: [forwardRef(() => Frame)],
template: `<door><lock></lock></door>`,
})
class App {
}
@Component({
selector: 'lock',
template: `{{frame.name}}(<span *ngFor="let lock of locks">{{lock.name}}</span>)`,
})
class Door {
@ContentChildren(forwardRef(() => Lock)) locks: QueryList<Lock>;
frame: Frame;
constructor(@Inject(forwardRef(() => Frame)) frame: Frame) { this.frame = frame; }
}
class Frame {
name: string = 'frame';
}
class ModuleFrame {
name: string = 'moduleFram';
}
@Directive({selector: 'lock'})
class Lock {
name: string = 'lock';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,118 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ComponentFactoryResolver} from '@angular/core';
import {noComponentFactoryError} from '@angular/core/src/linker/component_factory_resolver';
import {TestBed} from '@angular/core/testing';
import {Console} from '../../src/console';
export function main() {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
class DummyConsole implements Console {
public warnings: string[] = [];
log(message: string) {}
warn(message: string) { this.warnings.push(message); }
}
function declareTests({useJit}: {useJit: boolean}) {
describe('@Component.entryComponents', function() {
let console: DummyConsole;
beforeEach(() => {
console = new DummyConsole();
TestBed.configureCompiler(
{useJit: useJit, providers: [{provide: Console, useValue: console}]});
TestBed.configureTestingModule({declarations: [MainComp, ChildComp, NestedChildComp]});
});
it('should resolve ComponentFactories from the same component', () => {
const compFixture = TestBed.createComponent(MainComp);
const mainComp: MainComp = compFixture.componentInstance;
expect(compFixture.componentRef.injector.get(ComponentFactoryResolver)).toBe(mainComp.cfr);
const cf = mainComp.cfr.resolveComponentFactory(ChildComp);
expect(cf.componentType).toBe(ChildComp);
});
it('should resolve ComponentFactories via ANALYZE_FOR_ENTRY_COMPONENTS', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule(
{declarations: [CompWithAnalyzeEntryComponentsProvider, NestedChildComp, ChildComp]});
const compFixture = TestBed.createComponent(CompWithAnalyzeEntryComponentsProvider);
const mainComp: CompWithAnalyzeEntryComponentsProvider = compFixture.componentInstance;
const cfr: ComponentFactoryResolver =
compFixture.componentRef.injector.get(ComponentFactoryResolver);
expect(cfr.resolveComponentFactory(ChildComp).componentType).toBe(ChildComp);
expect(cfr.resolveComponentFactory(NestedChildComp).componentType).toBe(NestedChildComp);
});
it('should be able to get a component form a parent component (view hiearchy)', () => {
TestBed.overrideComponent(MainComp, {set: {template: '<child></child>'}});
const compFixture = TestBed.createComponent(MainComp);
const childCompEl = compFixture.debugElement.children[0];
const childComp: ChildComp = childCompEl.componentInstance;
// declared on ChildComp directly
expect(childComp.cfr.resolveComponentFactory(NestedChildComp).componentType)
.toBe(NestedChildComp);
// inherited from MainComp
expect(childComp.cfr.resolveComponentFactory(ChildComp).componentType).toBe(ChildComp);
});
it('should not be able to get components from a parent component (content hierarchy)', () => {
TestBed.overrideComponent(MainComp, {set: {template: '<child><nested></nested></child>'}});
TestBed.overrideComponent(ChildComp, {set: {template: '<ng-content></ng-content>'}});
const compFixture = TestBed.createComponent(MainComp);
const nestedChildCompEl = compFixture.debugElement.children[0].children[0];
const nestedChildComp: NestedChildComp = nestedChildCompEl.componentInstance;
expect(nestedChildComp.cfr.resolveComponentFactory(ChildComp).componentType).toBe(ChildComp);
expect(() => nestedChildComp.cfr.resolveComponentFactory(NestedChildComp))
.toThrow(noComponentFactoryError(NestedChildComp));
});
});
}
@Component({selector: 'nested', template: ''})
class NestedChildComp {
constructor(public cfr: ComponentFactoryResolver) {}
}
@Component({selector: 'child', entryComponents: [NestedChildComp], template: ''})
class ChildComp {
constructor(public cfr: ComponentFactoryResolver) {}
}
@Component({
selector: 'main',
entryComponents: [ChildComp],
template: '',
})
class MainComp {
constructor(public cfr: ComponentFactoryResolver) {}
}
@Component({
selector: 'comp-with-analyze',
template: '',
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
multi: true,
useValue: [
{a: 'b', component: ChildComp},
{b: 'c', anotherComponent: NestedChildComp},
]
}]
})
class CompWithAnalyzeEntryComponentsProvider {
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentInit, AfterViewInit, Component, ContentChildren, Directive, Input, QueryList, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
function declareTests({useJit}: {useJit: boolean}) {
describe('<ng-container>', function() {
beforeEach(() => {
TestBed.configureCompiler({useJit: useJit});
TestBed.configureTestingModule({
declarations: [
MyComp,
NeedsContentChildren,
NeedsViewChildren,
TextDirective,
Simple,
],
});
});
it('should support the "i18n" attribute', () => {
const template = '<ng-container i18n>foo</ng-container>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const el = fixture.nativeElement;
expect(el).toHaveText('foo');
});
it('should be rendered as comment with children as siblings', () => {
const template = '<ng-container><p></p></ng-container>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const el = fixture.nativeElement;
const children = getDOM().childNodes(el);
expect(children.length).toBe(2);
expect(getDOM().isCommentNode(children[0])).toBe(true);
expect(getDOM().tagName(children[1]).toUpperCase()).toEqual('P');
});
it('should support nesting', () => {
const template =
'<ng-container>1</ng-container><ng-container><ng-container>2</ng-container></ng-container>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const el = fixture.nativeElement;
const children = getDOM().childNodes(el);
expect(children.length).toBe(5);
expect(getDOM().isCommentNode(children[0])).toBe(true);
expect(children[1]).toHaveText('1');
expect(getDOM().isCommentNode(children[2])).toBe(true);
expect(getDOM().isCommentNode(children[3])).toBe(true);
expect(children[4]).toHaveText('2');
});
it('should group inner nodes', () => {
const template = '<ng-container *ngIf="ctxBoolProp"><p></p><b></b></ng-container>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges();
const el = fixture.nativeElement;
const children = getDOM().childNodes(el);
expect(children.length).toBe(4);
// ngIf anchor
expect(getDOM().isCommentNode(children[0])).toBe(true);
// ng-container anchor
expect(getDOM().isCommentNode(children[1])).toBe(true);
expect(getDOM().tagName(children[2]).toUpperCase()).toEqual('P');
expect(getDOM().tagName(children[3]).toUpperCase()).toEqual('B');
fixture.componentInstance.ctxBoolProp = false;
fixture.detectChanges();
expect(children.length).toBe(1);
expect(getDOM().isCommentNode(children[0])).toBe(true);
});
it('should work with static content projection', () => {
const template = `<simple><ng-container><p>1</p><p>2</p></ng-container></simple>`;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const el = fixture.nativeElement;
expect(el).toHaveText('SIMPLE(12)');
});
it('should support injecting the container from children', () => {
const template = `<ng-container [text]="'container'"><p></p></ng-container>`;
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const dir = fixture.debugElement.children[0].injector.get(TextDirective);
expect(dir).toBeAnInstanceOf(TextDirective);
expect(dir.text).toEqual('container');
});
it('should contain all direct child directives in a <ng-container> (content dom)', () => {
const template =
'<needs-content-children #q><ng-container><div text="foo"></div></ng-container></needs-content-children>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const q = fixture.debugElement.children[0].references['q'];
fixture.detectChanges();
expect(q.textDirChildren.length).toEqual(1);
expect(q.numberOfChildrenAfterContentInit).toEqual(1);
});
it('should contain all child directives in a <ng-container> (view dom)', () => {
const template = '<needs-view-children #q></needs-view-children>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const q = fixture.debugElement.children[0].references['q'];
fixture.detectChanges();
expect(q.textDirChildren.length).toEqual(1);
expect(q.numberOfChildrenAfterViewInit).toEqual(1);
});
});
}
@Directive({selector: '[text]'})
class TextDirective {
@Input() public text: string = null;
}
@Component({selector: 'needs-content-children', template: ''})
class NeedsContentChildren implements AfterContentInit {
@ContentChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
numberOfChildrenAfterContentInit: number;
ngAfterContentInit() { this.numberOfChildrenAfterContentInit = this.textDirChildren.length; }
}
@Component({selector: 'needs-view-children', template: '<div text></div>'})
class NeedsViewChildren implements AfterViewInit {
@ViewChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
numberOfChildrenAfterViewInit: number;
ngAfterViewInit() { this.numberOfChildrenAfterViewInit = this.textDirChildren.length; }
}
@Component({selector: 'simple', template: 'SIMPLE(<ng-content></ng-content>)'})
class Simple {
}
@Component({selector: 'my-comp', template: ''})
class MyComp {
ctxBoolProp: boolean = false;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,703 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('projection', () => {
beforeEach(() => TestBed.configureTestingModule({declarations: [MainComp, OtherComp, Simple]}));
it('should support simple components', () => {
const template = '<simple><div>A</div></simple>';
TestBed.overrideComponent(MainComp, {set: {template}});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('SIMPLE(A)');
});
it('should support simple components with text interpolation as direct children', () => {
const template = '{{\'START(\'}}<simple>' +
'{{text}}' +
'</simple>{{\')END\'}}';
TestBed.overrideComponent(MainComp, {set: {template}});
const main = TestBed.createComponent(MainComp);
main.componentInstance.text = 'A';
main.detectChanges();
expect(main.nativeElement).toHaveText('START(SIMPLE(A))END');
});
it('should support projecting text interpolation to a non bound element', () => {
TestBed.overrideComponent(
Simple, {set: {template: 'SIMPLE(<div><ng-content></ng-content></div>)'}});
TestBed.overrideComponent(MainComp, {set: {template: '<simple>{{text}}</simple>'}});
const main = TestBed.createComponent(MainComp);
main.componentInstance.text = 'A';
main.detectChanges();
expect(main.nativeElement).toHaveText('SIMPLE(A)');
});
it('should support projecting text interpolation to a non bound element with other bound elements after it',
() => {
TestBed.overrideComponent(Simple, {
set: {
template: 'SIMPLE(<div><ng-content></ng-content></div><div [tabIndex]="0">EL</div>)'
}
});
TestBed.overrideComponent(MainComp, {set: {template: '<simple>{{text}}</simple>'}});
const main = TestBed.createComponent(MainComp);
main.componentInstance.text = 'A';
main.detectChanges();
expect(main.nativeElement).toHaveText('SIMPLE(AEL)');
});
it('should project content components', () => {
TestBed.overrideComponent(
Simple, {set: {template: 'SIMPLE({{0}}|<ng-content></ng-content>|{{2}})'}});
TestBed.overrideComponent(OtherComp, {set: {template: '{{1}}'}});
TestBed.overrideComponent(MainComp, {set: {template: '<simple><other></other></simple>'}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(main.nativeElement).toHaveText('SIMPLE(0|1|2)');
});
it('should not show the light dom even if there is no content tag', () => {
TestBed.configureTestingModule({declarations: [Empty]});
TestBed.overrideComponent(MainComp, {set: {template: '<empty>A</empty>'}});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('');
});
it('should support multiple content tags', () => {
TestBed.configureTestingModule({declarations: [MultipleContentTagsComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<div>B</div>' +
'<div>C</div>' +
'<div class="left">A</div>' +
'</multiple-content-tags>'
}
});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('(A, BC)');
});
it('should redistribute only direct children', () => {
TestBed.configureTestingModule({declarations: [MultipleContentTagsComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<div>B<div class="left">A</div></div>' +
'<div>C</div>' +
'</multiple-content-tags>'
}
});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('(, BAC)');
});
it('should redistribute direct child viewcontainers when the light dom changes', () => {
TestBed.configureTestingModule(
{declarations: [MultipleContentTagsComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<ng-template manual class="left"><div>A1</div></ng-template>' +
'<div>B</div>' +
'</multiple-content-tags>'
}
});
const main = TestBed.createComponent(MainComp);
const viewportDirectives = main.debugElement.children[0]
.childNodes.filter(By.directive(ManualViewportDirective))
.map(de => de.injector.get(ManualViewportDirective));
expect(main.nativeElement).toHaveText('(, B)');
viewportDirectives.forEach(d => d.show());
expect(main.nativeElement).toHaveText('(A1, B)');
viewportDirectives.forEach(d => d.hide());
expect(main.nativeElement).toHaveText('(, B)');
});
it('should support nested components', () => {
TestBed.configureTestingModule({declarations: [OuterWithIndirectNestedComponent]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<outer-with-indirect-nested>' +
'<div>A</div>' +
'<div>B</div>' +
'</outer-with-indirect-nested>'
}
});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('OUTER(SIMPLE(AB))');
});
it('should support nesting with content being direct child of a nested component', () => {
TestBed.configureTestingModule({
declarations:
[InnerComponent, InnerInnerComponent, OuterComponent, ManualViewportDirective]
});
TestBed.overrideComponent(MainComp, {
set: {
template: '<outer>' +
'<ng-template manual class="left"><div>A</div></ng-template>' +
'<div>B</div>' +
'<div>C</div>' +
'</outer>'
}
});
const main = TestBed.createComponent(MainComp);
const viewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(,BC)))');
viewportDirective.show();
expect(main.nativeElement).toHaveText('OUTER(INNER(INNERINNER(A,BC)))');
});
it('should redistribute when the shadow dom changes', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'<div>C</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
const viewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('(, BC)');
viewportDirective.show();
expect(main.nativeElement).toHaveText('(A, BC)');
viewportDirective.hide();
expect(main.nativeElement).toHaveText('(, BC)');
});
// GH-2095 - https://github.com/angular/angular/issues/2095
// important as we are removing the ng-content element during compilation,
// which could skrew up text node indices.
it('should support text nodes after content tags', () => {
TestBed.overrideComponent(MainComp, {set: {template: '<simple stringProp="text"></simple>'}});
TestBed.overrideComponent(
Simple, {set: {template: '<ng-content></ng-content><p>P,</p>{{stringProp}}'}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(main.nativeElement).toHaveText('P,text');
});
// important as we are moving style tags around during compilation,
// which could skrew up text node indices.
it('should support text nodes after style tags', () => {
TestBed.overrideComponent(MainComp, {set: {template: '<simple stringProp="text"></simple>'}});
TestBed.overrideComponent(
Simple, {set: {template: '<style></style><p>P,</p>{{stringProp}}'}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(main.nativeElement).toHaveText('P,text');
});
it('should support moving non projected light dom around', () => {
let sourceDirective: ManualViewportDirective;
@Directive({selector: '[manual]'})
class ManualViewportDirective {
constructor(public templateRef: TemplateRef<Object>) { sourceDirective = this; }
}
TestBed.configureTestingModule(
{declarations: [Empty, ProjectDirective, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<empty>' +
' <ng-template manual><div>A</div></ng-template>' +
'</empty>' +
'START(<div project></div>)END'
}
});
const main = TestBed.createComponent(MainComp);
const projectDirective: ProjectDirective =
main.debugElement.queryAllNodes(By.directive(ProjectDirective))[0].injector.get(
ProjectDirective);
expect(main.nativeElement).toHaveText('START()END');
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('START(A)END');
});
it('should support moving projected light dom around', () => {
TestBed.configureTestingModule(
{declarations: [Empty, ProjectDirective, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<simple><ng-template manual><div>A</div></ng-template></simple>' +
'START(<div project></div>)END'
}
});
const main = TestBed.createComponent(MainComp);
const sourceDirective: ManualViewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const projectDirective: ProjectDirective =
main.debugElement.queryAllNodes(By.directive(ProjectDirective))[0].injector.get(
ProjectDirective);
expect(main.nativeElement).toHaveText('SIMPLE()START()END');
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('SIMPLE()START(A)END');
});
it('should support moving ng-content around', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ProjectDirective, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<div>B</div>' +
'</conditional-content>' +
'START(<div project></div>)END'
}
});
const main = TestBed.createComponent(MainComp);
const sourceDirective: ManualViewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
const projectDirective: ProjectDirective =
main.debugElement.queryAllNodes(By.directive(ProjectDirective))[0].injector.get(
ProjectDirective);
expect(main.nativeElement).toHaveText('(, B)START()END');
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
// Stamping ng-content multiple times should not produce the content multiple
// times...
projectDirective.show(sourceDirective.templateRef);
expect(main.nativeElement).toHaveText('(, B)START(A)END');
});
// 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', () => {
TestBed.configureTestingModule({declarations: [Tree, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {set: {template: '<tree></tree>'}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
const manualDirective: ManualViewportDirective =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('TREE(0:)');
manualDirective.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('TREE(0:TREE(1:))');
});
// 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', () => {
TestBed.configureTestingModule({declarations: [Tree, Tree2, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {set: {template: '<tree></tree>'}});
TestBed.overrideComponent(
Tree, {set: {template: 'TREE({{depth}}:<tree2 *manual [depth]="depth+1"></tree2>)'}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(main.nativeElement).toHaveText('TREE(0:)');
const tree = main.debugElement.query(By.directive(Tree));
let manualDirective: ManualViewportDirective = tree.queryAllNodes(By.directive(
ManualViewportDirective))[0].injector.get(ManualViewportDirective);
manualDirective.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('TREE(0:TREE2(1:))');
const tree2 = main.debugElement.query(By.directive(Tree2));
manualDirective = tree2.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
manualDirective.show();
main.detectChanges();
expect(main.nativeElement).toHaveText('TREE(0:TREE2(1:TREE(2:)))');
});
if (getDOM().supportsNativeShadowDOM()) {
it('should support native content projection and isolate styles per component', () => {
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<simple-native1><div>A</div></simple-native1>' +
'<simple-native2><div>B</div></simple-native2>'
}
});
const main = TestBed.createComponent(MainComp);
const childNodes = getDOM().childNodes(main.nativeElement);
expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)');
expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)');
main.destroy();
});
}
if (getDOM().supportsDOMEvents()) {
it('should support non emulated styles', () => {
TestBed.configureTestingModule({declarations: [OtherComp]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<div class="redStyle"></div>',
styles: ['.redStyle { color: red}'],
encapsulation: ViewEncapsulation.None,
}
});
const main = TestBed.createComponent(MainComp);
const mainEl = main.nativeElement;
const div1 = getDOM().firstChild(mainEl);
const div2 = getDOM().createElement('div');
getDOM().setAttribute(div2, 'class', 'redStyle');
getDOM().appendChild(mainEl, div2);
expect(getDOM().getComputedStyle(div1).color).toEqual('rgb(255, 0, 0)');
expect(getDOM().getComputedStyle(div2).color).toEqual('rgb(255, 0, 0)');
});
it('should support emulated style encapsulation', () => {
TestBed.configureTestingModule({declarations: [OtherComp]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<div></div>',
styles: ['div { color: red}'],
encapsulation: ViewEncapsulation.Emulated,
}
});
const main = TestBed.createComponent(MainComp);
const mainEl = main.nativeElement;
const div1 = getDOM().firstChild(mainEl);
const div2 = getDOM().createElement('div');
getDOM().appendChild(mainEl, div2);
expect(getDOM().getComputedStyle(div1).color).toEqual('rgb(255, 0, 0)');
expect(getDOM().getComputedStyle(div2).color).toEqual('rgb(0, 0, 0)');
});
}
it('should support nested conditionals that contain ng-contents', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalTextComponent, ManualViewportDirective]});
TestBed.overrideComponent(
MainComp, {set: {template: `<conditional-text>a</conditional-text>`}});
const main = TestBed.createComponent(MainComp);
expect(main.nativeElement).toHaveText('MAIN()');
let viewportElement =
main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[0];
viewportElement.injector.get(ManualViewportDirective).show();
expect(main.nativeElement).toHaveText('MAIN(FIRST())');
viewportElement = main.debugElement.queryAllNodes(By.directive(ManualViewportDirective))[1];
viewportElement.injector.get(ManualViewportDirective).show();
expect(main.nativeElement).toHaveText('MAIN(FIRST(SECOND(a)))');
});
it('should allow to switch the order of nested components via ng-content', () => {
TestBed.configureTestingModule({declarations: [CmpA, CmpB, CmpD, CmpC]});
TestBed.overrideComponent(MainComp, {set: {template: `<cmp-a><cmp-b></cmp-b></cmp-a>`}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(getDOM().getInnerHTML(main.nativeElement))
.toEqual(
'<cmp-a><cmp-b><cmp-d><i>cmp-d</i></cmp-d></cmp-b>' +
'<cmp-c><b>cmp-c</b></cmp-c></cmp-a>');
});
it('should create nested components in the right order', () => {
TestBed.configureTestingModule(
{declarations: [CmpA1, CmpA2, CmpB11, CmpB12, CmpB21, CmpB22]});
TestBed.overrideComponent(MainComp, {set: {template: `<cmp-a1></cmp-a1><cmp-a2></cmp-a2>`}});
const main = TestBed.createComponent(MainComp);
main.detectChanges();
expect(getDOM().getInnerHTML(main.nativeElement))
.toEqual(
'<cmp-a1>a1<cmp-b11>b11</cmp-b11><cmp-b12>b12</cmp-b12></cmp-a1>' +
'<cmp-a2>a2<cmp-b21>b21</cmp-b21><cmp-b22>b22</cmp-b22></cmp-a2>');
});
it('should project filled view containers into a view container', () => {
TestBed.configureTestingModule(
{declarations: [ConditionalContentComponent, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<ng-template manual class="left">B</ng-template>' +
'<div class="left">C</div>' +
'<div>D</div>' +
'</conditional-content>'
}
});
const main = TestBed.createComponent(MainComp);
const conditionalComp = main.debugElement.query(By.directive(ConditionalContentComponent));
const viewViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[0].injector.get(
ManualViewportDirective);
expect(main.nativeElement).toHaveText('(, D)');
expect(main.nativeElement).toHaveText('(, D)');
viewViewportDir.show();
expect(main.nativeElement).toHaveText('(AC, D)');
const contentViewportDir =
conditionalComp.queryAllNodes(By.directive(ManualViewportDirective))[1].injector.get(
ManualViewportDirective);
contentViewportDir.show();
expect(main.nativeElement).toHaveText('(ABC, D)');
// hide view viewport, and test that it also hides
// the content viewport's views
viewViewportDir.hide();
expect(main.nativeElement).toHaveText('(, D)');
});
});
}
@Component({selector: 'main', template: ''})
class MainComp {
text: string = '';
}
@Component({selector: 'other', template: ''})
class OtherComp {
text: string = '';
}
@Component({
selector: 'simple',
inputs: ['stringProp'],
template: 'SIMPLE(<ng-content></ng-content>)',
})
class Simple {
stringProp: string = '';
}
@Component({
selector: 'simple-native1',
template: 'SIMPLE1(<content></content>)',
encapsulation: ViewEncapsulation.Native,
styles: ['div {color: red}']
})
class SimpleNative1 {
}
@Component({
selector: 'simple-native2',
template: 'SIMPLE2(<content></content>)',
encapsulation: ViewEncapsulation.Native,
styles: ['div {color: blue}']
})
class SimpleNative2 {
}
@Component({selector: 'empty', template: ''})
class Empty {
}
@Component({
selector: 'multiple-content-tags',
template: '(<ng-content SELECT=".left"></ng-content>, <ng-content></ng-content>)',
})
class MultipleContentTagsComponent {
}
@Directive({selector: '[manual]'})
class ManualViewportDirective {
constructor(public vc: ViewContainerRef, public templateRef: TemplateRef<Object>) {}
show() { this.vc.createEmbeddedView(this.templateRef); }
hide() { this.vc.clear(); }
}
@Directive({selector: '[project]'})
class ProjectDirective {
constructor(public vc: ViewContainerRef) {}
show(templateRef: TemplateRef<Object>) { this.vc.createEmbeddedView(templateRef); }
hide() { this.vc.clear(); }
}
@Component({
selector: 'outer-with-indirect-nested',
template: 'OUTER(<simple><div><ng-content></ng-content></div></simple>)',
})
class OuterWithIndirectNestedComponent {
}
@Component({
selector: 'outer',
template:
'OUTER(<inner><ng-content select=".left" class="left"></ng-content><ng-content></ng-content></inner>)',
})
class OuterComponent {
}
@Component({
selector: 'inner',
template:
'INNER(<innerinner><ng-content select=".left" class="left"></ng-content><ng-content></ng-content></innerinner>)',
})
class InnerComponent {
}
@Component({
selector: 'innerinner',
template: 'INNERINNER(<ng-content select=".left"></ng-content>,<ng-content></ng-content>)',
})
class InnerInnerComponent {
}
@Component({
selector: 'conditional-content',
template:
'<div>(<div *manual><ng-content select=".left"></ng-content></div>, <ng-content></ng-content>)</div>',
})
class ConditionalContentComponent {
}
@Component({
selector: 'conditional-text',
template:
'MAIN(<ng-template manual>FIRST(<ng-template manual>SECOND(<ng-content></ng-content>)</ng-template>)</ng-template>)',
})
class ConditionalTextComponent {
}
@Component({
selector: 'tab',
template: '<div><div *manual>TAB(<ng-content></ng-content>)</div></div>',
})
class Tab {
}
@Component({
selector: 'tree2',
inputs: ['depth'],
template: 'TREE2({{depth}}:<tree *manual [depth]="depth+1"></tree>)',
})
class Tree2 {
depth = 0;
}
@Component({
selector: 'tree',
inputs: ['depth'],
template: 'TREE({{depth}}:<tree *manual [depth]="depth+1"></tree>)',
})
class Tree {
depth = 0;
}
@Component({selector: 'cmp-d', template: `<i>{{tagName}}</i>`})
class CmpD {
tagName: string;
constructor(elementRef: ElementRef) {
this.tagName = getDOM().tagName(elementRef.nativeElement).toLowerCase();
}
}
@Component({selector: 'cmp-c', template: `<b>{{tagName}}</b>`})
class CmpC {
tagName: string;
constructor(elementRef: ElementRef) {
this.tagName = getDOM().tagName(elementRef.nativeElement).toLowerCase();
}
}
@Component({selector: 'cmp-b', template: `<ng-content></ng-content><cmp-d></cmp-d>`})
class CmpB {
}
@Component({selector: 'cmp-a', template: `<ng-content></ng-content><cmp-c></cmp-c>`})
class CmpA {
}
@Component({selector: 'cmp-b11', template: `{{'b11'}}`})
class CmpB11 {
}
@Component({selector: 'cmp-b12', template: `{{'b12'}}`})
class CmpB12 {
}
@Component({selector: 'cmp-b21', template: `{{'b21'}}`})
class CmpB21 {
}
@Component({selector: 'cmp-b22', template: `{{'b22'}}`})
class CmpB22 {
}
@Component({
selector: 'cmp-a1',
template: `{{'a1'}}<cmp-b11></cmp-b11><cmp-b12></cmp-b12>`,
})
class CmpA1 {
}
@Component({
selector: 'cmp-a2',
template: `{{'a2'}}<cmp-b21></cmp-b21><cmp-b22></cmp-b22>`,
})
class CmpA2 {
}

View File

@ -0,0 +1,818 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, asNativeElements} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
import {stringify} from '../../src/util';
export function main() {
describe('Query API', () => {
beforeEach(() => TestBed.configureTestingModule({
declarations: [
MyComp0,
NeedsQuery,
NeedsQueryDesc,
NeedsQueryByLabel,
NeedsQueryByTwoLabels,
NeedsQueryAndProject,
NeedsViewQuery,
NeedsViewQueryIf,
NeedsViewQueryNestedIf,
NeedsViewQueryOrder,
NeedsViewQueryByLabel,
NeedsViewQueryOrderWithParent,
NeedsContentChildren,
NeedsViewChildren,
NeedsViewChild,
NeedsStaticContentAndViewChild,
NeedsContentChild,
NeedsTpl,
NeedsNamedTpl,
TextDirective,
InertDirective,
NeedsFourQueries,
NeedsContentChildrenWithRead,
NeedsContentChildWithRead,
NeedsViewChildrenWithRead,
NeedsViewChildWithRead,
NeedsContentChildTemplateRef,
NeedsContentChildTemplateRefApp,
NeedsViewContainerWithRead,
ManualProjecting
]
}));
describe('querying by directive type', () => {
it('should contain all direct child directives in the light dom (constructor)', () => {
const template = '<div text="1"></div>' +
'<needs-query text="2"><div text="3">' +
'<div text="too-deep"></div>' +
'</div></needs-query>' +
'<div text="4"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('2|3|');
});
it('should contain all direct child directives in the content dom', () => {
const template =
'<needs-content-children #q><div text="foo"></div></needs-content-children>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
view.detectChanges();
expect(q.textDirChildren.length).toEqual(1);
expect(q.numberOfChildrenAfterContentInit).toEqual(1);
});
it('should contain the first content child', () => {
const template =
'<needs-content-child #q><div *ngIf="shouldShow" text="foo"></div></needs-content-child>';
const view = createTestCmp(MyComp0, template);
view.componentInstance.shouldShow = true;
view.detectChanges();
const q: NeedsContentChild = view.debugElement.children[0].references['q'];
expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]);
view.componentInstance.shouldShow = false;
view.detectChanges();
expect(q.logs).toEqual([
['setter', 'foo'], ['init', 'foo'], ['check', 'foo'], ['setter', null], ['check', null]
]);
});
it('should contain the first view child', () => {
const template = '<needs-view-child #q></needs-view-child>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewChild = view.debugElement.children[0].references['q'];
expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]);
q.shouldShow = false;
view.detectChanges();
expect(q.logs).toEqual([
['setter', 'foo'], ['init', 'foo'], ['check', 'foo'], ['setter', null], ['check', null]
]);
});
it('should set static view and content children already after the constructor call', () => {
const template =
'<needs-static-content-view-child #q><div text="contentFoo"></div></needs-static-content-view-child>';
const view = createTestCmp(MyComp0, template);
const q: NeedsStaticContentAndViewChild = view.debugElement.children[0].references['q'];
expect(q.contentChild.text).toBeFalsy();
expect(q.viewChild.text).toBeFalsy();
view.detectChanges();
expect(q.contentChild.text).toEqual('contentFoo');
expect(q.viewChild.text).toEqual('viewFoo');
});
it('should contain the first view child across embedded views', () => {
TestBed.overrideComponent(
MyComp0, {set: {template: '<needs-view-child #q></needs-view-child>'}});
TestBed.overrideComponent(NeedsViewChild, {
set: {
template:
'<div *ngIf="true"><div *ngIf="shouldShow" text="foo"></div></div><div *ngIf="shouldShow2" text="bar"></div>'
}
});
const view = TestBed.createComponent(MyComp0);
view.detectChanges();
const q: NeedsViewChild = view.debugElement.children[0].references['q'];
expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]);
q.shouldShow = false;
q.shouldShow2 = true;
q.logs = [];
view.detectChanges();
expect(q.logs).toEqual([['setter', 'bar'], ['check', 'bar']]);
q.shouldShow = false;
q.shouldShow2 = false;
q.logs = [];
view.detectChanges();
expect(q.logs).toEqual([['setter', null], ['check', null]]);
});
it('should contain all directives in the light dom when descendants flag is used', () => {
const template = '<div text="1"></div>' +
'<needs-query-desc text="2"><div text="3">' +
'<div text="4"></div>' +
'</div></needs-query-desc>' +
'<div text="5"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('2|3|4|');
});
it('should contain all directives in the light dom', () => {
const template = '<div text="1"></div>' +
'<needs-query text="2"><div text="3"></div></needs-query>' +
'<div text="4"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('2|3|');
});
it('should reflect dynamically inserted directives', () => {
const template = '<div text="1"></div>' +
'<needs-query text="2"><div *ngIf="shouldShow" [text]="\'3\'"></div></needs-query>' +
'<div text="4"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('2|');
view.componentInstance.shouldShow = true;
view.detectChanges();
expect(asNativeElements(view.debugElement.children)).toHaveText('2|3|');
});
it('should be cleanly destroyed when a query crosses view boundaries', () => {
const template = '<div text="1"></div>' +
'<needs-query text="2"><div *ngIf="shouldShow" [text]="\'3\'"></div></needs-query>' +
'<div text="4"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
view.componentInstance.shouldShow = true;
view.detectChanges();
view.destroy();
});
it('should reflect moved directives', () => {
const template = '<div text="1"></div>' +
'<needs-query text="2"><div *ngFor="let i of list" [text]="i"></div></needs-query>' +
'<div text="4"></div>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('2|1d|2d|3d|');
view.componentInstance.list = ['3d', '2d'];
view.detectChanges();
expect(asNativeElements(view.debugElement.children)).toHaveText('2|3d|2d|');
});
it('should throw with descriptive error when query selectors are not present', () => {
TestBed.configureTestingModule({declarations: [MyCompBroken0, HasNullQueryCondition]});
const template = '<has-null-query-condition></has-null-query-condition>';
TestBed.overrideComponent(MyCompBroken0, {set: {template}});
expect(() => TestBed.createComponent(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', () => {
it('should find TemplateRefs in the light and shadow dom', () => {
const template = '<needs-tpl><ng-template><div>light</div></ng-template></needs-tpl>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const needsTpl: NeedsTpl = view.debugElement.children[0].injector.get(NeedsTpl);
expect(needsTpl.vc.createEmbeddedView(needsTpl.query.first).rootNodes[0])
.toHaveText('light');
expect(needsTpl.vc.createEmbeddedView(needsTpl.viewQuery.first).rootNodes[0])
.toHaveText('shadow');
});
it('should find named TemplateRefs', () => {
const template =
'<needs-named-tpl><ng-template #tpl><div>light</div></ng-template></needs-named-tpl>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const needsTpl: NeedsNamedTpl = view.debugElement.children[0].injector.get(NeedsNamedTpl);
expect(needsTpl.vc.createEmbeddedView(needsTpl.contentTpl).rootNodes[0])
.toHaveText('light');
expect(needsTpl.vc.createEmbeddedView(needsTpl.viewTpl).rootNodes[0]).toHaveText('shadow');
});
});
describe('read a different token', () => {
it('should contain all content children', () => {
const template =
'<needs-content-children-read #q text="ca"><div #q text="cb"></div></needs-content-children-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsContentChildrenWithRead =
view.debugElement.children[0].injector.get(NeedsContentChildrenWithRead);
expect(comp.textDirChildren.map(textDirective => textDirective.text)).toEqual(['ca', 'cb']);
});
it('should contain the first content child', () => {
const template =
'<needs-content-child-read><div #q text="ca"></div></needs-content-child-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsContentChildWithRead =
view.debugElement.children[0].injector.get(NeedsContentChildWithRead);
expect(comp.textDirChild.text).toEqual('ca');
});
it('should contain the first descendant content child', () => {
const template = '<needs-content-child-read>' +
'<div dir><div #q text="ca"></div></div>' +
'</needs-content-child-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsContentChildWithRead =
view.debugElement.children[0].injector.get(NeedsContentChildWithRead);
expect(comp.textDirChild.text).toEqual('ca');
});
it('should contain the first descendant content child templateRef', () => {
const template = '<needs-content-child-template-ref-app>' +
'</needs-content-child-template-ref-app>';
const view = createTestCmp(MyComp0, template);
// can't execute checkNoChanges as our view modifies our content children (via a query).
view.detectChanges(false);
expect(view.nativeElement).toHaveText('OUTER');
});
it('should contain the first view child', () => {
const template = '<needs-view-child-read></needs-view-child-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsViewChildWithRead =
view.debugElement.children[0].injector.get(NeedsViewChildWithRead);
expect(comp.textDirChild.text).toEqual('va');
});
it('should contain all child directives in the view', () => {
const template = '<needs-view-children-read></needs-view-children-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsViewChildrenWithRead =
view.debugElement.children[0].injector.get(NeedsViewChildrenWithRead);
expect(comp.textDirChildren.map(textDirective => textDirective.text)).toEqual(['va', 'vb']);
});
it('should support reading a ViewContainer', () => {
const template =
'<needs-viewcontainer-read><ng-template>hello</ng-template></needs-viewcontainer-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsViewContainerWithRead =
view.debugElement.children[0].injector.get(NeedsViewContainerWithRead);
comp.createView();
expect(view.debugElement.children[0].nativeElement).toHaveText('hello');
});
});
describe('changes', () => {
it('should notify query on change', async(() => {
const template = '<needs-query #q>' +
'<div text="1"></div>' +
'<div *ngIf="shouldShow" text="2"></div>' +
'</needs-query>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
q.query.changes.subscribe({
next: () => {
expect(q.query.first.text).toEqual('1');
expect(q.query.last.text).toEqual('2');
}
});
view.componentInstance.shouldShow = true;
view.detectChanges();
}));
it('should correctly clean-up when destroyed together with the directives it is querying',
() => {
const template =
'<needs-query #q *ngIf="shouldShow"><div text="foo"></div></needs-query>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
view.componentInstance.shouldShow = true;
view.detectChanges();
const q: NeedsQuery = view.debugElement.children[0].references['q'];
expect(q.query.length).toEqual(1);
view.componentInstance.shouldShow = false;
view.detectChanges();
view.componentInstance.shouldShow = true;
view.detectChanges();
const q2: NeedsQuery = view.debugElement.children[0].references['q'];
expect(q2.query.length).toEqual(1);
});
});
describe('querying by var binding', () => {
it('should contain all the child directives in the light dom with the given var binding',
() => {
const template = '<needs-query-by-ref-binding #q>' +
'<div *ngFor="let item of list" [text]="item" #textLabel="textDir"></div>' +
'</needs-query-by-ref-binding>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
view.componentInstance.list = ['1d', '2d'];
view.detectChanges();
expect(q.query.first.text).toEqual('1d');
expect(q.query.last.text).toEqual('2d');
});
it('should support querying by multiple var bindings', () => {
const template = '<needs-query-by-ref-bindings #q>' +
'<div text="one" #textLabel1="textDir"></div>' +
'<div text="two" #textLabel2="textDir"></div>' +
'</needs-query-by-ref-bindings>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
expect(q.query.first.text).toEqual('one');
expect(q.query.last.text).toEqual('two');
});
it('should support dynamically inserted directives', () => {
const template = '<needs-query-by-ref-binding #q>' +
'<div *ngFor="let item of list" [text]="item" #textLabel="textDir"></div>' +
'</needs-query-by-ref-binding>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
view.componentInstance.list = ['1d', '2d'];
view.detectChanges();
view.componentInstance.list = ['2d', '1d'];
view.detectChanges();
expect(q.query.last.text).toEqual('1d');
});
it('should contain all the elements in the light dom with the given var binding', () => {
const template = '<needs-query-by-ref-binding #q>' +
'<div *ngFor="let item of list">' +
'<div #textLabel>{{item}}</div>' +
'</div>' +
'</needs-query-by-ref-binding>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
view.componentInstance.list = ['1d', '2d'];
view.detectChanges();
expect(q.query.first.nativeElement).toHaveText('1d');
expect(q.query.last.nativeElement).toHaveText('2d');
});
it('should contain all the elements in the light dom even if they get projected', () => {
const template = '<needs-query-and-project #q>' +
'<div text="hello"></div><div text="world"></div>' +
'</needs-query-and-project>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
expect(asNativeElements(view.debugElement.children)).toHaveText('hello|world|');
});
it('should support querying the view by using a view query', () => {
const template = '<needs-view-query-by-ref-binding #q></needs-view-query-by-ref-binding>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryByLabel = view.debugElement.children[0].references['q'];
expect(q.query.first.nativeElement).toHaveText('text');
});
it('should contain all child directives in the view dom', () => {
const template = '<needs-view-children #q></needs-view-children>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
expect(q.textDirChildren.length).toEqual(1);
expect(q.numberOfChildrenAfterViewInit).toEqual(1);
});
});
describe('querying in the view', () => {
it('should contain all the elements in the view with that have the given directive', () => {
const template = '<needs-view-query #q><div text="ignoreme"></div></needs-view-query>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQuery = view.debugElement.children[0].references['q'];
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2', '3', '4']);
});
it('should not include directive present on the host element', () => {
const template = '<needs-view-query #q text="self"></needs-view-query>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQuery = view.debugElement.children[0].references['q'];
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2', '3', '4']);
});
it('should reflect changes in the component', () => {
const template = '<needs-view-query-if #q></needs-view-query-if>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryIf = view.debugElement.children[0].references['q'];
expect(q.query.length).toBe(0);
q.show = true;
view.detectChanges();
expect(q.query.length).toBe(1);
expect(q.query.first.text).toEqual('1');
});
it('should not be affected by other changes in the component', () => {
const template = '<needs-view-query-nested-if #q></needs-view-query-nested-if>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryNestedIf = view.debugElement.children[0].references['q'];
expect(q.query.length).toEqual(1);
expect(q.query.first.text).toEqual('1');
q.show = false;
view.detectChanges();
expect(q.query.length).toEqual(1);
expect(q.query.first.text).toEqual('1');
});
it('should maintain directives in pre-order depth-first DOM order after dynamic insertion',
() => {
const template = '<needs-view-query-order #q></needs-view-query-order>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryOrder = view.debugElement.children[0].references['q'];
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2', '3', '4']);
q.list = ['-3', '2'];
view.detectChanges();
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '-3', '2', '4']);
});
it('should maintain directives in pre-order depth-first DOM order after dynamic insertion',
() => {
const template = '<needs-view-query-order-with-p #q></needs-view-query-order-with-p>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryOrderWithParent = view.debugElement.children[0].references['q'];
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2', '3', '4']);
q.list = ['-3', '2'];
view.detectChanges();
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '-3', '2', '4']);
});
it('should handle long ngFor cycles', () => {
const template = '<needs-view-query-order #q></needs-view-query-order>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q: NeedsViewQueryOrder = view.debugElement.children[0].references['q'];
// no significance to 50, just a reasonably large cycle.
for (let i = 0; i < 50; i++) {
const newString = i.toString();
q.list = [newString];
view.detectChanges();
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', newString, '4']);
}
});
it('should support more than three queries', () => {
const template = '<needs-four-queries #q><div text="1"></div></needs-four-queries>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
expect(q.query1).toBeDefined();
expect(q.query2).toBeDefined();
expect(q.query3).toBeDefined();
expect(q.query4).toBeDefined();
});
});
describe('query over moved templates', () => {
it('should include manually projected templates in queries', () => {
const template =
'<manual-projecting #q><ng-template><div text="1"></div></ng-template></manual-projecting>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
expect(q.query.length).toBe(0);
q.create();
view.detectChanges();
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1']);
q.destroy();
view.detectChanges();
expect(q.query.length).toBe(0);
});
});
});
}
@Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'})
class TextDirective {
text: string;
constructor() {}
}
@Component({selector: 'needs-content-children', template: ''})
class NeedsContentChildren implements AfterContentInit {
@ContentChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
numberOfChildrenAfterContentInit: number;
ngAfterContentInit() { this.numberOfChildrenAfterContentInit = this.textDirChildren.length; }
}
@Component({selector: 'needs-view-children', template: '<div text></div>'})
class NeedsViewChildren implements AfterViewInit {
@ViewChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
numberOfChildrenAfterViewInit: number;
ngAfterViewInit() { this.numberOfChildrenAfterViewInit = this.textDirChildren.length; }
}
@Component({selector: 'needs-content-child', template: ''})
class NeedsContentChild implements AfterContentInit, AfterContentChecked {
/** @internal */
_child: TextDirective;
@ContentChild(TextDirective)
set child(value) {
this._child = value;
this.logs.push(['setter', value ? value.text : null]);
}
get child() { return this._child; }
logs: any[] /** TODO #9100 */ = [];
ngAfterContentInit() { this.logs.push(['init', this.child ? this.child.text : null]); }
ngAfterContentChecked() { this.logs.push(['check', this.child ? this.child.text : null]); }
}
@Component({selector: 'needs-view-child', template: `<div *ngIf="shouldShow" text="foo"></div>`})
class NeedsViewChild implements AfterViewInit, AfterViewChecked {
shouldShow: boolean = true;
shouldShow2: boolean = false;
/** @internal */
_child: TextDirective;
@ViewChild(TextDirective)
set child(value) {
this._child = value;
this.logs.push(['setter', value ? value.text : null]);
}
get child() { return this._child; }
logs: any[] /** TODO #9100 */ = [];
ngAfterViewInit() { this.logs.push(['init', this.child ? this.child.text : null]); }
ngAfterViewChecked() { this.logs.push(['check', this.child ? this.child.text : null]); }
}
function createTestCmp<T>(type: Type<T>, template: string): ComponentFixture<T> {
const view = TestBed.overrideComponent(type, {set: {template}}).createComponent(type);
return view;
}
function createTestCmpAndDetectChanges<T>(type: Type<T>, template: string): ComponentFixture<T> {
const view = createTestCmp(type, template);
view.detectChanges();
return view;
}
@Component({selector: 'needs-static-content-view-child', template: `<div text="viewFoo"></div>`})
class NeedsStaticContentAndViewChild {
@ContentChild(TextDirective) contentChild: TextDirective;
@ViewChild(TextDirective) viewChild: TextDirective;
}
@Directive({selector: '[dir]'})
class InertDirective {
}
@Component({
selector: 'needs-query',
template: '<div text="ignoreme"></div><b *ngFor="let dir of query">{{dir.text}}|</b>'
})
class NeedsQuery {
@ContentChildren(TextDirective) query: QueryList<TextDirective>;
}
@Component({selector: 'needs-four-queries', template: ''})
class NeedsFourQueries {
@ContentChild(TextDirective) query1: TextDirective;
@ContentChild(TextDirective) query2: TextDirective;
@ContentChild(TextDirective) query3: TextDirective;
@ContentChild(TextDirective) query4: TextDirective;
}
@Component({
selector: 'needs-query-desc',
template: '<ng-content></ng-content><div *ngFor="let dir of query">{{dir.text}}|</div>'
})
class NeedsQueryDesc {
@ContentChildren(TextDirective, {descendants: true}) query: QueryList<TextDirective>;
}
@Component({selector: 'needs-query-by-ref-binding', template: '<ng-content>'})
class NeedsQueryByLabel {
@ContentChildren('textLabel', {descendants: true}) query: QueryList<any>;
}
@Component({selector: 'needs-view-query-by-ref-binding', template: '<div #textLabel>text</div>'})
class NeedsViewQueryByLabel {
@ViewChildren('textLabel') query: QueryList<any>;
}
@Component({selector: 'needs-query-by-ref-bindings', template: '<ng-content>'})
class NeedsQueryByTwoLabels {
@ContentChildren('textLabel1,textLabel2', {descendants: true}) query: QueryList<any>;
}
@Component({
selector: 'needs-query-and-project',
template: '<div *ngFor="let dir of query">{{dir.text}}|</div><ng-content></ng-content>'
})
class NeedsQueryAndProject {
@ContentChildren(TextDirective) query: QueryList<TextDirective>;
}
@Component({
selector: 'needs-view-query',
template: '<div text="1"><div text="2"></div></div><div text="3"></div><div text="4"></div>'
})
class NeedsViewQuery {
@ViewChildren(TextDirective) query: QueryList<TextDirective>;
}
@Component({selector: 'needs-view-query-if', template: '<div *ngIf="show" text="1"></div>'})
class NeedsViewQueryIf {
show: boolean = false;
@ViewChildren(TextDirective) query: QueryList<TextDirective>;
}
@Component({
selector: 'needs-view-query-nested-if',
template: '<div text="1"><div *ngIf="show"><div dir></div></div></div>'
})
class NeedsViewQueryNestedIf {
show: boolean = true;
@ViewChildren(TextDirective) query: QueryList<TextDirective>;
}
@Component({
selector: 'needs-view-query-order',
template: '<div text="1"></div>' +
'<div *ngFor="let i of list" [text]="i"></div>' +
'<div text="4"></div>'
})
class NeedsViewQueryOrder {
@ViewChildren(TextDirective) query: QueryList<TextDirective>;
list: string[] = ['2', '3'];
}
@Component({
selector: 'needs-view-query-order-with-p',
template: '<div dir><div text="1"></div>' +
'<div *ngFor="let i of list" [text]="i"></div>' +
'<div text="4"></div></div>'
})
class NeedsViewQueryOrderWithParent {
@ViewChildren(TextDirective) query: QueryList<TextDirective>;
list: string[] = ['2', '3'];
}
@Component({selector: 'needs-tpl', template: '<ng-template><div>shadow</div></ng-template>'})
class NeedsTpl {
@ViewChildren(TemplateRef) viewQuery: QueryList<TemplateRef<Object>>;
@ContentChildren(TemplateRef) query: QueryList<TemplateRef<Object>>;
constructor(public vc: ViewContainerRef) {}
}
@Component(
{selector: 'needs-named-tpl', template: '<ng-template #tpl><div>shadow</div></ng-template>'})
class NeedsNamedTpl {
@ViewChild('tpl') viewTpl: TemplateRef<Object>;
@ContentChild('tpl') contentTpl: TemplateRef<Object>;
constructor(public vc: ViewContainerRef) {}
}
@Component({selector: 'needs-content-children-read', template: ''})
class NeedsContentChildrenWithRead {
@ContentChildren('q', {read: TextDirective}) textDirChildren: QueryList<TextDirective>;
@ContentChildren('nonExisting', {read: TextDirective}) nonExistingVar: QueryList<TextDirective>;
}
@Component({selector: 'needs-content-child-read', template: ''})
class NeedsContentChildWithRead {
@ContentChild('q', {read: TextDirective}) textDirChild: TextDirective;
@ContentChild('nonExisting', {read: TextDirective}) nonExistingVar: TextDirective;
}
@Component({
selector: 'needs-content-child-template-ref',
template: '<div [ngTemplateOutlet]="templateRef"></div>'
})
class NeedsContentChildTemplateRef {
@ContentChild(TemplateRef) templateRef: TemplateRef<any>;
}
@Component({
selector: 'needs-content-child-template-ref-app',
template: '<needs-content-child-template-ref>' +
'<ng-template>OUTER<ng-template>INNER</ng-template></ng-template>' +
'</needs-content-child-template-ref>'
})
class NeedsContentChildTemplateRefApp {
}
@Component({
selector: 'needs-view-children-read',
template: '<div #q text="va"></div><div #w text="vb"></div>',
})
class NeedsViewChildrenWithRead {
@ViewChildren('q,w', {read: TextDirective}) textDirChildren: QueryList<TextDirective>;
@ViewChildren('nonExisting', {read: TextDirective}) nonExistingVar: QueryList<TextDirective>;
}
@Component({
selector: 'needs-view-child-read',
template: '<div #q text="va"></div>',
})
class NeedsViewChildWithRead {
@ViewChild('q', {read: TextDirective}) textDirChild: TextDirective;
@ViewChild('nonExisting', {read: TextDirective}) nonExistingVar: TextDirective;
}
@Component({selector: 'needs-viewcontainer-read', template: '<div #q></div>'})
class NeedsViewContainerWithRead {
@ViewChild('q', {read: ViewContainerRef}) vc: ViewContainerRef;
@ViewChild('nonExisting', {read: ViewContainerRef}) nonExistingVar: ViewContainerRef;
@ContentChild(TemplateRef) template: TemplateRef<Object>;
createView() { this.vc.createEmbeddedView(this.template); }
}
@Component({selector: 'has-null-query-condition', template: '<div></div>'})
class HasNullQueryCondition {
@ContentChildren(null) errorTrigger: any;
}
@Component({selector: 'my-comp', template: ''})
class MyComp0 {
shouldShow: boolean = false;
list: string[] = ['1d', '2d', '3d'];
}
@Component({selector: 'my-comp', template: ''})
class MyCompBroken0 {
}
@Component({selector: 'manual-projecting', template: '<div #vc></div>'})
class ManualProjecting {
@ContentChild(TemplateRef) template: TemplateRef<any>;
@ViewChild('vc', {read: ViewContainerRef})
vc: ViewContainerRef;
@ContentChildren(TextDirective)
query: QueryList<TextDirective>;
create() { this.vc.createEmbeddedView(this.template); }
destroy() { this.vc.clear(); }
}

View File

@ -0,0 +1,152 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {iterateListLike} from '@angular/core/src/change_detection/change_detection_util';
import {QueryList} from '@angular/core/src/linker/query_list';
import {fakeAsync, tick} from '@angular/core/testing';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
export function main() {
describe('QueryList', () => {
let queryList: QueryList<string>;
let log: string;
beforeEach(() => {
queryList = new QueryList<string>();
log = '';
});
function logAppend(item: any /** TODO #9100 */) { log += (log.length == 0 ? '' : ', ') + item; }
it('should support resetting and iterating over the new objects', () => {
queryList.reset(['one']);
queryList.reset(['two']);
iterateListLike(queryList, logAppend);
expect(log).toEqual('two');
});
it('should support length', () => {
queryList.reset(['one', 'two']);
expect(queryList.length).toEqual(2);
});
it('should support map', () => {
queryList.reset(['one', 'two']);
expect(queryList.map((x) => x)).toEqual(['one', 'two']);
});
it('should support map with index', () => {
queryList.reset(['one', 'two']);
expect(queryList.map((x, i) => `${x}_${i}`)).toEqual(['one_0', 'two_1']);
});
it('should support forEach', () => {
queryList.reset(['one', 'two']);
let join = '';
queryList.forEach((x) => join = join + x);
expect(join).toEqual('onetwo');
});
it('should support forEach with index', () => {
queryList.reset(['one', 'two']);
let join = '';
queryList.forEach((x, i) => join = join + x + i);
expect(join).toEqual('one0two1');
});
it('should support filter', () => {
queryList.reset(['one', 'two']);
expect(queryList.filter((x: string) => x == 'one')).toEqual(['one']);
});
it('should support filter with index', () => {
queryList.reset(['one', 'two']);
expect(queryList.filter((x: string, i: number) => i == 0)).toEqual(['one']);
});
it('should support find', () => {
queryList.reset(['one', 'two']);
expect(queryList.find((x: string) => x == 'two')).toEqual('two');
});
it('should support find with index', () => {
queryList.reset(['one', 'two']);
expect(queryList.find((x: string, i: number) => i == 1)).toEqual('two');
});
it('should support reduce', () => {
queryList.reset(['one', 'two']);
expect(queryList.reduce((a: string, x: string) => a + x, 'start:')).toEqual('start:onetwo');
});
it('should support reduce with index', () => {
queryList.reset(['one', 'two']);
expect(queryList.reduce((a: string, x: string, i: number) => a + x + i, 'start:'))
.toEqual('start:one0two1');
});
it('should support toArray', () => {
queryList.reset(['one', 'two']);
expect(queryList.reduce((a: string, x: string) => a + x, 'start:')).toEqual('start:onetwo');
});
it('should support toArray', () => {
queryList.reset(['one', 'two']);
expect(queryList.toArray()).toEqual(['one', 'two']);
});
it('should support toString', () => {
queryList.reset(['one', 'two']);
const listString = queryList.toString();
expect(listString.indexOf('one') != -1).toBeTruthy();
expect(listString.indexOf('two') != -1).toBeTruthy();
});
it('should support first and last', () => {
queryList.reset(['one', 'two', 'three']);
expect(queryList.first).toEqual('one');
expect(queryList.last).toEqual('three');
});
it('should support some', () => {
queryList.reset(['one', 'two', 'three']);
expect(queryList.some(item => item === 'one')).toEqual(true);
expect(queryList.some(item => item === 'four')).toEqual(false);
});
if (getDOM().supportsDOMEvents()) {
describe('simple observable interface', () => {
it('should fire callbacks on change', fakeAsync(() => {
let fires = 0;
queryList.changes.subscribe({next: (_) => { fires += 1; }});
queryList.notifyOnChanges();
tick();
expect(fires).toEqual(1);
queryList.notifyOnChanges();
tick();
expect(fires).toEqual(2);
}));
it('should provides query list as an argument', fakeAsync(() => {
let recorded: any /** TODO #9100 */;
queryList.changes.subscribe({next: (v: any) => { recorded = v; }});
queryList.reset(['one']);
queryList.notifyOnChanges();
tick();
expect(recorded).toBe(queryList);
}));
});
}
});
}

View File

@ -0,0 +1,287 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, InjectionToken, Injector, Pipe, PipeTransform, Provider, Renderer2} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers';
export function main() {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
function declareTests({useJit}: {useJit: boolean}) {
// Place to put reproductions for regressions
describe('regressions', () => {
beforeEach(() => { TestBed.configureTestingModule({declarations: [MyComp1, PlatformPipe]}); });
describe('platform pipes', () => {
beforeEach(() => { TestBed.configureCompiler({useJit: useJit}); });
it('should overwrite them by custom pipes', () => {
TestBed.configureTestingModule({declarations: [CustomPipe]});
const template = '{{true | somePipe}}';
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('someCustomPipe');
});
});
describe('expressions', () => {
it('should evaluate conditional and boolean operators with right precedence - #8244', () => {
const template = `{{'red' + (true ? ' border' : '')}}`;
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('red border');
});
it('should evaluate conditional and unary operators with right precedence - #8235', () => {
const template = `{{!null?.length}}`;
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('true');
});
it('should only evaluate stateful pipes once - #10639', () => {
TestBed.configureTestingModule({declarations: [CountingPipe]});
const template = '{{(null|countingPipe)?.value}}';
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
CountingPipe.reset();
fixture.detectChanges(/* checkNoChanges */ false);
expect(fixture.nativeElement).toHaveText('counting pipe value');
expect(CountingPipe.calls).toBe(1);
});
it('should only evaluate methods once - #10639', () => {
TestBed.configureTestingModule({declarations: [MyCountingComp]});
const template = '{{method()?.value}}';
TestBed.overrideComponent(MyCountingComp, {set: {template}});
const fixture = TestBed.createComponent(MyCountingComp);
MyCountingComp.reset();
fixture.detectChanges(/* checkNoChanges */ false);
expect(fixture.nativeElement).toHaveText('counting method value');
expect(MyCountingComp.calls).toBe(1);
});
it('should evalute a conditional in a statement binding', () => {
@Component({selector: 'some-comp', template: '<p (click)="nullValue?.click()"></p>'})
class SomeComponent {
nullValue: SomeReferencedClass;
}
class SomeReferencedClass {
click() {}
}
expect(() => {
const fixture = TestBed.configureTestingModule({declarations: [SomeComponent]})
.createComponent(SomeComponent);
fixture.detectChanges(/* checkNoChanges */ false);
}).not.toThrow();
});
});
describe('providers', () => {
function createInjector(providers: Provider[]): Injector {
TestBed.overrideComponent(MyComp1, {add: {providers}});
return TestBed.createComponent(MyComp1).componentInstance.injector;
}
it('should support providers with an InjectionToken that contains a `.` in the name', () => {
const token = new InjectionToken('a.b');
const tokenValue = 1;
const injector = createInjector([{provide: token, useValue: tokenValue}]);
expect(injector.get(token)).toEqual(tokenValue);
});
it('should support providers with string token with a `.` in it', () => {
const token = 'a.b';
const tokenValue = 1;
const injector = createInjector([{provide: token, useValue: tokenValue}]);
expect(injector.get(token)).toEqual(tokenValue);
});
it('should support providers with an anonymous function as token', () => {
const token = () => true;
const tokenValue = 1;
const injector = createInjector([{provide: token, useValue: tokenValue}]);
expect(injector.get(token)).toEqual(tokenValue);
});
it('should support providers with an InjectionToken that has a StringMap as value', () => {
const token1 = new InjectionToken('someToken');
const token2 = new InjectionToken('someToken');
const tokenValue1 = {'a': 1};
const tokenValue2 = {'a': 1};
const injector = createInjector(
[{provide: token1, useValue: tokenValue1}, {provide: token2, useValue: tokenValue2}]);
expect(injector.get(token1)).toEqual(tokenValue1);
expect(injector.get(token2)).toEqual(tokenValue2);
});
it('should support providers that have a `name` property with a number value', () => {
class TestClass {
constructor(public name: number) {}
}
const data = [new TestClass(1), new TestClass(2)];
const injector = createInjector([{provide: 'someToken', useValue: data}]);
expect(injector.get('someToken')).toEqual(data);
});
describe('ANALYZE_FOR_ENTRY_COMPONENTS providers', () => {
it('should support class instances', () => {
class SomeObject {
someMethod() {}
}
expect(
() => createInjector([
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: new SomeObject(), multi: true}
]))
.not.toThrow();
});
});
});
it('should allow logging a previous elements class binding via interpolation', () => {
const template = `<div [class.a]="true" #el>Class: {{el.className}}</div>`;
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Class: a');
});
it('should support ngClass before a component and content projection inside of an ngIf', () => {
TestBed.configureTestingModule({declarations: [CmpWithNgContent]});
const template = `A<cmp-content *ngIf="true" [ngClass]="'red'">B</cmp-content>C`;
TestBed.overrideComponent(MyComp1, {set: {template}});
const fixture = TestBed.createComponent(MyComp1);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('ABC');
});
it('should handle mutual recursion entered from multiple sides - #7084', () => {
TestBed.configureTestingModule({declarations: [FakeRecursiveComp, LeftComp, RightComp]});
const fixture = TestBed.createComponent(FakeRecursiveComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('[]');
});
it('should generate the correct output when constructors have the same name', () => {
function ComponentFactory(selector: string, template: string) {
@Component({selector, template})
class MyComponent {
}
return MyComponent;
}
const HeroComponent = ComponentFactory('my-hero', 'my hero');
const VillianComponent = ComponentFactory('a-villian', 'a villian');
const MainComponent = ComponentFactory(
'my-app', 'I was saved by <my-hero></my-hero> from <a-villian></a-villian>.');
TestBed.configureTestingModule(
{declarations: [HeroComponent, VillianComponent, MainComponent]});
const fixture = TestBed.createComponent(MainComponent);
expect(fixture.nativeElement).toHaveText('I was saved by my hero from a villian.');
});
it('should allow to use the renderer outside of views', () => {
@Component({template: ''})
class MyComp {
constructor(public renderer: Renderer2) {}
}
TestBed.configureTestingModule({declarations: [MyComp]});
const ctx = TestBed.createComponent(MyComp);
const txtNode = ctx.componentInstance.renderer.createText('test');
expect(txtNode).toHaveText('test');
});
});
}
@Component({selector: 'my-comp', template: ''})
class MyComp1 {
constructor(public injector: Injector) {}
}
@Pipe({name: 'somePipe', pure: true})
class PlatformPipe implements PipeTransform {
transform(value: any): any { return 'somePlatformPipe'; }
}
@Pipe({name: 'somePipe', pure: true})
class CustomPipe implements PipeTransform {
transform(value: any): any { return 'someCustomPipe'; }
}
@Component({selector: 'cmp-content', template: `<ng-content></ng-content>`})
class CmpWithNgContent {
}
@Component({selector: 'counting-cmp', template: ''})
class MyCountingComp {
method(): {value: string}|undefined {
MyCountingComp.calls++;
return {value: 'counting method value'};
}
static reset() { MyCountingComp.calls = 0; }
static calls = 0;
}
@Pipe({name: 'countingPipe'})
class CountingPipe implements PipeTransform {
transform(value: any): any {
CountingPipe.calls++;
return {value: 'counting pipe value'};
}
static reset() { CountingPipe.calls = 0; }
static calls = 0;
}
@Component({
selector: 'left',
template: `L<right *ngIf="false"></right>`,
})
class LeftComp {
}
@Component({
selector: 'right',
template: `R<left *ngIf="false"></left>`,
})
class RightComp {
}
@Component({
selector: 'fakeRecursiveComp',
template: `[<left *ngIf="false"></left><right *ngIf="false"></right>]`,
})
export class FakeRecursiveComp {
}

View File

@ -0,0 +1,252 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, HostBinding, Input, NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed, getTestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DomSanitizer} from '@angular/platform-browser/src/security/dom_sanitization_service';
export function main() {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
@Component({selector: 'my-comp', template: ''})
class SecuredComponent {
ctxProp: any = 'some value';
}
@Directive({selector: '[onPrefixedProp]'})
class OnPrefixDir {
@Input() onPrefixedProp: any;
@Input() onclick: any;
}
function declareTests({useJit}: {useJit: boolean}) {
describe('security integration tests', function() {
beforeEach(() => {
TestBed.configureCompiler({useJit: useJit}).configureTestingModule({
declarations: [
SecuredComponent,
OnPrefixDir,
]
});
});
let originalLog: (msg: any) => any;
beforeEach(() => {
originalLog = getDOM().log;
getDOM().log = (msg) => { /* disable logging */ };
});
afterEach(() => { getDOM().log = originalLog; });
describe('events', () => {
it('should disallow binding to attr.on*', () => {
const template = `<div [attr.onclick]="ctxProp"></div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
expect(() => TestBed.createComponent(SecuredComponent))
.toThrowError(
/Binding to event attribute 'onclick' is disallowed for security reasons, please use \(click\)=.../);
});
it('should disallow binding to on* with NO_ERRORS_SCHEMA', () => {
const template = `<div [onclick]="ctxProp"></div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}}).configureTestingModule({
schemas: [NO_ERRORS_SCHEMA]
});
expect(() => TestBed.createComponent(SecuredComponent))
.toThrowError(
/Binding to event property 'onclick' is disallowed for security reasons, please use \(click\)=.../);
});
it('should disallow binding to on* unless it is consumed by a directive', () => {
const template = `<div [onPrefixedProp]="ctxProp" [onclick]="ctxProp"></div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}}).configureTestingModule({
schemas: [NO_ERRORS_SCHEMA]
});
// should not throw for inputs starting with "on"
let cmp: ComponentFixture<SecuredComponent>;
expect(() => cmp = TestBed.createComponent(SecuredComponent)).not.toThrow();
// must bind to the directive not to the property of the div
const value = cmp.componentInstance.ctxProp = {};
cmp.detectChanges();
const div = cmp.debugElement.children[0];
expect(div.injector.get(OnPrefixDir).onclick).toBe(value);
expect(getDOM().getProperty(div.nativeElement, 'onclick')).not.toBe(value);
expect(getDOM().hasAttribute(div.nativeElement, 'onclick')).toEqual(false);
});
});
describe('safe HTML values', function() {
it('should not escape values marked as trusted', () => {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer);
const e = fixture.debugElement.children[0].nativeElement;
const ci = fixture.componentInstance;
const trusted = sanitizer.bypassSecurityTrustUrl('javascript:alert(1)');
ci.ctxProp = trusted;
fixture.detectChanges();
expect(getDOM().getProperty(e, 'href')).toEqual('javascript:alert(1)');
});
it('should error when using the wrong trusted value', () => {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer);
const trusted = sanitizer.bypassSecurityTrustScript('javascript:alert(1)');
const ci = fixture.componentInstance;
ci.ctxProp = trusted;
expect(() => fixture.detectChanges()).toThrowError(/Required a safe URL, got a Script/);
});
it('should warn when using in string interpolation', () => {
const template = `<a href="/foo/{{ctxProp}}">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer);
const e = fixture.debugElement.children[0].nativeElement;
const trusted = sanitizer.bypassSecurityTrustUrl('bar/baz');
const ci = fixture.componentInstance;
ci.ctxProp = trusted;
fixture.detectChanges();
expect(getDOM().getProperty(e, 'href')).toMatch(/SafeValue(%20| )must(%20| )use/);
});
});
describe('sanitizing', () => {
function checkEscapeOfHrefProperty(fixture: ComponentFixture<any>, isAttribute: boolean) {
const e = fixture.debugElement.children[0].nativeElement;
const ci = fixture.componentInstance;
ci.ctxProp = 'hello';
fixture.detectChanges();
// In the browser, reading href returns an absolute URL. On the server side,
// it just echoes back the property.
let value =
isAttribute ? getDOM().getAttribute(e, 'href') : getDOM().getProperty(e, 'href');
expect(value).toMatch(/.*\/?hello$/);
ci.ctxProp = 'javascript:alert(1)';
fixture.detectChanges();
value = isAttribute ? getDOM().getAttribute(e, 'href') : getDOM().getProperty(e, 'href');
expect(value).toEqual('unsafe:javascript:alert(1)');
}
it('should escape unsafe properties', () => {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, false);
});
it('should escape unsafe attributes', () => {
const template = `<a [attr.href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, true);
});
it('should escape unsafe properties if they are used in host bindings', () => {
@Directive({selector: '[dirHref]'})
class HrefDirective {
@HostBinding('href') @Input()
dirHref: string;
}
const template = `<a [dirHref]="ctxProp">Link Title</a>`;
TestBed.configureTestingModule({declarations: [HrefDirective]});
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, false);
});
it('should escape unsafe attributes if they are used in host bindings', () => {
@Directive({selector: '[dirHref]'})
class HrefDirective {
@HostBinding('attr.href') @Input()
dirHref: string;
}
const template = `<a [dirHref]="ctxProp">Link Title</a>`;
TestBed.configureTestingModule({declarations: [HrefDirective]});
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, true);
});
it('should escape unsafe style values', () => {
const template = `<div [style.background]="ctxProp">Text</div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
const e = fixture.debugElement.children[0].nativeElement;
const ci = fixture.componentInstance;
// Make sure binding harmless values works.
ci.ctxProp = 'red';
fixture.detectChanges();
// In some browsers, this will contain the full background specification, not just
// the color.
expect(getDOM().getStyle(e, 'background')).toMatch(/red.*/);
ci.ctxProp = 'url(javascript:evil())';
fixture.detectChanges();
// Updated value gets rejected, no value change.
expect(getDOM().getStyle(e, 'background')).not.toContain('javascript');
});
it('should escape unsafe SVG attributes', () => {
const template = `<svg:circle [xlink:href]="ctxProp">Text</svg:circle>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
expect(() => TestBed.createComponent(SecuredComponent))
.toThrowError(/Can't bind to 'xlink:href'/);
});
it('should escape unsafe HTML values', () => {
const template = `<div [innerHTML]="ctxProp">Text</div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
const e = fixture.debugElement.children[0].nativeElement;
const ci = fixture.componentInstance;
// Make sure binding harmless values works.
ci.ctxProp = 'some <p>text</p>';
fixture.detectChanges();
expect(getDOM().getInnerHTML(e)).toEqual('some <p>text</p>');
ci.ctxProp = 'ha <script>evil()</script>';
fixture.detectChanges();
expect(getDOM().getInnerHTML(e)).toEqual('ha evil()');
ci.ctxProp = 'also <img src="x" onerror="evil()"> evil';
fixture.detectChanges();
expect(getDOM().getInnerHTML(e)).toEqual('also <img src="x"> evil');
ci.ctxProp = 'also <iframe srcdoc="evil"></iframe> evil';
fixture.detectChanges();
expect(getDOM().getInnerHTML(e)).toEqual('also evil');
});
});
});
}

View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, SystemJsNgModuleLoader} from '@angular/core';
import {global} from '@angular/core/src/util';
import {async} from '@angular/core/testing';
import {afterEach, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
function mockSystem(modules: {[module: string]: any}) {
return {
'import': (target: string) => {
expect(modules[target]).not.toBe(undefined);
return Promise.resolve(modules[target]);
}
};
}
export function main() {
describe('SystemJsNgModuleLoader', () => {
let oldSystem: any = null;
beforeEach(() => {
oldSystem = global['System'];
global['System'] = mockSystem({
'test.ngfactory':
{'default': 'test module factory', 'NamedNgFactory': 'test NamedNgFactory'},
'prefixed/test/suffixed': {'NamedNgFactory': 'test module factory'}
});
});
afterEach(() => { global['System'] = oldSystem; });
it('loads a default factory by appending the factory suffix', async(() => {
const loader = new SystemJsNgModuleLoader(new Compiler());
loader.load('test').then(contents => { expect(contents).toBe('test module factory'); });
}));
it('loads a named factory by appending the factory suffix', async(() => {
const loader = new SystemJsNgModuleLoader(new Compiler());
loader.load('test#Named').then(contents => {
expect(contents).toBe('test NamedNgFactory');
});
}));
it('loads a named factory with a configured prefix and suffix', async(() => {
const loader = new SystemJsNgModuleLoader(new Compiler(), {
factoryPathPrefix: 'prefixed/',
factoryPathSuffix: '/suffixed',
});
loader.load('test#Named').then(contents => {
expect(contents).toBe('test module factory');
});
}));
});
};

View File

@ -0,0 +1,732 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Input, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/matchers';
@Directive({selector: '[simpleDirective]'})
class SimpleDirective {
@Input('simpleDirective') value: any = null;
}
@Component({selector: '[simpleComponent]', template: ''})
class SimpleComponent {
}
@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: any) { this.service = service; }
}
@Directive({selector: '[needsAppService]'})
class NeedsAppService {
service: any;
constructor(@Inject('appService') service: any) { this.service = service; }
}
@Component({selector: '[needsHostAppService]', template: ''})
class NeedsHostAppService {
service: any;
constructor(@Host() @Inject('appService') service: any) { this.service = service; }
}
@Component({selector: '[needsServiceComponent]', template: ''})
class NeedsServiceComponent {
service: any;
constructor(@Inject('service') service: any) { this.service = service; }
}
@Directive({selector: '[needsServiceFromHost]'})
class NeedsServiceFromHost {
service: any;
constructor(@Host() @Inject('service') service: any) { this.service = service; }
}
@Directive({selector: '[needsAttribute]'})
class NeedsAttribute {
typeAttribute: any;
titleAttribute: any;
fooAttribute: any;
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 {
constructor(@Attribute('foo') public fooAttribute: any) {}
}
@Directive({selector: '[needsElementRef]'})
class NeedsElementRef {
constructor(public elementRef: ElementRef) {}
}
@Directive({selector: '[needsViewContainerRef]'})
class NeedsViewContainerRef {
constructor(public viewContainer: ViewContainerRef) {}
}
@Directive({selector: '[needsTemplateRef]'})
class NeedsTemplateRef {
constructor(public templateRef: TemplateRef<Object>) {}
}
@Directive({selector: '[optionallyNeedsTemplateRef]'})
class OptionallyNeedsTemplateRef {
constructor(@Optional() public templateRef: TemplateRef<Object>) {}
}
@Directive({selector: '[directiveNeedsChangeDetectorRef]'})
class DirectiveNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Component({
selector: '[componentNeedsChangeDetectorRef]',
template: '{{counter}}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class PushComponentNeedsChangeDetectorRef {
counter: number = 0;
constructor(public changeDetectorRef: ChangeDetectorRef) {}
}
@Pipe({name: 'purePipe', pure: true})
class PurePipe implements PipeTransform {
constructor() {}
transform(value: any): any { return this; }
}
@Pipe({name: 'impurePipe', pure: false})
class ImpurePipe implements PipeTransform {
constructor() {}
transform(value: any): any { return this; }
}
@Pipe({name: 'pipeNeedsChangeDetectorRef'})
class PipeNeedsChangeDetectorRef {
constructor(public changeDetectorRef: ChangeDetectorRef) {}
transform(value: any): any { return this; }
}
@Pipe({name: 'pipeNeedsService'})
export class PipeNeedsService implements PipeTransform {
service: any;
constructor(@Inject('service') service: any) { this.service = service; }
transform(value: any): any { return this; }
}
@Pipe({name: 'duplicatePipe'})
export class DuplicatePipe1 implements PipeTransform {
transform(value: any): any { return this; }
}
@Pipe({name: 'duplicatePipe'})
export class DuplicatePipe2 implements PipeTransform {
transform(value: any): any { return this; }
}
@Component({selector: 'root', template: ''})
class TestComp {
}
export function main() {
function createComponentFixture<T>(
template: string, providers: Provider[] = null, comp: Type<T> = null): ComponentFixture<T> {
if (!comp) {
comp = <any>TestComp;
}
TestBed.overrideComponent(comp, {set: {template}});
if (providers && providers.length) {
TestBed.overrideComponent(comp, {add: {providers: providers}});
}
return TestBed.createComponent(comp);
}
function createComponent(
template: string, providers: Provider[] = null, comp: Type<any> = null): DebugElement {
const fixture = createComponentFixture(template, providers, comp);
fixture.detectChanges();
return fixture.debugElement;
}
describe('View injector', () => {
// On CJS fakeAsync is not supported...
if (!getDOM().supportsDOMEvents()) return;
const TOKEN = new InjectionToken<string>('token');
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComp],
providers: [
{provide: TOKEN, useValue: 'appService'},
{provide: 'appService', useFactory: (v: string) => v, deps: [TOKEN]},
],
});
});
describe('injection', () => {
it('should instantiate directives that have no dependencies', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective]});
const el = createComponent('<div simpleDirective>');
expect(el.children[0].injector.get(SimpleDirective)).toBeAnInstanceOf(SimpleDirective);
});
it('should instantiate directives that depend on another directive', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, NeedsDirective]});
const el = createComponent('<div simpleDirective needsDirective>');
const d = el.children[0].injector.get(NeedsDirective);
expect(d).toBeAnInstanceOf(NeedsDirective);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
});
it('should support useValue with different values', () => {
const el = createComponent('', [
{provide: 'numLiteral', useValue: 0},
{provide: 'boolLiteral', useValue: true},
{provide: 'strLiteral', useValue: 'a'},
{provide: 'null', useValue: null},
{provide: 'array', useValue: [1]},
{provide: 'map', useValue: {'a': 1}},
{provide: 'instance', useValue: new TestValue('a')},
{provide: 'nested', useValue: [{'a': [1]}, new TestValue('b')]},
]);
expect(el.injector.get('numLiteral')).toBe(0);
expect(el.injector.get('boolLiteral')).toBe(true);
expect(el.injector.get('strLiteral')).toBe('a');
expect(el.injector.get('null')).toBe(null);
expect(el.injector.get('array')).toEqual([1]);
expect(el.injector.get('map')).toEqual({'a': 1});
expect(el.injector.get('instance')).toEqual(new TestValue('a'));
expect(el.injector.get('nested')).toEqual([{'a': [1]}, new TestValue('b')]);
});
it('should instantiate providers that have dependencies with SkipSelf', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, SomeOtherDirective]});
TestBed.overrideDirective(
SimpleDirective,
{add: {providers: [{provide: 'injectable1', useValue: 'injectable1'}]}});
TestBed.overrideDirective(SomeOtherDirective, {
add: {
providers: [
{provide: 'injectable1', useValue: 'new-injectable1'}, {
provide: 'injectable2',
useFactory: (val: any) => `${val}-injectable2`,
deps: [[new Inject('injectable1'), new SkipSelf()]]
}
]
}
});
const el = createComponent('<div simpleDirective><span someOtherDirective></span></div>');
expect(el.children[0].children[0].injector.get('injectable2'))
.toEqual('injectable1-injectable2');
});
it('should instantiate providers that have dependencies', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective]});
const providers = [
{provide: 'injectable1', useValue: 'injectable1'}, {
provide: 'injectable2',
useFactory: (val: any) => `${val}-injectable2`,
deps: ['injectable1']
}
];
TestBed.overrideDirective(SimpleDirective, {add: {providers}});
const el = createComponent('<div simpleDirective></div>');
expect(el.children[0].injector.get('injectable2')).toEqual('injectable1-injectable2');
});
it('should instantiate viewProviders that have dependencies', () => {
TestBed.configureTestingModule({declarations: [SimpleComponent]});
const viewProviders = [
{provide: 'injectable1', useValue: 'injectable1'}, {
provide: 'injectable2',
useFactory: (val: any) => `${val}-injectable2`,
deps: ['injectable1']
}
];
TestBed.overrideComponent(SimpleComponent, {set: {viewProviders}});
const el = createComponent('<div simpleComponent></div>');
expect(el.children[0].injector.get('injectable2')).toEqual('injectable1-injectable2');
});
it('should instantiate components that depend on viewProviders providers', () => {
TestBed.configureTestingModule({declarations: [NeedsServiceComponent]});
TestBed.overrideComponent(
NeedsServiceComponent, {set: {providers: [{provide: 'service', useValue: 'service'}]}});
const el = createComponent('<div needsServiceComponent></div>');
expect(el.children[0].injector.get(NeedsServiceComponent).service).toEqual('service');
});
it('should instantiate multi providers', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective]});
const providers = [
{provide: 'injectable1', useValue: 'injectable11', multi: true},
{provide: 'injectable1', useValue: 'injectable12', multi: true}
];
TestBed.overrideDirective(SimpleDirective, {set: {providers}});
const el = createComponent('<div simpleDirective></div>');
expect(el.children[0].injector.get('injectable1')).toEqual([
'injectable11', 'injectable12'
]);
});
it('should instantiate providers lazily', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective]});
let created = false;
TestBed.overrideDirective(
SimpleDirective,
{set: {providers: [{provide: 'service', useFactory: () => created = true}]}});
const el = createComponent('<div simpleDirective></div>');
expect(created).toBe(false);
el.children[0].injector.get('service');
expect(created).toBe(true);
});
it('should instantiate providers lazily', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective]});
let created = false;
TestBed.overrideDirective(
SimpleDirective,
{set: {providers: [{provide: 'service', useFactory: () => created = true}]}});
const el = createComponent('<div simpleDirective></div>');
expect(created).toBe(false);
el.children[0].injector.get('service');
expect(created).toBe(true);
});
it('should instantiate providers with a lifecycle hook eagerly', () => {
let created = false;
class SomeInjectable {
constructor() { created = true; }
ngOnDestroy() {}
}
TestBed.configureTestingModule({declarations: [SimpleDirective]});
TestBed.overrideDirective(SimpleDirective, {set: {providers: [SomeInjectable]}});
const el = createComponent('<div simpleDirective></div>');
expect(created).toBe(true);
});
it('should instantiate view providers lazily', () => {
let created = false;
TestBed.configureTestingModule({declarations: [SimpleComponent]});
TestBed.overrideComponent(
SimpleComponent,
{set: {viewProviders: [{provide: 'service', useFactory: () => created = true}]}});
const el = createComponent('<div simpleComponent></div>');
expect(created).toBe(false);
el.children[0].injector.get('service');
expect(created).toBe(true);
});
it('should not instantiate other directives that depend on viewProviders providers (same element)',
() => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsService]});
TestBed.overrideComponent(
SimpleComponent,
{set: {viewProviders: [{provide: 'service', useValue: 'service'}]}});
expect(() => createComponent('<div simpleComponent needsService></div>'))
.toThrowError(/No provider for service!/);
});
it('should not instantiate other directives that depend on viewProviders providers (child element)',
() => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsService]});
TestBed.overrideComponent(
SimpleComponent,
{set: {viewProviders: [{provide: 'service', useValue: 'service'}]}});
expect(() => createComponent('<div simpleComponent><div needsService></div></div>'))
.toThrowError(/No provider for service!/);
});
it('should instantiate directives that depend on providers of other directives', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, NeedsService]});
TestBed.overrideDirective(
SimpleDirective, {set: {providers: [{provide: 'service', useValue: 'parentService'}]}});
const el = createComponent('<div simpleDirective><div needsService></div></div>');
expect(el.children[0].children[0].injector.get(NeedsService).service)
.toEqual('parentService');
});
it('should instantiate directives that depend on providers in a parent view', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, NeedsService]});
TestBed.overrideDirective(
SimpleDirective, {set: {providers: [{provide: 'service', useValue: 'parentService'}]}});
const el = createComponent(
'<div simpleDirective><ng-container *ngIf="true"><div *ngIf="true" needsService></div></ng-container></div>');
expect(el.children[0].children[0].injector.get(NeedsService).service)
.toEqual('parentService');
});
it('should instantiate directives that depend on providers of a component', () => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsService]});
TestBed.overrideComponent(
SimpleComponent, {set: {providers: [{provide: 'service', useValue: 'hostService'}]}});
TestBed.overrideComponent(SimpleComponent, {set: {template: '<div needsService></div>'}});
const el = createComponent('<div simpleComponent></div>');
expect(el.children[0].children[0].injector.get(NeedsService).service)
.toEqual('hostService');
});
it('should instantiate directives that depend on view providers of a component', () => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsService]});
TestBed.overrideComponent(
SimpleComponent, {set: {providers: [{provide: 'service', useValue: 'hostService'}]}});
TestBed.overrideComponent(SimpleComponent, {set: {template: '<div needsService></div>'}});
const el = createComponent('<div simpleComponent></div>');
expect(el.children[0].children[0].injector.get(NeedsService).service)
.toEqual('hostService');
});
it('should instantiate directives in a root embedded view that depend on view providers of a component',
() => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsService]});
TestBed.overrideComponent(
SimpleComponent,
{set: {providers: [{provide: 'service', useValue: 'hostService'}]}});
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div *ngIf="true" needsService></div>'}});
const el = createComponent('<div simpleComponent></div>');
expect(el.children[0].children[0].injector.get(NeedsService).service)
.toEqual('hostService');
});
it('should instantiate directives that depend on instances in the app injector', () => {
TestBed.configureTestingModule({declarations: [NeedsAppService]});
const el = createComponent('<div needsAppService></div>');
expect(el.children[0].injector.get(NeedsAppService).service).toEqual('appService');
});
it('should not instantiate a directive with cyclic dependencies', () => {
TestBed.configureTestingModule({declarations: [CycleDirective]});
expect(() => createComponent('<div cycleDirective></div>'))
.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',
() => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsServiceFromHost]});
TestBed.overrideComponent(
SimpleComponent,
{set: {providers: [{provide: 'service', useValue: 'hostService'}]}});
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div needsServiceFromHost><div>'}});
expect(() => createComponent('<div simpleComponent></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',
() => {
TestBed.configureTestingModule(
{declarations: [SimpleComponent, SomeOtherDirective, NeedsServiceFromHost]});
TestBed.overrideComponent(
SimpleComponent,
{set: {providers: [{provide: 'service', useValue: 'hostService'}]}});
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div needsServiceFromHost><div>'}});
expect(() => createComponent('<div simpleComponent someOtherDirective></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',
() => {
TestBed.configureTestingModule(
{declarations: [SimpleDirective, NeedsDirectiveFromSelf]});
expect(
() =>
createComponent('<div simpleDirective><div needsDirectiveFromSelf></div></div>'))
.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(() => {
TestBed.configureTestingModule({declarations: [SimpleDirective, NeedsDirective]});
const el = createComponent('<div simpleDirective><div needsDirective></div></div>');
const d = el.children[0].children[0].injector.get(NeedsDirective);
expect(d).toBeAnInstanceOf(NeedsDirective);
expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
}));
it('should throw when a dependency cannot be resolved', fakeAsync(() => {
TestBed.configureTestingModule({declarations: [NeedsService]});
expect(() => createComponent('<div needsService></div>'))
.toThrowError(/No provider for service!/);
}));
it('should inject null when an optional dependency cannot be resolved', () => {
TestBed.configureTestingModule({declarations: [OptionallyNeedsDirective]});
const el = createComponent('<div optionallyNeedsDirective></div>');
const d = el.children[0].injector.get(OptionallyNeedsDirective);
expect(d.dependency).toEqual(null);
});
it('should instantiate directives that depends on the host component', () => {
TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsComponentFromHost]});
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div needsComponentFromHost></div>'}});
const el = createComponent('<div simpleComponent></div>');
const d = el.children[0].children[0].injector.get(NeedsComponentFromHost);
expect(d.dependency).toBeAnInstanceOf(SimpleComponent);
});
it('should instantiate host views for components that have a @Host dependency ', () => {
TestBed.configureTestingModule({declarations: [NeedsHostAppService]});
const el = createComponent('', [], NeedsHostAppService);
expect(el.componentInstance.service).toEqual('appService');
});
it('should not instantiate directives that depend on other directives on the host element', () => {
TestBed.configureTestingModule(
{declarations: [SimpleComponent, SimpleDirective, NeedsDirectiveFromHost]});
TestBed.overrideComponent(
SimpleComponent, {set: {template: '<div needsDirectiveFromHost></div>'}});
expect(() => createComponent('<div simpleComponent simpleDirective></div>'))
.toThrowError(
`Template parse errors:\nNo provider for SimpleDirective ("[ERROR ->]<div needsDirectiveFromHost></div>"): SimpleComponent@0:0`);
});
});
describe('static attributes', () => {
it('should be injectable', () => {
TestBed.configureTestingModule({declarations: [NeedsAttribute]});
const el = createComponent('<div needsAttribute type="text" title></div>');
const needsAttribute = el.children[0].injector.get(NeedsAttribute);
expect(needsAttribute.typeAttribute).toEqual('text');
expect(needsAttribute.titleAttribute).toEqual('');
expect(needsAttribute.fooAttribute).toEqual(null);
});
it('should be injectable without type annotation', () => {
TestBed.configureTestingModule({declarations: [NeedsAttributeNoType]});
const el = createComponent('<div needsAttributeNoType foo="bar"></div>');
const needsAttribute = el.children[0].injector.get(NeedsAttributeNoType);
expect(needsAttribute.fooAttribute).toEqual('bar');
});
});
describe('refs', () => {
it('should inject ElementRef', () => {
TestBed.configureTestingModule({declarations: [NeedsElementRef]});
const el = createComponent('<div needsElementRef></div>');
expect(el.children[0].injector.get(NeedsElementRef).elementRef.nativeElement)
.toBe(el.children[0].nativeElement);
});
it('should inject ChangeDetectorRef of the component\'s view into the component', () => {
TestBed.configureTestingModule({declarations: [PushComponentNeedsChangeDetectorRef]});
const cf = createComponentFixture('<div componentNeedsChangeDetectorRef></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp = compEl.injector.get(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', () => {
TestBed.configureTestingModule(
{declarations: [PushComponentNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef]});
TestBed.overrideComponent(PushComponentNeedsChangeDetectorRef, {
set: {
template:
'{{counter}}<div directiveNeedsChangeDetectorRef></div><div *ngIf="true" directiveNeedsChangeDetectorRef></div>'
}
});
const cf = createComponentFixture('<div componentNeedsChangeDetectorRef></div>');
cf.detectChanges();
const compEl = cf.debugElement.children[0];
const comp: PushComponentNeedsChangeDetectorRef =
compEl.injector.get(PushComponentNeedsChangeDetectorRef);
comp.counter = 1;
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('0');
expect(compEl.children[0].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef)
.toEqual(comp.changeDetectorRef);
expect(compEl.children[1].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef)
.toEqual(comp.changeDetectorRef);
comp.changeDetectorRef.markForCheck();
cf.detectChanges();
expect(compEl.nativeElement).toHaveText('1');
});
it('should inject ViewContainerRef', () => {
TestBed.configureTestingModule({declarations: [NeedsViewContainerRef]});
const el = createComponent('<div needsViewContainerRef></div>');
expect(
el.children[0].injector.get(NeedsViewContainerRef).viewContainer.element.nativeElement)
.toBe(el.children[0].nativeElement);
});
it('should inject TemplateRef', () => {
TestBed.configureTestingModule({declarations: [NeedsViewContainerRef, NeedsTemplateRef]});
const el =
createComponent('<ng-template needsViewContainerRef needsTemplateRef></ng-template>');
expect(el.childNodes[0].injector.get(NeedsTemplateRef).templateRef.elementRef)
.toEqual(el.childNodes[0].injector.get(NeedsViewContainerRef).viewContainer.element);
});
it('should throw if there is no TemplateRef', () => {
TestBed.configureTestingModule({declarations: [NeedsTemplateRef]});
expect(() => createComponent('<div needsTemplateRef></div>'))
.toThrowError(/No provider for TemplateRef!/);
});
it('should inject null if there is no TemplateRef when the dependency is optional', () => {
TestBed.configureTestingModule({declarations: [OptionallyNeedsTemplateRef]});
const el = createComponent('<div optionallyNeedsTemplateRef></div>');
const instance = el.children[0].injector.get(OptionallyNeedsTemplateRef);
expect(instance.templateRef).toBeNull();
});
});
describe('pipes', () => {
it('should instantiate pipes that have dependencies', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, PipeNeedsService]});
const el = createComponent(
'<div [simpleDirective]="true | pipeNeedsService"></div>',
[{provide: 'service', useValue: 'pipeService'}]);
expect(el.children[0].injector.get(SimpleDirective).value.service).toEqual('pipeService');
});
it('should overwrite pipes with later entry in the pipes array', () => {
TestBed.configureTestingModule(
{declarations: [SimpleDirective, DuplicatePipe1, DuplicatePipe2]});
const el = createComponent('<div [simpleDirective]="true | duplicatePipe"></div>');
expect(el.children[0].injector.get(SimpleDirective).value).toBeAnInstanceOf(DuplicatePipe2);
});
it('should inject ChangeDetectorRef into pipes', () => {
TestBed.configureTestingModule({
declarations:
[SimpleDirective, PipeNeedsChangeDetectorRef, DirectiveNeedsChangeDetectorRef]
});
const el = createComponent(
'<div [simpleDirective]="true | pipeNeedsChangeDetectorRef" directiveNeedsChangeDetectorRef></div>');
const cdRef =
el.children[0].injector.get(DirectiveNeedsChangeDetectorRef).changeDetectorRef;
expect(el.children[0].injector.get(SimpleDirective).value.changeDetectorRef).toEqual(cdRef);
});
it('should cache pure pipes', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, PurePipe]});
const el = createComponent(
'<div [simpleDirective]="true | purePipe"></div><div [simpleDirective]="true | purePipe"></div>' +
'<div *ngFor="let x of [1,2]" [simpleDirective]="true | purePipe"></div>');
const purePipe1 = el.children[0].injector.get(SimpleDirective).value;
const purePipe2 = el.children[1].injector.get(SimpleDirective).value;
const purePipe3 = el.children[2].injector.get(SimpleDirective).value;
const purePipe4 = el.children[3].injector.get(SimpleDirective).value;
expect(purePipe1).toBeAnInstanceOf(PurePipe);
expect(purePipe2).toBe(purePipe1);
expect(purePipe3).toBe(purePipe1);
expect(purePipe4).toBe(purePipe1);
});
it('should not cache impure pipes', () => {
TestBed.configureTestingModule({declarations: [SimpleDirective, ImpurePipe]});
const el = createComponent(
'<div [simpleDirective]="true | impurePipe"></div><div [simpleDirective]="true | impurePipe"></div>' +
'<div *ngFor="let x of [1,2]" [simpleDirective]="true | impurePipe"></div>');
const impurePipe1 = el.children[0].injector.get(SimpleDirective).value;
const impurePipe2 = el.children[1].injector.get(SimpleDirective).value;
const impurePipe3 = el.children[2].injector.get(SimpleDirective).value;
const impurePipe4 = el.children[3].injector.get(SimpleDirective).value;
expect(impurePipe1).toBeAnInstanceOf(ImpurePipe);
expect(impurePipe2).toBeAnInstanceOf(ImpurePipe);
expect(impurePipe2).not.toBe(impurePipe1);
expect(impurePipe3).toBeAnInstanceOf(ImpurePipe);
expect(impurePipe3).not.toBe(impurePipe1);
expect(impurePipe4).toBeAnInstanceOf(ImpurePipe);
expect(impurePipe4).not.toBe(impurePipe1);
});
});
});
}
class TestValue {
constructor(public value: string) {}
}

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive} from '@angular/core';
import {reflector} from '@angular/core/src/reflection/reflection';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
export function main() {
describe('es5 decorators', () => {
it('should declare directive class', () => {
const MyDirective = Directive({}).Class({constructor: function() { this.works = true; }});
expect(new MyDirective().works).toEqual(true);
});
it('should declare Component class', () => {
const MyComponent = Component({}).Class({constructor: function() { this.works = true; }});
expect(new MyComponent().works).toEqual(true);
});
it('should create type in ES5', () => {
class MyComponent {};
let as: any /** TODO #9100 */;
(<any>MyComponent).annotations = as = Component({});
expect(reflector.annotations(MyComponent)).toEqual(as.annotations);
});
});
}

View File

@ -0,0 +1,101 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, ElementRef, Input, NO_ERRORS_SCHEMA, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
export function main() {
describe('ViewChild', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ViewChildTypeSelectorComponent, ViewChildStringSelectorComponent, Simple],
schemas: [NO_ERRORS_SCHEMA],
});
});
it('should support type selector', () => {
TestBed.overrideComponent(
ViewChildTypeSelectorComponent,
{set: {template: `<simple [marker]="'1'"></simple><simple [marker]="'2'"></simple>`}});
const view = TestBed.createComponent(ViewChildTypeSelectorComponent);
view.detectChanges();
expect(view.componentInstance.child).toBeDefined();
expect(view.componentInstance.child.marker).toBe('1');
});
it('should support string selector', () => {
TestBed.overrideComponent(
ViewChildStringSelectorComponent, {set: {template: `<simple #child></simple>`}});
const view = TestBed.createComponent(ViewChildStringSelectorComponent);
view.detectChanges();
expect(view.componentInstance.child).toBeDefined();
});
});
describe('ViewChildren', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations:
[ViewChildrenTypeSelectorComponent, ViewChildrenStringSelectorComponent, Simple],
schemas: [NO_ERRORS_SCHEMA],
});
});
it('should support type selector', () => {
TestBed.overrideComponent(
ViewChildrenTypeSelectorComponent,
{set: {template: `<simple></simple><simple></simple>`}});
const view = TestBed.createComponent(ViewChildrenTypeSelectorComponent);
view.detectChanges();
expect(view.componentInstance.children).toBeDefined();
expect(view.componentInstance.children.length).toBe(2);
});
it('should support string selector', () => {
TestBed.overrideComponent(
ViewChildrenStringSelectorComponent,
{set: {template: `<simple #child1></simple><simple #child2></simple>`}});
const view = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(ViewChildrenStringSelectorComponent);
view.detectChanges();
expect(view.componentInstance.children).toBeDefined();
expect(view.componentInstance.children.length).toBe(2);
});
});
}
@Directive({selector: 'simple'})
class Simple {
@Input() marker: string;
}
@Component({selector: 'view-child-type-selector', template: ''})
class ViewChildTypeSelectorComponent {
@ViewChild(Simple) child: Simple;
}
@Component({selector: 'view-child-string-selector', template: ''})
class ViewChildStringSelectorComponent {
@ViewChild('child') child: ElementRef;
}
@Component({selector: 'view-children-type-selector', template: ''})
class ViewChildrenTypeSelectorComponent {
@ViewChildren(Simple) children: QueryList<Simple>;
}
@Component({selector: 'view-child-string-selector', template: ''})
class ViewChildrenStringSelectorComponent {
// Allow comma separated selector (with spaces).
@ViewChildren('child1 , child2') children: QueryList<ElementRef>;
}

View File

@ -0,0 +1,503 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Reflector} from '@angular/core/src/reflection/reflection';
import {DELEGATE_CTOR, ReflectionCapabilities} from '@angular/core/src/reflection/reflection_capabilities';
import {makeDecorator, makeParamDecorator, makePropDecorator} from '@angular/core/src/util/decorators';
interface ClassDecoratorFactory {
(data: ClassDecorator): any;
new (data: ClassDecorator): ClassDecorator;
}
interface ClassDecorator {
value: any;
}
interface ParamDecorator {
value: any;
}
interface PropDecorator {
value: any;
}
/** @Annotation */ const ClassDecorator =
<ClassDecoratorFactory>makeDecorator('ClassDecorator', {value: undefined});
/** @Annotation */ const ParamDecorator =
makeParamDecorator('ParamDecorator', [['value', undefined]]);
/** @Annotation */ const PropDecorator = makePropDecorator('PropDecorator', [['value', undefined]]);
class AType {
constructor(public value: any) {}
}
@ClassDecorator({value: 'class'})
class ClassWithDecorators {
@PropDecorator('p1') @PropDecorator('p2') a: AType;
b: AType;
@PropDecorator('p3')
set c(value: any) {}
@PropDecorator('p4')
someMethod() {}
constructor(@ParamDecorator('a') a: AType, @ParamDecorator('b') b: AType) {
this.a = a;
this.b = b;
}
}
class ClassWithoutDecorators {
constructor(a: any, b: any) {}
}
class TestObj {
constructor(public a: any, public b: any) {}
identity(arg: any) { return arg; }
}
export function main() {
describe('Reflector', () => {
let reflector: Reflector;
beforeEach(() => { reflector = new Reflector(new ReflectionCapabilities()); });
describe('factory', () => {
it('should create a factory for the given type', () => {
const obj = reflector.factory(TestObj)(1, 2);
expect(obj.a).toEqual(1);
expect(obj.b).toEqual(2);
});
});
describe('parameters', () => {
it('should return an array of parameters for a type', () => {
const p = reflector.parameters(ClassWithDecorators);
expect(p).toEqual([[AType, new ParamDecorator('a')], [AType, new ParamDecorator('b')]]);
});
it('should work for a class without annotations', () => {
const p = reflector.parameters(ClassWithoutDecorators);
expect(p.length).toEqual(2);
});
// See https://github.com/angular/tsickle/issues/261
it('should read forwardRef down-leveled type', () => {
class Dep {}
class ForwardLegacy {
constructor(d: Dep) {}
// Older tsickle had a bug: wrote a forward reference
static ctorParameters = [{type: Dep}];
}
expect(reflector.parameters(ForwardLegacy)).toEqual([[Dep]]);
class Forward {
constructor(d: Dep) {}
// Newer tsickle generates a functionClosure
static ctorParameters = () => [{type: ForwardDep}];
}
class ForwardDep {}
expect(reflector.parameters(Forward)).toEqual([[ForwardDep]]);
});
});
describe('propMetadata', () => {
it('should return a string map of prop metadata for the given class', () => {
const p = reflector.propMetadata(ClassWithDecorators);
expect(p['a']).toEqual([new PropDecorator('p1'), new PropDecorator('p2')]);
expect(p['c']).toEqual([new PropDecorator('p3')]);
expect(p['someMethod']).toEqual([new PropDecorator('p4')]);
});
it('should also return metadata if the class has no decorator', () => {
class Test {
@PropDecorator('test')
prop: any;
}
expect(reflector.propMetadata(Test)).toEqual({'prop': [new PropDecorator('test')]});
});
});
describe('annotations', () => {
it('should return an array of annotations for a type', () => {
const p = reflector.annotations(ClassWithDecorators);
expect(p).toEqual([new ClassDecorator({value: 'class'})]);
});
it('should work for a class without annotations', () => {
const p = reflector.annotations(ClassWithoutDecorators);
expect(p).toEqual([]);
});
});
describe('getter', () => {
it('returns a function reading a property', () => {
const getA = reflector.getter('a');
expect(getA(new TestObj(1, 2))).toEqual(1);
});
});
describe('setter', () => {
it('returns a function setting a property', () => {
const setA = reflector.setter('a');
const obj = new TestObj(1, 2);
setA(obj, 100);
expect(obj.a).toEqual(100);
});
});
describe('method', () => {
it('returns a function invoking a method', () => {
const func = reflector.method('identity');
const obj = new TestObj(1, 2);
expect(func(obj, ['value'])).toEqual('value');
});
});
describe('ctor inheritance detection', () => {
it('should use the right regex', () => {
class Parent {}
class ChildNoCtor extends Parent {}
class ChildWithCtor extends Parent {
constructor() { super(); }
}
expect(DELEGATE_CTOR.exec(ChildNoCtor.toString())).toBeTruthy();
expect(DELEGATE_CTOR.exec(ChildWithCtor.toString())).toBeFalsy();
});
});
describe('inheritance with decorators', () => {
it('should inherit annotations', () => {
@ClassDecorator({value: 'parent'})
class Parent {
}
@ClassDecorator({value: 'child'})
class Child extends Parent {
}
class ChildNoDecorators extends Parent {}
class NoDecorators {}
// Check that metadata for Parent was not changed!
expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]);
expect(reflector.annotations(Child)).toEqual([
new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'})
]);
expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator(
{value: 'parent'})]);
expect(reflector.annotations(NoDecorators)).toEqual([]);
expect(reflector.annotations(<any>{})).toEqual([]);
expect(reflector.annotations(<any>1)).toEqual([]);
expect(reflector.annotations(null)).toEqual([]);
});
it('should inherit parameters', () => {
class A {}
class B {}
class C {}
// Note: We need the class decorator as well,
// as otherwise TS won't capture the ctor arguments!
@ClassDecorator({value: 'parent'})
class Parent {
constructor(@ParamDecorator('a') a: A, @ParamDecorator('b') b: B) {}
}
class Child extends Parent {}
// Note: We need the class decorator as well,
// as otherwise TS won't capture the ctor arguments!
@ClassDecorator({value: 'child'})
class ChildWithCtor extends Parent {
constructor(@ParamDecorator('c') c: C) { super(null, null); }
}
class ChildWithCtorNoDecorator extends Parent {
constructor(a: any, b: any, c: any) { super(null, null); }
}
class NoDecorators {}
// Check that metadata for Parent was not changed!
expect(reflector.parameters(Parent)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(Child)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]);
// If we have no decorator, we don't get metadata about the ctor params.
// But we should still get an array of the right length based on function.length.
expect(reflector.parameters(ChildWithCtorNoDecorator)).toEqual([
undefined, undefined, undefined
]);
expect(reflector.parameters(NoDecorators)).toEqual([]);
expect(reflector.parameters(<any>{})).toEqual([]);
expect(reflector.parameters(<any>1)).toEqual([]);
expect(reflector.parameters(null)).toEqual([]);
});
it('should inherit property metadata', () => {
class A {}
class B {}
class C {}
class Parent {
@PropDecorator('a')
a: A;
@PropDecorator('b1')
b: B;
}
class Child extends Parent {
@PropDecorator('b2')
b: B;
@PropDecorator('c')
c: C;
}
class NoDecorators {}
// Check that metadata for Parent was not changed!
expect(reflector.propMetadata(Parent)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1')],
});
expect(reflector.propMetadata(Child)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1'), new PropDecorator('b2')],
'c': [new PropDecorator('c')]
});
expect(reflector.propMetadata(NoDecorators)).toEqual({});
expect(reflector.propMetadata(<any>{})).toEqual({});
expect(reflector.propMetadata(<any>1)).toEqual({});
expect(reflector.propMetadata(null)).toEqual({});
});
it('should inherit lifecycle hooks', () => {
class Parent {
hook1() {}
hook2() {}
}
class Child extends Parent {
hook2() {}
hook3() {}
}
function hooks(symbol: any, names: string[]): boolean[] {
return names.map(name => reflector.hasLifecycleHook(symbol, name));
}
// Check that metadata for Parent was not changed!
expect(hooks(Parent, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, false]);
expect(hooks(Child, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, true]);
});
});
describe('inheritance with tsickle', () => {
it('should inherit annotations', () => {
class Parent {
static decorators = [{type: ClassDecorator, args: [{value: 'parent'}]}];
}
class Child extends Parent {
static decorators = [{type: ClassDecorator, args: [{value: 'child'}]}];
}
class ChildNoDecorators extends Parent {}
// Check that metadata for Parent was not changed!
expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]);
expect(reflector.annotations(Child)).toEqual([
new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'})
]);
expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator(
{value: 'parent'})]);
});
it('should inherit parameters', () => {
class A {}
class B {}
class C {}
class Parent {
static ctorParameters = () =>
[{type: A, decorators: [{type: ParamDecorator, args: ['a']}]},
{type: B, decorators: [{type: ParamDecorator, args: ['b']}]},
]
}
class Child extends Parent {}
class ChildWithCtor extends Parent {
static ctorParameters =
() => [{type: C, decorators: [{type: ParamDecorator, args: ['c']}]}, ];
constructor() { super(); }
}
// Check that metadata for Parent was not changed!
expect(reflector.parameters(Parent)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(Child)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]);
});
it('should inherit property metadata', () => {
class A {}
class B {}
class C {}
class Parent {
static propDecorators: any = {
'a': [{type: PropDecorator, args: ['a']}],
'b': [{type: PropDecorator, args: ['b1']}],
};
}
class Child extends Parent {
static propDecorators: any = {
'b': [{type: PropDecorator, args: ['b2']}],
'c': [{type: PropDecorator, args: ['c']}],
};
}
// Check that metadata for Parent was not changed!
expect(reflector.propMetadata(Parent)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1')],
});
expect(reflector.propMetadata(Child)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1'), new PropDecorator('b2')],
'c': [new PropDecorator('c')]
});
});
});
describe('inheritance with es5 API', () => {
it('should inherit annotations', () => {
class Parent {
static annotations = [new ClassDecorator({value: 'parent'})];
}
class Child extends Parent {
static annotations = [new ClassDecorator({value: 'child'})];
}
class ChildNoDecorators extends Parent {}
// Check that metadata for Parent was not changed!
expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({value: 'parent'})]);
expect(reflector.annotations(Child)).toEqual([
new ClassDecorator({value: 'parent'}), new ClassDecorator({value: 'child'})
]);
expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator(
{value: 'parent'})]);
});
it('should inherit parameters', () => {
class A {}
class B {}
class C {}
class Parent {
static parameters = [
[A, new ParamDecorator('a')],
[B, new ParamDecorator('b')],
];
}
class Child extends Parent {}
class ChildWithCtor extends Parent {
static parameters = [
[C, new ParamDecorator('c')],
];
constructor() { super(); }
}
// Check that metadata for Parent was not changed!
expect(reflector.parameters(Parent)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(Child)).toEqual([
[A, new ParamDecorator('a')], [B, new ParamDecorator('b')]
]);
expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]);
});
it('should inherit property metadata', () => {
class A {}
class B {}
class C {}
class Parent {
static propMetadata: any = {
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1')],
};
}
class Child extends Parent {
static propMetadata: any = {
'b': [new PropDecorator('b2')],
'c': [new PropDecorator('c')],
};
}
// Check that metadata for Parent was not changed!
expect(reflector.propMetadata(Parent)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1')],
});
expect(reflector.propMetadata(Child)).toEqual({
'a': [new PropDecorator('a')],
'b': [new PropDecorator('b1'), new PropDecorator('b2')],
'c': [new PropDecorator('c')]
});
});
});
});
}

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef} from '@angular/core';
import {ChangeDetectorRef} from '@angular/core/src/change_detection/change_detection';
import {SpyObject} from '@angular/core/testing/testing_internal';
import {DomAdapter} from '@angular/platform-browser/src/dom/dom_adapter';
export class SpyChangeDetectorRef extends SpyObject {
constructor() {
super(ChangeDetectorRef);
this.spy('detectChanges');
this.spy('checkNoChanges');
}
}
export class SpyIterableDifferFactory extends SpyObject {}
export class SpyElementRef extends SpyObject {
constructor() { super(ElementRef); }
}
export class SpyDomAdapter extends SpyObject {
constructor() { super(DomAdapter); }
}

View File

@ -0,0 +1,285 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {EventEmitter} from '@angular/core';
import {Injectable} from '@angular/core/src/di';
import {Testability} from '@angular/core/src/testability/testability';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {AsyncTestCompleter, SpyObject, beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {scheduleMicroTask} from '../../src/util';
// Schedules a microtasks (using a resolved promise .then())
function microTask(fn: Function): void {
scheduleMicroTask(() => {
// We do double dispatch so that we can wait for scheduleMicrotask in the Testability when
// NgZone becomes stable.
scheduleMicroTask(fn);
});
}
@Injectable()
class MockNgZone extends NgZone {
/** @internal */
_onUnstableStream: EventEmitter<any>;
get onUnstable() { return this._onUnstableStream; }
/** @internal */
_onStableStream: EventEmitter<any>;
get onStable() { return this._onStableStream; }
constructor() {
super({enableLongStackTrace: false});
this._onUnstableStream = new EventEmitter(false);
this._onStableStream = new EventEmitter(false);
}
unstable(): void { this._onUnstableStream.emit(null); }
stable(): void { this._onStableStream.emit(null); }
}
export function main() {
describe('Testability', () => {
let testability: Testability;
let execute: any;
let execute2: any;
let ngZone: MockNgZone;
beforeEach(() => {
ngZone = new MockNgZone();
testability = new Testability(ngZone);
execute = new SpyObject().spy('execute');
execute2 = new SpyObject().spy('execute');
});
describe('Pending count logic', () => {
it('should start with a pending count of 0',
() => { expect(testability.getPendingRequestCount()).toEqual(0); });
it('should fire whenstable callbacks if pending count is 0',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
}));
it('should not fire whenstable callbacks synchronously if pending count is 0', () => {
testability.whenStable(execute);
expect(execute).not.toHaveBeenCalled();
});
it('should not call whenstable callbacks when there are pending counts',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
testability.increasePendingRequestCount();
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).not.toHaveBeenCalled();
async.done();
});
});
}));
it('should fire whenstable callbacks when pending drops to 0',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
});
}));
it('should not fire whenstable callbacks synchronously when pending drops to 0', () => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
testability.decreasePendingRequestCount();
expect(execute).not.toHaveBeenCalled();
});
it('should fire whenstable callbacks with didWork if pending count is 0',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalledWith(false);
async.done();
});
}));
it('should fire whenstable callbacks with didWork when pending drops to 0',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
});
}));
});
describe('NgZone callback logic', () => {
it('should fire whenstable callback if event is already finished',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
}));
it('should not fire whenstable callbacks synchronously if event is already finished', () => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
expect(execute).not.toHaveBeenCalled();
});
it('should fire whenstable callback when event finishes',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
});
}));
it('should not fire whenstable callbacks synchronously when event finishes', () => {
ngZone.unstable();
testability.whenStable(execute);
ngZone.stable();
expect(execute).not.toHaveBeenCalled();
});
it('should not fire whenstable callback when event did not finish',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
});
});
}));
it('should not fire whenstable callback when there are pending counts',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
testability.increasePendingRequestCount();
testability.increasePendingRequestCount();
testability.whenStable(execute);
microTask(() => {
expect(execute).not.toHaveBeenCalled();
ngZone.stable();
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).not.toHaveBeenCalled();
testability.decreasePendingRequestCount();
microTask(() => {
expect(execute).toHaveBeenCalled();
async.done();
});
});
});
});
}));
it('should fire whenstable callback with didWork if event is already finished',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
ngZone.stable();
testability.whenStable(execute);
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
}));
it('should fire whenstable callback with didwork when event finishes',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
ngZone.unstable();
testability.whenStable(execute);
microTask(() => {
ngZone.stable();
microTask(() => {
expect(execute).toHaveBeenCalledWith(true);
testability.whenStable(execute2);
microTask(() => {
expect(execute2).toHaveBeenCalledWith(false);
async.done();
});
});
});
}));
});
});
}

View File

@ -0,0 +1,117 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SpyObject} from '@angular/core/testing/testing_internal';
class TestObj {
prop: any;
constructor(prop: any) { this.prop = prop; }
someFunc(): number { return -1; }
someComplexFunc(a: any) { return a; }
}
class SpyTestObj extends SpyObject {
constructor() { super(TestObj); }
}
export function main() {
describe('testing', () => {
describe('equality', () => {
it('should structurally compare objects', () => {
const expected = new TestObj(new TestObj({'one': [1, 2]}));
const actual = new TestObj(new TestObj({'one': [1, 2]}));
const falseActual = new TestObj(new TestObj({'one': [1, 3]}));
expect(actual).toEqual(expected);
expect(falseActual).not.toEqual(expected);
});
});
describe('toEqual for Maps', () => {
it('should detect equality for same reference', () => {
const m1: Map<string, number> = new Map();
m1.set('a', 1);
expect(m1).toEqual(m1);
});
it('should detect equality for same content', () => {
const m1: Map<string, number> = new Map();
m1.set('a', 1);
const m2: Map<string, number> = new Map();
m2.set('a', 1);
expect(m1).toEqual(m2);
});
it('should detect missing entries', () => {
const m1: Map<string, number> = new Map();
m1.set('a', 1);
const m2: Map<string, number> = new Map();
expect(m1).not.toEqual(m2);
});
it('should detect different values', () => {
const m1: Map<string, number> = new Map();
m1.set('a', 1);
const m2: Map<string, number> = new Map();
m2.set('a', 2);
expect(m1).not.toEqual(m2);
});
it('should detect additional entries', () => {
const m1: Map<string, number> = new Map();
m1.set('a', 1);
const m2: Map<string, number> = new Map();
m2.set('a', 1);
m2.set('b', 2);
expect(m1).not.toEqual(m2);
});
});
describe('spy objects', () => {
let spyObj: any;
beforeEach(() => { spyObj = new SpyTestObj(); });
it('should return a new spy func with no calls',
() => { expect(spyObj.spy('someFunc')).not.toHaveBeenCalled(); });
it('should record function calls', () => {
spyObj.spy('someFunc').and.callFake((a: any, b: any) => a + b);
expect(spyObj.someFunc(1, 2)).toEqual(3);
expect(spyObj.spy('someFunc')).toHaveBeenCalledWith(1, 2);
});
it('should match multiple function calls', () => {
spyObj.someFunc(1, 2);
spyObj.someFunc(3, 4);
expect(spyObj.spy('someFunc')).toHaveBeenCalledWith(1, 2);
expect(spyObj.spy('someFunc')).toHaveBeenCalledWith(3, 4);
});
it('should match null arguments', () => {
spyObj.someFunc(null, 'hello');
expect(spyObj.spy('someFunc')).toHaveBeenCalledWith(null, 'hello');
});
it('should match using deep equality', () => {
spyObj.someComplexFunc([1]);
expect(spyObj.spy('someComplexFunc')).toHaveBeenCalledWith([1]);
});
it('should support stubs', () => {
const s = SpyObject.stub({'a': 1}, {'b': 2});
expect(s.a()).toEqual(1);
expect(s.b()).toEqual(2);
});
it('should create spys for all methods',
() => { expect(() => spyObj.someFunc()).not.toThrow(); });
});
});
}

View File

@ -0,0 +1,163 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject} from '@angular/core';
import {reflector} from '@angular/core/src/reflection/reflection';
import {global} from '@angular/core/src/util';
import {Class, makeDecorator, makePropDecorator} from '@angular/core/src/util/decorators';
class DecoratedParent {}
class DecoratedChild extends DecoratedParent {}
export function main() {
const Reflect = global['Reflect'];
const TerminalDecorator = makeDecorator('TerminalDecorator', {terminal: true});
const TestDecorator = makeDecorator(
'TestDecorator', {marker: undefined}, Object, (fn: any) => fn.Terminal = TerminalDecorator);
describe('Property decorators', () => {
// https://github.com/angular/angular/issues/12224
it('should work on the "watch" property', () => {
const Prop = makePropDecorator('Prop', [['value', undefined]]);
class TestClass {
@Prop('firefox!')
watch: any;
}
const p = reflector.propMetadata(TestClass);
expect(p['watch']).toEqual([new Prop('firefox!')]);
});
it('should work with any default plain values', () => {
const Default = makePropDecorator('Default', [['value', 5]]);
expect(new Default(0)['value']).toEqual(0);
});
it('should work with any object values', () => {
// make sure we don't walk up the prototype chain
const Default = makePropDecorator('Default', [{value: 5}]);
const value = Object.create({value: 10});
expect(new Default(value)['value']).toEqual(5);
});
});
describe('decorators', () => {
it('should invoke as decorator', () => {
function Type() {}
TestDecorator({marker: 'WORKS'})(Type);
const annotations = Reflect.getOwnMetadata('annotations', Type);
expect(annotations[0].marker).toEqual('WORKS');
});
it('should invoke as new', () => {
const annotation = new (<any>TestDecorator)({marker: 'WORKS'});
expect(annotation instanceof TestDecorator).toEqual(true);
expect(annotation.marker).toEqual('WORKS');
});
it('should invoke as chain', () => {
let chain: any = TestDecorator({marker: 'WORKS'});
expect(typeof chain.Terminal).toEqual('function');
chain = chain.Terminal();
expect(chain.annotations[0] instanceof TestDecorator).toEqual(true);
expect(chain.annotations[0].marker).toEqual('WORKS');
expect(chain.annotations[1] instanceof TerminalDecorator).toEqual(true);
});
it('should not apply decorators from the prototype chain', function() {
TestDecorator({marker: 'parent'})(DecoratedParent);
TestDecorator({marker: 'child'})(DecoratedChild);
const annotations = Reflect.getOwnMetadata('annotations', DecoratedChild);
expect(annotations.length).toBe(1);
expect(annotations[0].marker).toEqual('child');
});
describe('Class', () => {
it('should create a class', () => {
let i0: any;
let i1: any;
const MyClass = (<any>TestDecorator({marker: 'test-works'})).Class(<any>{
extends: Class(<any>{
constructor: function() {},
extendWorks: function() { return 'extend ' + this.arg; }
}),
constructor: [String, function(arg: any) { this.arg = arg; }],
methodA: [
i0 = new Inject(String),
[i1 = Inject(String), Number],
function(a: any, b: any) {},
],
works: function() { return this.arg; },
prototype: 'IGNORE'
});
const obj: any = new MyClass('WORKS');
expect(obj.arg).toEqual('WORKS');
expect(obj.works()).toEqual('WORKS');
expect(obj.extendWorks()).toEqual('extend WORKS');
expect(reflector.parameters(MyClass)).toEqual([[String]]);
expect(reflector.parameters(obj.methodA)).toEqual([[i0], [i1.annotation, Number]]);
const proto = (<Function>MyClass).prototype;
expect(proto.extends).toEqual(undefined);
expect(proto.prototype).toEqual(undefined);
expect(reflector.annotations(MyClass)[0].marker).toEqual('test-works');
});
describe('errors', () => {
it('should ensure that last constructor is required', () => {
expect(() => { (<Function>Class)({}); })
.toThrowError(
'Only Function or Array is supported in Class definition for key \'constructor\' is \'undefined\'');
});
it('should ensure that we dont accidently patch native objects', () => {
expect(() => {
(<Function>Class)({constructor: Object});
}).toThrowError('Can not use native Object as constructor');
});
it('should ensure that last position is function', () => {
expect(() => { Class({constructor: []}); })
.toThrowError(
'Last position of Class method array must be Function in key constructor was \'undefined\'');
});
it('should ensure that annotation count matches parameters count', () => {
expect(() => {
Class({constructor: [String, function MyType() {}]});
})
.toThrowError(
'Number of annotations (1) does not match number of arguments (0) in the function: MyType');
});
it('should ensure that only Function|Arrays are supported', () => {
expect(() => { Class({constructor: function() {}, method: <any>'non_function'}); })
.toThrowError(
'Only Function or Array is supported in Class definition for key \'method\' is \'non_function\'');
});
it('should ensure that extends is a Function', () => {
expect(() => { Class({extends: <any>'non_type', constructor: function() {}}); })
.toThrowError(
'Class definition \'extends\' property must be a constructor function was: non_type');
});
it('should assign an overridden name for anonymous constructor functions', () => {
expect((Class({constructor: function() {}}) as any).overriddenName).not.toBeUndefined();
});
});
});
});
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {isObservable, isPromise} from '@angular/core/src/util/lang';
import {of } from 'rxjs/observable/of';
export function main() {
describe('isPromise', () => {
it('should be true for native Promises',
() => expect(isPromise(Promise.resolve(true))).toEqual(true));
it('should be true for thenables', () => expect(isPromise({then: () => {}})).toEqual(true));
it('should be false if "then" is not a function',
() => expect(isPromise({then: 0})).toEqual(false));
it('should be false if the argument has no "then" function',
() => expect(isPromise({})).toEqual(false));
it('should be false if the argument is undefined or null', () => {
expect(isPromise(undefined)).toEqual(false);
expect(isPromise(null)).toEqual(false);
});
});
describe('isObservable', () => {
it('should be true for an Observable', () => expect(isObservable(of (true))).toEqual(true));
it('should be false if the argument is undefined',
() => expect(isObservable(undefined)).toEqual(false));
it('should be false if the argument is null', () => expect(isObservable(null)).toEqual(false));
it('should be false if the argument is an object',
() => expect(isObservable({})).toEqual(false));
it('should be false if the argument is a function',
() => expect(isObservable(() => {})).toEqual(false));
it('should be false if the argument is the object with subscribe function',
() => expect(isObservable({subscribe: () => {}})).toEqual(false));
});
}

View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core';
import {DebugContext, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView, isBrowser} from './helper';
export function main() {
describe(`View Anchor`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn,
updateRenderer?: ViewUpdateFn): ViewDefinition {
return viewDef(ViewFlags.None, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, ctx?: any): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, ctx);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create anchor nodes without parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, null, null, 0)
])).rootNodes;
expect(rootNodes.length).toBe(1);
});
it('should create views with multiple root anchor nodes', () => {
const rootNodes =
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, null, null, 0), anchorDef(NodeFlags.None, null, null, 0)
])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create anchor nodes with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
anchorDef(NodeFlags.None, null, null, 0),
])).rootNodes;
expect(getDOM().childNodes(rootNodes[0]).length).toBe(1);
});
it('should add debug information to the renderer', () => {
const someContext = new Object();
const {view, rootNodes} = createAndGetRootNodes(
compViewDef([anchorDef(NodeFlags.None, null, null, 0)]), someContext);
expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asElementData(view, 0).renderElement);
});
});
});
}

View File

@ -0,0 +1,312 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {ArgumentType, BindingType, NodeCheckFn, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView, isBrowser, removeNodes} from './helper';
export function main() {
describe(
`Component Views`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(viewDef: ViewDefinition):
{rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
it('should create and attach component views', () => {
let instance: AComp;
class AComp {
constructor() { instance = this; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span'),
])),
directiveDef(NodeFlags.Component, null, 0, AComp, []),
]));
const compView = asElementData(view, 0).componentView;
expect(compView.context).toBe(instance);
expect(compView.component).toBe(instance);
const compRootEl = getDOM().childNodes(rootNodes[0])[0];
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
});
if (isBrowser()) {
describe('root views', () => {
let rootNode: HTMLElement;
beforeEach(() => {
rootNode = document.createElement('root');
document.body.appendChild(rootNode);
removeNodes.push(rootNode);
});
it('should select root elements based on a selector', () => {
const view = createRootView(
compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'div'),
]),
{}, [], 'root');
const rootNodes = rootRenderNodes(view);
expect(rootNodes).toEqual([rootNode]);
});
it('should select root elements based on a node', () => {
const view = createRootView(
compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'div'),
]),
{}, [], rootNode);
const rootNodes = rootRenderNodes(view);
expect(rootNodes).toEqual([rootNode]);
});
it('should set attributes on the root node', () => {
const view = createRootView(
compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
]),
{}, [], rootNode);
expect(rootNode.getAttribute('a')).toBe('b');
});
it('should clear the content of the root node', () => {
rootNode.appendChild(document.createElement('div'));
const view = createRootView(
compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
]),
{}, [], rootNode);
expect(rootNode.childNodes.length).toBe(0);
});
});
}
describe(
'data binding', () => {
it('should dirty check component views',
() => {
let value: any;
class AComp {
a: any;
}
const update = jasmine.createSpy('updater').and.callFake(
(check: NodeCheckFn, view: ViewData) => {
check(view, 0, ArgumentType.Inline, value);
});
const {view, rootNodes} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
], null, update
)),
directiveDef(NodeFlags.Component, null, 0, AComp, []),
]));
const compView = asElementData(view, 0).componentView;
value = 'v1';
Services.checkAndUpdateView(view);
expect(update.calls.mostRecent().args[1]).toBe(compView);
update.calls.reset();
Services.checkNoChangesView(view);
expect(update.calls.mostRecent().args[1]).toBe(compView);
value = 'v2';
expect(() => Services.checkNoChangesView(view))
.toThrowError(
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
it('should support detaching and attaching component views for dirty checking',
() => {
class AComp {
a: any;
}
const update = jasmine.createSpy('updater');
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span'),
],
update)),
directiveDef(NodeFlags.Component, null, 0, AComp, [], null, null),
]));
const compView = asElementData(view, 0).componentView;
Services.checkAndUpdateView(view);
update.calls.reset();
compView.state &= ~ViewState.ChecksEnabled;
Services.checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
compView.state |= ViewState.ChecksEnabled;
Services.checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
});
if (isBrowser()) {
it('should support OnPush components', () => {
let compInputValue: any;
class AComp {
a: any;
}
const update = jasmine.createSpy('updater');
const addListenerSpy =
spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
const {view} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => {
return compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'span', null, null,
[[null, 'click']]),
],
update, null, ViewFlags.OnPush);
}),
directiveDef(NodeFlags.Component, null, 0, AComp, [], {a: [0, 'a']}),
],
(check, view) => { check(view, 1, ArgumentType.Inline, compInputValue); }));
Services.checkAndUpdateView(view);
// auto detach
update.calls.reset();
Services.checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
// auto attach on input changes
update.calls.reset();
compInputValue = 'v1';
Services.checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
// auto detach
update.calls.reset();
Services.checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
// auto attach on events
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
update.calls.reset();
Services.checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
// auto detach
update.calls.reset();
Services.checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
});
}
it('should stop dirty checking views that threw errors in change detection',
() => {
class AComp {
a: any;
}
const update = jasmine.createSpy('updater');
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
],
null, update)),
directiveDef(
NodeFlags.Component, null, 0, AComp, [], null, null,
),
]));
const compView = asElementData(view, 0).componentView;
update.and.callFake(
(check: NodeCheckFn, view: ViewData) => { throw new Error('Test'); });
expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
expect(update).toHaveBeenCalled();
update.calls.reset();
Services.checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
});
});
describe('destroy', () => {
it('should destroy component views', () => {
const log: string[] = [];
class AComp {}
class ChildProvider {
ngOnDestroy() { log.push('ngOnDestroy'); };
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
])),
directiveDef(NodeFlags.Component, null, 0, AComp, [], null, null, ),
]));
Services.destroyView(view);
expect(log).toEqual(['ngOnDestroy']);
});
it('should throw on dirty checking destroyed views', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'div'),
],
(view) => {}));
Services.destroyView(view);
expect(() => Services.checkAndUpdateView(view))
.toThrowError('ViewDestroyedError: Attempt to use a destroyed view: detectChanges');
});
});
});
}

View File

@ -0,0 +1,302 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors';
import {ArgumentType, BindingType, DebugContext, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, removeNodes} from './helper';
export function main() {
describe(`View Elements`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context?: any): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create elements without parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span')
])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().nodeName(rootNodes[0]).toLowerCase()).toBe('span');
});
it('should create views with multiple root elements', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span'),
elementDef(NodeFlags.None, null, null, 0, 'span')
])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create elements with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
elementDef(NodeFlags.None, null, null, 0, 'span'),
])).rootNodes;
expect(rootNodes.length).toBe(1);
const spanEl = getDOM().childNodes(rootNodes[0])[0];
expect(getDOM().nodeName(spanEl).toLowerCase()).toBe('span');
});
it('should set fixed attributes', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'div', [['title', 'a']]),
])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().getAttribute(rootNodes[0], 'title')).toBe('a');
});
it('should add debug information to the renderer', () => {
const someContext = new Object();
const {view, rootNodes} = createAndGetRootNodes(
compViewDef([elementDef(NodeFlags.None, null, null, 0, 'div')]), someContext);
expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asElementData(view, 0).renderElement);
});
});
describe('change properties', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'input', null,
[
[BindingType.ElementProperty, 'title', SecurityContext.NONE],
[BindingType.ElementProperty, 'value', SecurityContext.NONE]
]),
],
null, (check, view) => {
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['v1', 'v2']);
}));
Services.checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getProperty(el, 'title')).toBe('v1');
expect(getDOM().getProperty(el, 'value')).toBe('v2');
});
});
});
describe('change attributes', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'div', null,
[
[BindingType.ElementAttribute, 'a1', SecurityContext.NONE],
[BindingType.ElementAttribute, 'a2', SecurityContext.NONE]
]),
],
null, (check, view) => {
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['v1', 'v2']);
}));
Services.checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getAttribute(el, 'a1')).toBe('v1');
expect(getDOM().getAttribute(el, 'a2')).toBe('v2');
});
});
});
describe('change classes', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'div', null,
[[BindingType.ElementClass, 'c1'], [BindingType.ElementClass, 'c2']]),
],
(check, view) => {
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, [true, true]);
}));
Services.checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().hasClass(el, 'c1')).toBeTruthy();
expect(getDOM().hasClass(el, 'c2')).toBeTruthy();
});
});
});
describe('change styles', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'div', null,
[
[BindingType.ElementStyle, 'width', 'px'],
[BindingType.ElementStyle, 'color', null]
]),
],
null, (check, view) => {
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, [10, 'red']);
}));
Services.checkAndUpdateView(view);
const el = rootNodes[0];
expect(getDOM().getStyle(el, 'width')).toBe('10px');
expect(getDOM().getStyle(el, 'color')).toBe('red');
});
});
});
if (isBrowser()) {
describe('listen to DOM events', () => {
function createAndAttachAndGetRootNodes(viewDef: ViewDefinition):
{rootNodes: any[], view: ViewData} {
const result = createAndGetRootNodes(viewDef);
// Note: We need to append the node to the document.body, otherwise `click` events
// won't work in IE.
result.rootNodes.forEach((node) => {
document.body.appendChild(node);
removeNodes.push(node);
});
return result;
}
it('should listen to DOM events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const removeListenerSpy =
spyOn(HTMLElement.prototype, 'removeEventListener').and.callThrough();
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
handleEventSpy)]));
rootNodes[0].click();
expect(handleEventSpy).toHaveBeenCalled();
let handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe('click');
expect(handleEventArgs[2]).toBeTruthy();
Services.destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should listen to window events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const addListenerSpy = spyOn(window, 'addEventListener');
const removeListenerSpy = spyOn(window, 'removeEventListener');
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null, null, 0, 'button', null, null, [['window', 'windowClick']],
handleEventSpy)]));
expect(addListenerSpy).toHaveBeenCalled();
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('windowClick');
addListenerSpy.calls.mostRecent().args[1]({name: 'windowClick'});
expect(handleEventSpy).toHaveBeenCalled();
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe('window:windowClick');
expect(handleEventArgs[2]).toBeTruthy();
Services.destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should listen to document events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const addListenerSpy = spyOn(document, 'addEventListener');
const removeListenerSpy = spyOn(document, 'removeEventListener');
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null, null, 0, 'button', null, null, [['document', 'documentClick']],
handleEventSpy)]));
expect(addListenerSpy).toHaveBeenCalled();
expect(addListenerSpy.calls.mostRecent().args[0]).toBe('documentClick');
addListenerSpy.calls.mostRecent().args[1]({name: 'documentClick'});
expect(handleEventSpy).toHaveBeenCalled();
const handleEventArgs = handleEventSpy.calls.mostRecent().args;
expect(handleEventArgs[0]).toBe(view);
expect(handleEventArgs[1]).toBe('document:documentClick');
expect(handleEventArgs[2]).toBeTruthy();
Services.destroyView(view);
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should preventDefault only if the handler returns false', () => {
let eventHandlerResult: any;
let preventDefaultSpy: jasmine.Spy;
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
(view, eventName, event) => {
preventDefaultSpy = spyOn(event, 'preventDefault').and.callThrough();
return eventHandlerResult;
})]));
eventHandlerResult = undefined;
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = true;
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = 'someString';
rootNodes[0].click();
expect(preventDefaultSpy).not.toHaveBeenCalled();
eventHandlerResult = false;
rootNodes[0].click();
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should report debug info on event errors', () => {
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
() => { throw new Error('Test'); })]));
let err: any;
try {
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(0);
});
});
}
});
}

View File

@ -0,0 +1,189 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {ArgumentType, BindingType, NodeCheckFn, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, moveEmbeddedView, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView, isBrowser} from './helper';
export function main() {
describe(`Embedded Views`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
return () => viewDef(ViewFlags.None, nodes, update);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
it('should create embedded views with the right context', () => {
const parentContext = new Object();
const childContext = new Object();
const {view: parentView} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
anchorDef(
NodeFlags.EmbeddedViews, null, null, 0, null,
embeddedViewDef([elementDef(NodeFlags.None, null, null, 0, 'span')])),
]),
parentContext);
const childView =
Services.createEmbeddedView(parentView, parentView.def.nodes[1], childContext);
expect(childView.component).toBe(parentContext);
expect(childView.context).toBe(childContext);
});
it('should attach and detach embedded views', () => {
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'div'),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
])),
anchorDef(NodeFlags.None, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'child1']])
]))
]));
const viewContainerData = asElementData(parentView, 1);
const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]);
const childView1 = Services.createEmbeddedView(parentView, parentView.def.nodes[2]);
attachEmbeddedView(parentView, viewContainerData, 0, childView0);
attachEmbeddedView(parentView, viewContainerData, 1, childView1);
// 2 anchors + 2 elements
const rootChildren = getDOM().childNodes(rootNodes[0]);
expect(rootChildren.length).toBe(4);
expect(getDOM().getAttribute(rootChildren[1], 'name')).toBe('child0');
expect(getDOM().getAttribute(rootChildren[2], 'name')).toBe('child1');
detachEmbeddedView(viewContainerData, 1);
detachEmbeddedView(viewContainerData, 0);
expect(getDOM().childNodes(rootNodes[0]).length).toBe(2);
});
it('should move embedded views', () => {
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'div'),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
])),
anchorDef(NodeFlags.None, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'child1']])
]))
]));
const viewContainerData = asElementData(parentView, 1);
const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]);
const childView1 = Services.createEmbeddedView(parentView, parentView.def.nodes[2]);
attachEmbeddedView(parentView, viewContainerData, 0, childView0);
attachEmbeddedView(parentView, viewContainerData, 1, childView1);
moveEmbeddedView(viewContainerData, 0, 1);
expect(viewContainerData.embeddedViews).toEqual([childView1, childView0]);
// 2 anchors + 2 elements
const rootChildren = getDOM().childNodes(rootNodes[0]);
expect(rootChildren.length).toBe(4);
expect(getDOM().getAttribute(rootChildren[1], 'name')).toBe('child1');
expect(getDOM().getAttribute(rootChildren[2], 'name')).toBe('child0');
});
it('should include embedded views in root nodes', () => {
const {view: parentView} = createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'child0']])
])),
elementDef(NodeFlags.None, null, null, 0, 'span', [['name', 'after']])
]));
const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[0]);
attachEmbeddedView(parentView, asElementData(parentView, 0), 0, childView0);
const rootNodes = rootRenderNodes(parentView);
expect(rootNodes.length).toBe(3);
expect(getDOM().getAttribute(rootNodes[1], 'name')).toBe('child0');
expect(getDOM().getAttribute(rootNodes[2], 'name')).toBe('after');
});
it('should dirty check embedded views', () => {
let childValue = 'v1';
const update =
jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => {
check(view, 0, ArgumentType.Inline, childValue);
});
const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
anchorDef(
NodeFlags.EmbeddedViews, null, null, 0, null,
embeddedViewDef(
[elementDef(
NodeFlags.None, null, null, 0, 'span', null,
[[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])],
update))
]));
const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]);
attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0);
Services.checkAndUpdateView(parentView);
expect(update.calls.mostRecent().args[1]).toBe(childView0);
update.calls.reset();
Services.checkNoChangesView(parentView);
expect(update.calls.mostRecent().args[1]).toBe(childView0);
childValue = 'v2';
expect(() => Services.checkNoChangesView(parentView))
.toThrowError(
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
});
it('should destroy embedded views', () => {
const log: string[] = [];
class ChildProvider {
ngOnDestroy() { log.push('ngOnDestroy'); };
}
const {view: parentView} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
]))
]));
const childView0 = Services.createEmbeddedView(parentView, parentView.def.nodes[1]);
attachEmbeddedView(parentView, asElementData(parentView, 1), 0, childView0);
Services.destroyView(parentView);
expect(log).toEqual(['ngOnDestroy']);
});
});
}

View File

@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RootRenderer, Sanitizer} from '@angular/core';
import {ArgumentType, NodeCheckFn, RootData, Services, ViewData, ViewDefinition, initServicesIfNeeded} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
export function isBrowser() {
return getDOM().supportsDOMEvents();
}
export const ARG_TYPE_VALUES = [ArgumentType.Inline, ArgumentType.Dynamic];
export function checkNodeInlineOrDynamic(
check: NodeCheckFn, view: ViewData, nodeIndex: number, argType: ArgumentType,
values: any[]): any {
switch (argType) {
case ArgumentType.Inline:
return (<any>check)(view, nodeIndex, argType, ...values);
case ArgumentType.Dynamic:
return check(view, nodeIndex, argType, values);
}
}
export function createRootView(
def: ViewDefinition, context?: any, projectableNodes?: any[][],
rootSelectorOrNode?: any): ViewData {
initServicesIfNeeded();
return Services.createRootView(
TestBed.get(Injector), projectableNodes || [], rootSelectorOrNode, def, context);
}
export let removeNodes: Node[];
beforeEach(() => { removeNodes = []; });
afterEach(() => { removeNodes.forEach((node) => getDOM().remove(node)); });

View File

@ -0,0 +1,139 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core';
import {DebugContext, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, asTextData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, ngContentDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView, isBrowser} from './helper';
export function main() {
describe(`View NgContent`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
return () => viewDef(ViewFlags.None, nodes, update);
}
function hostElDef(contentNodes: NodeDef[], viewNodes: NodeDef[]): NodeDef[] {
class AComp {}
const aCompViewDef = compViewDef(viewNodes);
return [
elementDef(
NodeFlags.None, null, null, 1 + contentNodes.length, 'acomp', null, null, null, null,
() => aCompViewDef),
directiveDef(NodeFlags.Component, null, 0, AComp, []), ...contentNodes
];
}
function createAndGetRootNodes(
viewDef: ViewDefinition, ctx?: any): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, ctx || {});
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
it('should create ng-content nodes without parents', () => {
const {view, rootNodes} = createAndGetRootNodes(
compViewDef(hostElDef([textDef(0, ['a'])], [ngContentDef(null, 0)])));
expect(getDOM().firstChild(rootNodes[0])).toBe(asTextData(view, 2).renderText);
});
it('should create views with multiple root ng-content nodes', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
[textDef(0, ['a']), textDef(1, ['b'])], [ngContentDef(null, 0), ngContentDef(null, 1)])));
expect(getDOM().childNodes(rootNodes[0])[0]).toBe(asTextData(view, 2).renderText);
expect(getDOM().childNodes(rootNodes[0])[1]).toBe(asTextData(view, 3).renderText);
});
it('should create ng-content nodes with parents', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
[textDef(0, ['a'])],
[elementDef(NodeFlags.None, null, null, 1, 'div'), ngContentDef(null, 0)])));
expect(getDOM().firstChild(getDOM().firstChild(rootNodes[0])))
.toBe(asTextData(view, 2).renderText);
});
it('should reproject ng-content nodes', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
hostElDef([textDef(0, ['a'])], hostElDef([ngContentDef(0, 0)], [
elementDef(NodeFlags.None, null, null, 1, 'span'), ngContentDef(null, 0)
]))));
expect(getDOM().firstChild(getDOM().firstChild(getDOM().firstChild(rootNodes[0]))))
.toBe(asTextData(view, 2).renderText);
});
it('should project already attached embedded views', () => {
class CreateViewService {
constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
viewContainerRef.createEmbeddedView(templateRef);
}
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef(
[
anchorDef(
NodeFlags.EmbeddedViews, null, 0, 1, null, embeddedViewDef([textDef(null, ['a'])])),
directiveDef(
NodeFlags.None, null, 0, CreateViewService, [TemplateRef, ViewContainerRef])
],
[elementDef(NodeFlags.None, null, null, 1, 'div'), ngContentDef(null, 0)])));
const anchor = asElementData(view, 2);
expect((getDOM().childNodes(getDOM().firstChild(rootNodes[0]))[0]))
.toBe(anchor.renderElement);
const embeddedView = anchor.embeddedViews[0];
expect((getDOM().childNodes(getDOM().firstChild(rootNodes[0]))[1]))
.toBe(asTextData(embeddedView, 0).renderText);
});
it('should include projected nodes when attaching / detaching embedded views', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(hostElDef([textDef(0, ['a'])], [
elementDef(NodeFlags.None, null, null, 1, 'div'),
anchorDef(NodeFlags.EmbeddedViews, null, 0, 0, null, embeddedViewDef([
ngContentDef(null, 0),
// The anchor would be added by the compiler after the ngContent
anchorDef(NodeFlags.None, null, null, 0),
])),
])));
const componentView = asElementData(view, 0).componentView;
const view0 = Services.createEmbeddedView(componentView, componentView.def.nodes[1]);
attachEmbeddedView(view, asElementData(componentView, 1), 0, view0);
expect(getDOM().childNodes(getDOM().firstChild(rootNodes[0])).length).toBe(3);
expect(getDOM().childNodes(getDOM().firstChild(rootNodes[0]))[1])
.toBe(asTextData(view, 2).renderText);
detachEmbeddedView(asElementData(componentView, 1), 0);
expect(getDOM().childNodes(getDOM().firstChild(rootNodes[0])).length).toBe(1);
});
if (isBrowser()) {
it('should use root projectable nodes', () => {
const projectableNodes = [[document.createTextNode('a')], [document.createTextNode('b')]];
const view = createRootView(
compViewDef(hostElDef([], [ngContentDef(null, 0), ngContentDef(null, 1)])), {},
projectableNodes);
const rootNodes = rootRenderNodes(view);
expect(getDOM().childNodes(rootNodes[0])[0]).toBe(projectableNodes[0][0]);
expect(getDOM().childNodes(rootNodes[0])[1]).toBe(projectableNodes[1][0]);
});
}
});
}

View File

@ -0,0 +1,554 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, Renderer2, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors';
import {ArgumentType, BindingType, DebugContext, DepFlags, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {TestBed, inject, withModule} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser} from './helper';
export function main() {
describe(`View Providers`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
return () => viewDef(ViewFlags.None, nodes, update);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, {});
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
let instance: SomeService;
class SomeService {
constructor(public dep: any) { instance = this; }
}
beforeEach(() => { instance = null; });
it('should create providers eagerly', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [])
]));
expect(instance instanceof SomeService).toBe(true);
});
it('should create providers lazily', () => {
let lazy: LazyService;
class LazyService {
constructor() { lazy = this; }
}
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'span'),
providerDef(
NodeFlags.TypeClassProvider | NodeFlags.LazyProvider, null, LazyService, LazyService,
[]),
directiveDef(NodeFlags.None, null, 0, SomeService, [Injector])
]));
expect(lazy).toBeUndefined();
instance.dep.get(LazyService);
expect(lazy instanceof LazyService).toBe(true);
});
it('should create value providers', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
directiveDef(NodeFlags.None, null, 0, SomeService, ['someToken']),
]));
expect(instance.dep).toBe('someValue');
});
it('should create factory providers', () => {
function someFactory() { return 'someValue'; }
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeFactoryProvider, null, 'someToken', someFactory, []),
directiveDef(NodeFlags.None, null, 0, SomeService, ['someToken']),
]));
expect(instance.dep).toBe('someValue');
});
it('should create useExisting providers', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someExistingToken', 'someValue', []),
providerDef(
NodeFlags.TypeUseExistingProvider, null, 'someToken', null, ['someExistingToken']),
directiveDef(NodeFlags.None, null, 0, SomeService, ['someToken']),
]));
expect(instance.dep).toBe('someValue');
});
it('should add a DebugContext to errors in provider factories', () => {
class SomeService {
constructor() { throw new Error('Test'); }
}
let err: any;
try {
createRootView(
compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([textDef(null, ['a'])])),
directiveDef(NodeFlags.Component, null, 0, SomeService, [])
]),
TestBed.get(Injector), [], getDOM().createElement('div'));
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBeTruthy();
expect(debugCtx.nodeIndex).toBe(1);
});
describe('deps', () => {
class Dep {}
it('should inject deps from the same element', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.None, null, 0, Dep, []),
directiveDef(NodeFlags.None, null, 0, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should inject deps from a parent element', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'span'),
directiveDef(NodeFlags.None, null, 0, Dep, []),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should not inject deps from sibling root elements', () => {
const nodes = [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, Dep, []),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [Dep])
];
// root elements
expect(() => createAndGetRootNodes(compViewDef(nodes)))
.toThrowError('No provider for Dep!');
// non root elements
expect(
() => createAndGetRootNodes(
compViewDef([elementDef(NodeFlags.None, null, null, 4, 'span')].concat(nodes))))
.toThrowError('No provider for Dep!');
});
it('should inject from a parent elment in a parent view', () => {
createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [Dep])
])),
directiveDef(NodeFlags.Component, null, 0, Dep, []),
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should throw for missing dependencies', () => {
expect(() => createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
])))
.toThrowError('No provider for nonExistingDep!');
});
it('should use null for optional missing dependencies', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(
NodeFlags.None, null, 0, SomeService, [[DepFlags.Optional, 'nonExistingDep']])
]));
expect(instance.dep).toBe(null);
});
it('should skip the current element when using SkipSelf', () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 4, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someParentValue', []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
directiveDef(
NodeFlags.None, null, 0, SomeService, [[DepFlags.SkipSelf, 'someToken']])
]));
expect(instance.dep).toBe('someParentValue');
});
it('should ask the root injector',
withModule({providers: [{provide: 'rootDep', useValue: 'rootValue'}]}, () => {
createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, ['rootDep'])
]));
expect(instance.dep).toBe('rootValue');
}));
describe('builtin tokens', () => {
it('should inject ViewContainerRef', () => {
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.EmbeddedViews, null, null, 1),
directiveDef(NodeFlags.None, null, 0, SomeService, [ViewContainerRef])
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject TemplateRef', () => {
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, null, null, 1, null, embeddedViewDef([anchorDef(
NodeFlags.None, null, null, 0)])),
directiveDef(NodeFlags.None, null, 0, SomeService, [TemplateRef])
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject ElementRef', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [ElementRef])
]));
expect(instance.dep.nativeElement).toBe(asElementData(view, 0).renderElement);
});
it('should inject Injector', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [Injector])
]));
expect(instance.dep.get(SomeService)).toBe(instance);
});
it('should inject ChangeDetectorRef for non component providers', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [ChangeDetectorRef])
]));
expect(instance.dep._view).toBe(view);
});
it('should inject ChangeDetectorRef for component providers', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(NodeFlags.None, null, null, 0, 'span'),
])),
directiveDef(NodeFlags.Component, null, 0, SomeService, [ChangeDetectorRef]),
]));
const compView = asElementData(view, 0).componentView;
expect(instance.dep._view).toBe(compView);
});
it('should inject RendererV1', () => {
createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'span', null, null, null, null,
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(NodeFlags.Component, null, 0, SomeService, [Renderer])
]));
expect(instance.dep.createElement).toBeTruthy();
});
it('should inject Renderer2', () => {
createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'span', null, null, null, null,
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(NodeFlags.Component, null, 0, SomeService, [Renderer2])
]));
expect(instance.dep.createElement).toBeTruthy();
});
});
});
});
describe('data binding', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
let instance: SomeService;
class SomeService {
a: any;
b: any;
constructor() { instance = this; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, null, 0, SomeService, [], {a: [0, 'a'], b: [1, 'b']})
],
(check, view) => {
checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, ['v1', 'v2']);
}));
Services.checkAndUpdateView(view);
expect(instance.a).toBe('v1');
expect(instance.b).toBe('v2');
const el = rootNodes[0];
expect(getDOM().getAttribute(el, 'ng-reflect-a')).toBe('v1');
});
});
});
describe('outputs', () => {
it('should listen to provider events', () => {
let emitter = new EventEmitter<any>();
let unsubscribeSpy: any;
class SomeService {
emitter = {
subscribe: (callback: any) => {
const subscription = emitter.subscribe(callback);
unsubscribeSpy = spyOn(subscription, 'unsubscribe').and.callThrough();
return subscription;
}
};
}
const handleEvent = jasmine.createSpy('handleEvent');
const subscribe = spyOn(emitter, 'subscribe').and.callThrough();
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span', null, null, null, handleEvent),
directiveDef(
NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'})
]));
emitter.emit('someEventInstance');
expect(handleEvent).toHaveBeenCalledWith(view, 'someEventName', 'someEventInstance');
Services.destroyView(view);
expect(unsubscribeSpy).toHaveBeenCalled();
});
it('should report debug info on event errors', () => {
let emitter = new EventEmitter<any>();
class SomeService {
emitter = emitter;
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'span', null, null, null,
() => { throw new Error('Test'); }),
directiveDef(
NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'})
]));
let err: any;
try {
emitter.emit('someEventInstance');
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
// events are emitted with the index of the element, not the index of the provider.
expect(debugCtx.nodeIndex).toBe(0);
});
});
describe('lifecycle hooks', () => {
it('should call the lifecycle hooks in the right order', () => {
let instanceCount = 0;
let log: string[] = [];
class SomeService implements OnInit, DoCheck, OnChanges, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
id: number;
a: any;
ngOnInit() { log.push(`${this.id}_ngOnInit`); }
ngDoCheck() { log.push(`${this.id}_ngDoCheck`); }
ngOnChanges() { log.push(`${this.id}_ngOnChanges`); }
ngAfterContentInit() { log.push(`${this.id}_ngAfterContentInit`); }
ngAfterContentChecked() { log.push(`${this.id}_ngAfterContentChecked`); }
ngAfterViewInit() { log.push(`${this.id}_ngAfterViewInit`); }
ngAfterViewChecked() { log.push(`${this.id}_ngAfterViewChecked`); }
ngOnDestroy() { log.push(`${this.id}_ngOnDestroy`); }
constructor() { this.id = instanceCount++; }
}
const allFlags = NodeFlags.OnInit | NodeFlags.DoCheck | NodeFlags.OnChanges |
NodeFlags.AfterContentInit | NodeFlags.AfterContentChecked | NodeFlags.AfterViewInit |
NodeFlags.AfterViewChecked | NodeFlags.OnDestroy;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 3, 'span'),
directiveDef(allFlags, null, 0, SomeService, [], {a: [0, 'a']}),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(allFlags, null, 0, SomeService, [], {a: [0, 'a']})
],
(check, view) => {
check(view, 1, ArgumentType.Inline, 'someValue');
check(view, 3, ArgumentType.Inline, 'someValue');
}));
Services.checkAndUpdateView(view);
// Note: After... hooks are called bottom up.
expect(log).toEqual([
'0_ngOnChanges',
'0_ngOnInit',
'0_ngDoCheck',
'1_ngOnChanges',
'1_ngOnInit',
'1_ngDoCheck',
'1_ngAfterContentInit',
'1_ngAfterContentChecked',
'0_ngAfterContentInit',
'0_ngAfterContentChecked',
'1_ngAfterViewInit',
'1_ngAfterViewChecked',
'0_ngAfterViewInit',
'0_ngAfterViewChecked',
]);
log = [];
Services.checkAndUpdateView(view);
// Note: After... hooks are called bottom up.
expect(log).toEqual([
'0_ngDoCheck', '1_ngDoCheck', '1_ngAfterContentChecked', '0_ngAfterContentChecked',
'1_ngAfterViewChecked', '0_ngAfterViewChecked'
]);
log = [];
Services.destroyView(view);
// Note: ngOnDestroy ist called bottom up.
expect(log).toEqual(['1_ngOnDestroy', '0_ngOnDestroy']);
});
it('should call ngOnChanges with the changed values and the non minified names', () => {
let changesLog: SimpleChange[] = [];
let currValue = 'v1';
class SomeService implements OnChanges {
a: any;
ngOnChanges(changes: {[name: string]: SimpleChange}) {
changesLog.push(changes['nonMinifiedA']);
}
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(
NodeFlags.OnChanges, null, 0, SomeService, [], {a: [0, 'nonMinifiedA']})
],
(check, view) => { check(view, 1, ArgumentType.Inline, currValue); }));
Services.checkAndUpdateView(view);
expect(changesLog).toEqual([new SimpleChange(undefined, 'v1', true)]);
currValue = 'v2';
changesLog = [];
Services.checkAndUpdateView(view);
expect(changesLog).toEqual([new SimpleChange('v1', 'v2', false)]);
});
it('should add a DebugContext to errors in provider afterXXX lifecycles', () => {
class SomeService implements AfterContentChecked {
ngAfterContentChecked() { throw new Error('Test'); }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.AfterContentChecked, null, 0, SomeService, [], {a: [0, 'a']}),
]));
let err: any;
try {
Services.checkAndUpdateView(view);
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(1);
});
it('should add a DebugContext to errors inServices.destroyView', () => {
class SomeService implements OnDestroy {
ngOnDestroy() { throw new Error('Test'); }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.OnDestroy, null, 0, SomeService, [], {a: [0, 'a']}),
]));
let err: any;
try {
Services.destroyView(view);
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(1);
});
});
});
}

View File

@ -0,0 +1,149 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, PipeTransform, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue} from '@angular/core';
import {ArgumentType, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, asPureExpressionData, directiveDef, elementDef, nodeValue, pipeDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView} from './helper';
export function main() {
describe(`View Pure Expressions`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
class Service {
data: any;
}
describe('pure arrays', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureArrayDef(2),
directiveDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(check, view) => {
const pureValue = checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, values);
checkNodeInlineOrDynamic(check, view, 2, inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
values = [1, 2];
Services.checkAndUpdateView(view);
const arr0 = service.data;
expect(arr0).toEqual([1, 2]);
// instance should not change
// if the values don't change
Services.checkAndUpdateView(view);
expect(service.data).toBe(arr0);
values = [3, 2];
Services.checkAndUpdateView(view);
const arr1 = service.data;
expect(arr1).not.toBe(arr0);
expect(arr1).toEqual([3, 2]);
});
});
});
describe('pure objects', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureObjectDef(['a', 'b']),
directiveDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(check, view) => {
const pureValue = checkNodeInlineOrDynamic(check, view, 1, inlineDynamic, values);
checkNodeInlineOrDynamic(check, view, 2, inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
values = [1, 2];
Services.checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual({a: 1, b: 2});
// instance should not change
// if the values don't change
Services.checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [3, 2];
Services.checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual({a: 3, b: 2});
});
});
});
describe('pure pipes', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
class SomePipe implements PipeTransform {
transform(v1: any, v2: any) { return [v1 + 10, v2 + 20]; }
}
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 3, 'span'),
pipeDef(NodeFlags.None, SomePipe, []), purePipeDef(2),
directiveDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(check, view) => {
const pureValue = checkNodeInlineOrDynamic(
check, view, 2, inlineDynamic, [nodeValue(view, 1)].concat(values));
checkNodeInlineOrDynamic(check, view, 3, inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 3).instance;
values = [1, 2];
Services.checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual([11, 22]);
// instance should not change
// if the values don't change
Services.checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [3, 2];
Services.checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual([13, 22]);
});
});
});
});
}

View File

@ -0,0 +1,431 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef, Injector, QueryList, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors';
import {BindingType, DebugContext, NodeDef, NodeFlags, QueryBindingType, QueryValueType, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, queryDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView} from './helper';
export function main() {
describe(`Query Views`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
return () => viewDef(ViewFlags.None, nodes, update);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
const someQueryId = 1;
class AService {}
class QueryService {
a: QueryList<AService>;
}
function contentQueryProviders() {
return [
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.All})
];
}
function compViewQueryProviders(extraChildCount: number, nodes: NodeDef[]) {
return [
elementDef(
NodeFlags.None, null, null, 1 + extraChildCount, 'div', null, null, null, null,
() => compViewDef([
queryDef(
NodeFlags.TypeViewQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.All}),
...nodes
])),
directiveDef(NodeFlags.Component, null, 0, QueryService, [], null, null, ),
];
}
function aServiceProvider() {
return directiveDef(
NodeFlags.None, [[someQueryId, QueryValueType.Provider]], 0, AService, []);
}
describe('content queries', () => {
it('should query providers on the same element and child elements', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 5, 'div'),
...contentQueryProviders(),
aServiceProvider(),
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
]));
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a).toBeUndefined();
Services.checkAndUpdateView(view);
const as = qs.a.toArray();
expect(as.length).toBe(2);
expect(as[0]).toBe(asProviderData(view, 3).instance);
expect(as[1]).toBe(asProviderData(view, 5).instance);
});
it('should not query providers on sibling or parent elements', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 6, 'div'),
aServiceProvider(),
elementDef(NodeFlags.None, null, null, 2, 'div'),
...contentQueryProviders(),
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 3).instance;
expect(qs.a.length).toBe(0);
});
});
describe('view queries', () => {
it('should query providers in the view', () => {
const {view} = createAndGetRootNodes(compViewDef([
...compViewQueryProviders(
0,
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
aServiceProvider(),
]),
]));
Services.checkAndUpdateView(view);
const comp: QueryService = asProviderData(view, 1).instance;
const compView = asElementData(view, 0).componentView;
expect(comp.a.length).toBe(1);
expect(comp.a.first).toBe(asProviderData(compView, 2).instance);
});
it('should not query providers on the host element', () => {
const {view} = createAndGetRootNodes(compViewDef([
...compViewQueryProviders(
1,
[
elementDef(NodeFlags.None, null, null, 0, 'span'),
]),
aServiceProvider(),
]));
Services.checkAndUpdateView(view);
const comp: QueryService = asProviderData(view, 1).instance;
expect(comp.a.length).toBe(0);
});
});
describe('embedded views', () => {
it('should query providers in embedded views', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 5, 'div'),
...contentQueryProviders(),
anchorDef(NodeFlags.EmbeddedViews, null, null, 2, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
])),
...contentQueryProviders(),
]));
const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
attachEmbeddedView(view, asElementData(view, 3), 0, childView);
Services.checkAndUpdateView(view);
// queries on parent elements of anchors
const qs1: QueryService = asProviderData(view, 1).instance;
expect(qs1.a.length).toBe(1);
expect(qs1.a.first instanceof AService).toBe(true);
// queries on the anchor
const qs2: QueryService = asProviderData(view, 4).instance;
expect(qs2.a.length).toBe(1);
expect(qs2.a.first instanceof AService).toBe(true);
});
it('should query providers in embedded views only at the template declaration', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'div'),
...contentQueryProviders(),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
])),
elementDef(NodeFlags.None, null, null, 3, 'div'),
...contentQueryProviders(),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0),
]));
const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
// attach at a different place than the one where the template was defined
attachEmbeddedView(view, asElementData(view, 7), 0, childView);
Services.checkAndUpdateView(view);
// query on the declaration place
const qs1: QueryService = asProviderData(view, 1).instance;
expect(qs1.a.length).toBe(1);
expect(qs1.a.first instanceof AService).toBe(true);
// query on the attach place
const qs2: QueryService = asProviderData(view, 5).instance;
expect(qs2.a.length).toBe(0);
});
it('should update content queries if embedded views are added or removed', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'div'),
...contentQueryProviders(),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
])),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a.length).toBe(0);
const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
attachEmbeddedView(view, asElementData(view, 3), 0, childView);
Services.checkAndUpdateView(view);
expect(qs.a.length).toBe(1);
detachEmbeddedView(asElementData(view, 3), 0);
Services.checkAndUpdateView(view);
expect(qs.a.length).toBe(0);
});
it('should update view queries if embedded views are added or removed', () => {
const {view} = createAndGetRootNodes(compViewDef([
...compViewQueryProviders(
0,
[
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
])),
]),
]));
Services.checkAndUpdateView(view);
const comp: QueryService = asProviderData(view, 1).instance;
expect(comp.a.length).toBe(0);
const compView = asElementData(view, 0).componentView;
const childView = Services.createEmbeddedView(compView, compView.def.nodes[1]);
attachEmbeddedView(view, asElementData(compView, 1), 0, childView);
Services.checkAndUpdateView(view);
expect(comp.a.length).toBe(1);
detachEmbeddedView(asElementData(compView, 1), 0);
Services.checkAndUpdateView(view);
expect(comp.a.length).toBe(0);
});
});
describe('QueryBindingType', () => {
it('should query all matches', () => {
class QueryService {
a: QueryList<AService>;
}
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 4, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.All}),
aServiceProvider(),
aServiceProvider(),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a instanceof QueryList).toBeTruthy();
expect(qs.a.toArray()).toEqual([
asProviderData(view, 3).instance,
asProviderData(view, 4).instance,
]);
});
it('should query the first match', () => {
class QueryService {
a: AService;
}
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 4, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
aServiceProvider(),
aServiceProvider(),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a).toBe(asProviderData(view, 3).instance);
});
});
describe('query builtins', () => {
it('should query ElementRef', () => {
class QueryService {
a: ElementRef;
}
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null, 2, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a.nativeElement).toBe(asElementData(view, 0).renderElement);
});
it('should query TemplateRef', () => {
class QueryService {
a: TemplateRef<any>;
}
const {view} = createAndGetRootNodes(compViewDef([
anchorDef(
NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null, 2, null,
embeddedViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a.createEmbeddedView).toBeTruthy();
});
it('should query ViewContainerRef', () => {
class QueryService {
a: ViewContainerRef;
}
const {view} = createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.First}),
]));
Services.checkAndUpdateView(view);
const qs: QueryService = asProviderData(view, 1).instance;
expect(qs.a.createEmbeddedView).toBeTruthy();
});
});
describe('general binding behavior', () => {
it('should checkNoChanges', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'div'),
...contentQueryProviders(),
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
aServiceProvider(),
])),
]));
Services.checkAndUpdateView(view);
Services.checkNoChangesView(view);
const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
attachEmbeddedView(view, asElementData(view, 3), 0, childView);
let err: any;
try {
Services.checkNoChangesView(view);
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message)
.toBe(
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'Query 1 not dirty'. Current value: 'Query 1 dirty'.`);
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(2);
});
it('should report debug info on binding errors', () => {
class QueryService {
set a(value: any) { throw new Error('Test'); }
}
const {view} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 3, 'div'),
directiveDef(NodeFlags.None, null, 1, QueryService, []),
queryDef(
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
{'a': QueryBindingType.All}),
aServiceProvider(),
]));
let err: any;
try {
Services.checkAndUpdateView(view);
} catch (e) {
err = e;
}
expect(err).toBeTruthy();
expect(err.message).toBe('Test');
const debugCtx = getDebugContext(err);
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(2);
});
});
});
}

View File

@ -0,0 +1,90 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core';
import {DebugContext, NodeDef, NodeFlags, QueryValueType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, asTextData, directiveDef, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {createRootView, isBrowser} from './helper';
export function main() {
describe('View Services', () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('DebugContext', () => {
class AComp {}
class AService {}
function createViewWithData() {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(NodeFlags.None, [['ref', QueryValueType.ElementRef]], null, 2, 'span'),
directiveDef(NodeFlags.None, null, 0, AService, []), textDef(null, ['a'])
])),
directiveDef(NodeFlags.Component, null, 0, AComp, []),
]));
return view;
}
it('should provide data for elements', () => {
const view = createViewWithData();
const compView = asElementData(view, 0).componentView;
const debugCtx = Services.createDebugContext(compView, 0);
expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement);
expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement);
expect(debugCtx.injector.get(AComp)).toBe(compView.component);
expect(debugCtx.component).toBe(compView.component);
expect(debugCtx.context).toBe(compView.context);
expect(debugCtx.providerTokens).toEqual([AService]);
expect(debugCtx.source).toBeTruthy();
expect(debugCtx.references['ref'].nativeElement)
.toBe(asElementData(compView, 0).renderElement);
});
it('should provide data for text nodes', () => {
const view = createViewWithData();
const compView = asElementData(view, 0).componentView;
const debugCtx = Services.createDebugContext(compView, 2);
expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement);
expect(debugCtx.renderNode).toBe(asTextData(compView, 2).renderText);
expect(debugCtx.injector.get(AComp)).toBe(compView.component);
expect(debugCtx.component).toBe(compView.component);
expect(debugCtx.context).toBe(compView.context);
expect(debugCtx.source).toBeTruthy();
});
it('should provide data for other nodes based on the nearest element parent', () => {
const view = createViewWithData();
const compView = asElementData(view, 0).componentView;
const debugCtx = Services.createDebugContext(compView, 1);
expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement);
});
});
});
}

View File

@ -0,0 +1,84 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {ArgumentType, DebugContext, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asTextData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser} from './helper';
export function main() {
describe(`View Text`, () => {
function compViewDef(
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
}
function createAndGetRootNodes(
viewDef: ViewDefinition, context?: any): {rootNodes: any[], view: ViewData} {
const view = createRootView(viewDef, context);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
describe('create', () => {
it('should create text nodes without parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([textDef(null, ['a'])])).rootNodes;
expect(rootNodes.length).toBe(1);
expect(getDOM().getText(rootNodes[0])).toBe('a');
});
it('should create views with multiple root text nodes', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
textDef(null, ['a']), textDef(null, ['b'])
])).rootNodes;
expect(rootNodes.length).toBe(2);
});
it('should create text nodes with parents', () => {
const rootNodes = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
textDef(null, ['a']),
])).rootNodes;
expect(rootNodes.length).toBe(1);
const textNode = getDOM().firstChild(rootNodes[0]);
expect(getDOM().getText(textNode)).toBe('a');
});
it('should add debug information to the renderer', () => {
const someContext = new Object();
const {view, rootNodes} =
createAndGetRootNodes(compViewDef([textDef(null, ['a'])]), someContext);
expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asTextData(view, 0).renderText);
});
});
describe('change text', () => {
ARG_TYPE_VALUES.forEach((inlineDynamic) => {
it(`should update via strategy ${inlineDynamic}`, () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
textDef(null, ['0', '1', '2']),
],
null, (check, view) => {
checkNodeInlineOrDynamic(check, view, 0, inlineDynamic, ['a', 'b']);
}));
Services.checkAndUpdateView(view);
const node = rootNodes[0];
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
});
});
});
});
}

View File

@ -0,0 +1,219 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NodeFlags, QueryValueType, ViewData, ViewDefinition, ViewFlags, anchorDef, directiveDef, elementDef, textDef, viewDef} from '@angular/core/src/view/index';
import {filterQueryId} from '@angular/core/src/view/util';
export function main() {
describe('viewDef', () => {
describe('parent', () => {
function parents(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.parent ? node.parent.index : null);
}
it('should calculate parents for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
textDef(null, ['a']),
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([null, 0, 0]);
});
it('should calculate parents for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
textDef(null, ['a']),
elementDef(NodeFlags.None, null, null, 1, 'span'),
textDef(null, ['a']),
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([null, 0, null, 2, null]);
});
it('should calculate parents for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
textDef(null, ['a']),
elementDef(NodeFlags.None, null, null, 1, 'span'),
textDef(null, ['a']),
textDef(null, ['a']),
]);
expect(parents(vd)).toEqual([null, 0, 1, null, 3, null]);
});
});
describe('childFlags', () => {
function childFlags(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.childFlags);
}
function directChildFlags(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.directChildFlags);
}
it('should calculate childFlags for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.AfterContentChecked, null, 0, AService, [])
]);
expect(childFlags(vd)).toEqual([
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None
]);
expect(directChildFlags(vd)).toEqual([
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None
]);
});
it('should calculate childFlags for two levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.AfterContentChecked, null, 0, AService, [])
]);
expect(childFlags(vd)).toEqual([
NodeFlags.TypeElement | NodeFlags.TypeDirective | NodeFlags.AfterContentChecked,
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None
]);
expect(directChildFlags(vd)).toEqual([
NodeFlags.TypeElement, NodeFlags.TypeDirective | NodeFlags.AfterContentChecked,
NodeFlags.None
]);
});
it('should calculate childFlags for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.AfterContentChecked, null, 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.AfterContentInit, null, 0, AService, []),
directiveDef(NodeFlags.AfterViewChecked, null, 0, AService, []),
]);
expect(childFlags(vd)).toEqual([
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None,
NodeFlags.TypeDirective | NodeFlags.AfterContentInit | NodeFlags.AfterViewChecked,
NodeFlags.None, NodeFlags.None
]);
expect(directChildFlags(vd)).toEqual([
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None,
NodeFlags.TypeDirective | NodeFlags.AfterContentInit | NodeFlags.AfterViewChecked,
NodeFlags.None, NodeFlags.None
]);
});
it('should calculate childFlags for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.AfterContentChecked, null, 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.AfterContentInit, null, 0, AService, []),
directiveDef(NodeFlags.AfterViewInit, null, 0, AService, []),
]);
expect(childFlags(vd)).toEqual([
NodeFlags.TypeElement | NodeFlags.TypeDirective | NodeFlags.AfterContentChecked,
NodeFlags.TypeDirective | NodeFlags.AfterContentChecked, NodeFlags.None,
NodeFlags.TypeDirective | NodeFlags.AfterContentInit | NodeFlags.AfterViewInit,
NodeFlags.None, NodeFlags.None
]);
expect(directChildFlags(vd)).toEqual([
NodeFlags.TypeElement, NodeFlags.TypeDirective | NodeFlags.AfterContentChecked,
NodeFlags.None,
NodeFlags.TypeDirective | NodeFlags.AfterContentInit | NodeFlags.AfterViewInit,
NodeFlags.None, NodeFlags.None
]);
});
});
describe('childMatchedQueries', () => {
function childMatchedQueries(viewDef: ViewDefinition): number[] {
return viewDef.nodes.map(node => node.childMatchedQueries);
}
it('should calculate childMatchedQueries for one level', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, [])
]);
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), 0]);
});
it('should calculate childMatchedQueries for two levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, [])
]);
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), filterQueryId(1), 0]);
});
it('should calculate childMatchedQueries for one level, multiple roots', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.None, [[2, QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[3, QueryValueType.Provider]], 0, AService, []),
]);
expect(childMatchedQueries(vd)).toEqual([
filterQueryId(1), 0, filterQueryId(2) | filterQueryId(3), 0, 0
]);
});
it('should calculate childMatchedQueries for multiple levels', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 2, 'span'),
elementDef(NodeFlags.None, null, null, 1, 'span'),
directiveDef(NodeFlags.None, [[1, QueryValueType.Provider]], 0, AService, []),
elementDef(NodeFlags.None, null, null, 2, 'span'),
directiveDef(NodeFlags.None, [[2, QueryValueType.Provider]], 0, AService, []),
directiveDef(NodeFlags.None, [[3, QueryValueType.Provider]], 0, AService, []),
]);
expect(childMatchedQueries(vd)).toEqual([
filterQueryId(1), filterQueryId(1), 0, filterQueryId(2) | filterQueryId(3), 0, 0
]);
});
it('should included embedded views into childMatchedQueries', () => {
const vd = viewDef(ViewFlags.None, [
elementDef(NodeFlags.None, null, null, 1, 'span'),
anchorDef(
NodeFlags.None, null, null, 0, null,
() => viewDef(
ViewFlags.None,
[
elementDef(NodeFlags.None, [[1, QueryValueType.Provider]], null, 0, 'span'),
]))
]);
// Note: the template will become a sibling to the anchor once stamped out,
expect(childMatchedQueries(vd)).toEqual([filterQueryId(1), 0]);
});
});
});
}
class AService {}

View File

@ -0,0 +1,795 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {async, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {AsyncTestCompleter, Log, beforeEach, describe, expect, inject, it, xit} from '@angular/core/testing/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
import {scheduleMicroTask} from '../../src/util';
const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge;
const resultTimer = 1000;
const testTimeout = browserDetection.isEdge ? 1200 : 500;
// Schedules a macrotask (using a timer)
function macroTask(fn: (...args: any[]) => void, timer = 1): void {
// adds longer timers for passing tests in IE and Edge
setTimeout(fn, needsLongerTimers ? timer : 1);
}
let _log: Log;
let _errors: any[];
let _traces: any[];
let _zone: NgZone;
const resolvedPromise = Promise.resolve(null);
function logOnError() {
_zone.onError.subscribe({
next: (error: any) => {
_errors.push(error);
_traces.push(error.stack);
}
});
}
function logOnUnstable() {
_zone.onUnstable.subscribe({next: _log.fn('onUnstable')});
}
function logOnMicrotaskEmpty() {
_zone.onMicrotaskEmpty.subscribe({next: _log.fn('onMicrotaskEmpty')});
}
function logOnStable() {
_zone.onStable.subscribe({next: _log.fn('onStable')});
}
function runNgZoneNoLog(fn: () => any) {
const length = _log.logItems.length;
try {
return _zone.run(fn);
} finally {
// delete anything which may have gotten logged.
_log.logItems.length = length;
}
}
export function main() {
describe('NgZone', () => {
function createZone(enableLongStackTrace: boolean) {
return new NgZone({enableLongStackTrace: enableLongStackTrace});
}
beforeEach(() => {
_log = new Log();
_errors = [];
_traces = [];
});
describe('long stack trace', () => {
beforeEach(() => {
_zone = createZone(true);
logOnUnstable();
logOnMicrotaskEmpty();
logOnStable();
logOnError();
});
commonTests();
it('should produce long stack traces',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => {
let resolve: (result: any) => void;
const promise: Promise<any> = new Promise((res) => { resolve = res; });
_zone.run(() => {
setTimeout(() => {
setTimeout(() => {
resolve(null);
throw new Error('ccc');
}, 0);
}, 0);
});
promise.then((_) => {
expect(_traces.length).toBe(1);
expect(_traces[0].length).toBeGreaterThan(1);
async.done();
});
});
}), testTimeout);
it('should produce long stack traces (when using microtasks)',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => {
let resolve: (result: any) => void;
const promise: Promise<any> = new Promise((res) => { resolve = res; });
_zone.run(() => {
scheduleMicroTask(() => {
scheduleMicroTask(() => {
resolve(null);
throw new Error('ddd');
});
});
});
promise.then((_) => {
expect(_traces.length).toBe(1);
expect(_traces[0].length).toBeGreaterThan(1);
async.done();
});
});
}), testTimeout);
});
describe('short stack trace', () => {
beforeEach(() => {
_zone = createZone(false);
logOnUnstable();
logOnMicrotaskEmpty();
logOnStable();
logOnError();
});
commonTests();
it('should disable long stack traces',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => {
let resolve: (result: any) => void;
const promise: Promise<any> = new Promise((res) => { resolve = res; });
_zone.run(() => {
setTimeout(() => {
setTimeout(() => {
resolve(null);
throw new Error('ccc');
}, 0);
}, 0);
});
promise.then((_) => {
expect(_traces.length).toBe(1);
if (_traces[0] != null) {
// some browsers don't have stack traces.
expect(_traces[0].indexOf('---')).toEqual(-1);
}
async.done();
});
});
}), testTimeout);
});
});
}
function commonTests() {
describe('hasPendingMicrotasks', () => {
it('should be false', () => { expect(_zone.hasPendingMicrotasks).toBe(false); });
it('should be true', () => {
runNgZoneNoLog(() => { scheduleMicroTask(() => {}); });
expect(_zone.hasPendingMicrotasks).toBe(true);
});
});
describe('hasPendingTimers', () => {
it('should be false', () => { expect(_zone.hasPendingMacrotasks).toBe(false); });
it('should be true', () => {
runNgZoneNoLog(() => { setTimeout(() => {}, 0); });
expect(_zone.hasPendingMacrotasks).toBe(true);
});
});
describe('hasPendingAsyncTasks', () => {
it('should be false', () => { expect(_zone.hasPendingMicrotasks).toBe(false); });
it('should be true when microtask is scheduled', () => {
runNgZoneNoLog(() => { scheduleMicroTask(() => {}); });
expect(_zone.hasPendingMicrotasks).toBe(true);
});
it('should be true when timer is scheduled', () => {
runNgZoneNoLog(() => { setTimeout(() => {}, 0); });
expect(_zone.hasPendingMacrotasks).toBe(true);
});
});
describe('isInInnerZone', () => {
it('should return whether the code executes in the inner zone', () => {
expect(NgZone.isInAngularZone()).toEqual(false);
runNgZoneNoLog(() => { expect(NgZone.isInAngularZone()).toEqual(true); });
}, testTimeout);
});
describe('run', () => {
it('should return the body return value from run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => { expect(_zone.run(() => 6)).toEqual(6); });
macroTask(() => { async.done(); });
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run')));
macroTask(() => {
expect(_log.result()).toEqual('onUnstable; run; onMicrotaskEmpty; onStable');
async.done();
});
}), testTimeout);
it('should call onStable once at the end of event',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
// The test is set up in a way that causes the zone loop to run onMicrotaskEmpty twice
// then verified that onStable is only called once at the end
runNgZoneNoLog(() => macroTask(_log.fn('run')));
let times = 0;
_zone.onMicrotaskEmpty.subscribe({
next: () => {
times++;
_log.add(`onMicrotaskEmpty ${times}`);
if (times < 2) {
// Scheduling a microtask causes a second digest
runNgZoneNoLog(() => { scheduleMicroTask(() => {}); });
}
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; run; onMicrotaskEmpty; onMicrotaskEmpty 1; ' +
'onMicrotaskEmpty; onMicrotaskEmpty 2; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call standalone onStable',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run')));
macroTask(() => {
expect(_log.result()).toEqual('onUnstable; run; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
xit('should run subscriber listeners in the subscription zone (outside)',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
// Each subscriber fires a microtask outside the Angular zone. The test
// then verifies that those microtasks do not cause additional digests.
let turnStart = false;
_zone.onUnstable.subscribe({
next: () => {
if (turnStart) throw 'Should not call this more than once';
_log.add('onUnstable');
scheduleMicroTask(() => {});
turnStart = true;
}
});
let turnDone = false;
_zone.onMicrotaskEmpty.subscribe({
next: () => {
if (turnDone) throw 'Should not call this more than once';
_log.add('onMicrotaskEmpty');
scheduleMicroTask(() => {});
turnDone = true;
}
});
let eventDone = false;
_zone.onStable.subscribe({
next: () => {
if (eventDone) throw 'Should not call this more than once';
_log.add('onStable');
scheduleMicroTask(() => {});
eventDone = true;
}
});
macroTask(() => { _zone.run(_log.fn('run')); });
macroTask(() => {
expect(_log.result()).toEqual('onUnstable; run; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should run subscriber listeners in the subscription zone (inside)',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run')));
// the only practical use-case to run a callback inside the zone is
// change detection after "onMicrotaskEmpty". That's the only case tested.
let turnDone = false;
_zone.onMicrotaskEmpty.subscribe({
next: () => {
_log.add('onMyMicrotaskEmpty');
if (turnDone) return;
_zone.run(() => { scheduleMicroTask(() => {}); });
turnDone = true;
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; run; onMicrotaskEmpty; onMyMicrotaskEmpty; ' +
'onMicrotaskEmpty; onMyMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should run async tasks scheduled inside onStable outside Angular zone',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run')));
_zone.onStable.subscribe({
next: () => {
NgZone.assertNotInAngularZone();
_log.add('onMyTaskDone');
}
});
macroTask(() => {
expect(_log.result())
.toEqual('onUnstable; run; onMicrotaskEmpty; onStable; onMyTaskDone');
async.done();
});
}), testTimeout);
it('should call onUnstable once before a turn and onMicrotaskEmpty once after the turn',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => {
macroTask(() => {
_log.add('run start');
scheduleMicroTask(_log.fn('async'));
_log.add('run end');
});
});
macroTask(() => {
// The microtask (async) is executed after the macrotask (run)
expect(_log.result())
.toEqual('onUnstable; run start; run end; async; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should not run onUnstable and onMicrotaskEmpty for nested Zone.run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => {
macroTask(() => {
_log.add('start run');
_zone.run(() => {
_log.add('nested run');
scheduleMicroTask(_log.fn('nested run microtask'));
});
_log.add('end run');
});
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; start run; nested run; end run; nested run microtask; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should not run onUnstable and onMicrotaskEmpty for nested Zone.run invoked from onMicrotaskEmpty',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('start run')));
_zone.onMicrotaskEmpty.subscribe({
next: () => {
_log.add('onMicrotaskEmpty:started');
_zone.run(() => _log.add('nested run'));
_log.add('onMicrotaskEmpty:finished');
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; start run; onMicrotaskEmpty; onMicrotaskEmpty:started; nested run; onMicrotaskEmpty:finished; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty before and after each top-level run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run1')));
runNgZoneNoLog(() => macroTask(_log.fn('run2')));
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; run1; onMicrotaskEmpty; onStable; onUnstable; run2; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty before and after each turn',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
let aResolve: (result: string) => void;
let aPromise: Promise<string>;
let bResolve: (result: string) => void;
let bPromise: Promise<string>;
runNgZoneNoLog(() => {
macroTask(() => {
aPromise = new Promise(res => { aResolve = res; });
bPromise = new Promise(res => { bResolve = res; });
_log.add('run start');
aPromise.then(_log.fn('a then'));
bPromise.then(_log.fn('b then'));
});
});
runNgZoneNoLog(() => {
macroTask(() => {
aResolve('a');
bResolve('b');
});
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; run start; onMicrotaskEmpty; onStable; onUnstable; a then; b then; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should run a function outside of the angular zone',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => { _zone.runOutsideAngular(_log.fn('run')); });
macroTask(() => {
expect(_log.result()).toEqual('run');
async.done();
});
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty when an inner microtask is scheduled from outside angular',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
let resolve: (result: string) => void;
let promise: Promise<string>;
macroTask(() => {
NgZone.assertNotInAngularZone();
promise = new Promise(res => { resolve = res; });
});
runNgZoneNoLog(() => {
macroTask(() => {
NgZone.assertInAngularZone();
promise.then(_log.fn('executedMicrotask'));
});
});
macroTask(() => {
NgZone.assertNotInAngularZone();
_log.add('scheduling a microtask');
resolve(null);
});
macroTask(() => {
expect(_log.result())
.toEqual(
// First VM turn => setup Promise then
'onUnstable; onMicrotaskEmpty; onStable; ' +
// Second VM turn (outside of angular)
'scheduling a microtask; onUnstable; ' +
// Third VM Turn => execute the microtask (inside angular)
// No onUnstable; because we don't own the task which started the turn.
'executedMicrotask; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable only before executing a microtask scheduled in onMicrotaskEmpty ' +
'and not onMicrotaskEmpty after executing the task',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => macroTask(_log.fn('run')));
let ran = false;
_zone.onMicrotaskEmpty.subscribe({
next: () => {
_log.add('onMicrotaskEmpty(begin)');
if (!ran) {
_zone.run(() => {
scheduleMicroTask(() => {
ran = true;
_log.add('executedMicrotask');
});
});
}
_log.add('onMicrotaskEmpty(end)');
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
// First VM turn => 'run' macrotask
'onUnstable; run; onMicrotaskEmpty; onMicrotaskEmpty(begin); onMicrotaskEmpty(end); ' +
// Second microtaskDrain Turn => microtask enqueued from onMicrotaskEmpty
'executedMicrotask; onMicrotaskEmpty; onMicrotaskEmpty(begin); onMicrotaskEmpty(end); onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty for a scheduleMicroTask in onMicrotaskEmpty triggered by ' +
'a scheduleMicroTask in run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => {
macroTask(() => {
_log.add('scheduleMicroTask');
scheduleMicroTask(_log.fn('run(executeMicrotask)'));
});
});
let ran = false;
_zone.onMicrotaskEmpty.subscribe({
next: () => {
_log.add('onMicrotaskEmpty(begin)');
if (!ran) {
_log.add('onMicrotaskEmpty(scheduleMicroTask)');
_zone.run(() => {
scheduleMicroTask(() => {
ran = true;
_log.add('onMicrotaskEmpty(executeMicrotask)');
});
});
}
_log.add('onMicrotaskEmpty(end)');
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
// First VM Turn => a macrotask + the microtask it enqueues
'onUnstable; scheduleMicroTask; run(executeMicrotask); onMicrotaskEmpty; onMicrotaskEmpty(begin); onMicrotaskEmpty(scheduleMicroTask); onMicrotaskEmpty(end); ' +
// Second VM Turn => the microtask enqueued from onMicrotaskEmpty
'onMicrotaskEmpty(executeMicrotask); onMicrotaskEmpty; onMicrotaskEmpty(begin); onMicrotaskEmpty(end); onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should execute promises scheduled in onUnstable before promises scheduled in run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => {
macroTask(() => {
_log.add('run start');
resolvedPromise
.then((_) => {
_log.add('promise then');
resolvedPromise.then(_log.fn('promise foo'));
return Promise.resolve(null);
})
.then(_log.fn('promise bar'));
_log.add('run end');
});
});
let donePromiseRan = false;
let startPromiseRan = false;
_zone.onUnstable.subscribe({
next: () => {
_log.add('onUnstable(begin)');
if (!startPromiseRan) {
_log.add('onUnstable(schedulePromise)');
_zone.run(() => { scheduleMicroTask(_log.fn('onUnstable(executePromise)')); });
startPromiseRan = true;
}
_log.add('onUnstable(end)');
}
});
_zone.onMicrotaskEmpty.subscribe({
next: () => {
_log.add('onMicrotaskEmpty(begin)');
if (!donePromiseRan) {
_log.add('onMicrotaskEmpty(schedulePromise)');
_zone.run(() => { scheduleMicroTask(_log.fn('onMicrotaskEmpty(executePromise)')); });
donePromiseRan = true;
}
_log.add('onMicrotaskEmpty(end)');
}
});
macroTask(() => {
expect(_log.result())
.toEqual(
// First VM turn: enqueue a microtask in onUnstable
'onUnstable; onUnstable(begin); onUnstable(schedulePromise); onUnstable(end); ' +
// First VM turn: execute the macrotask which enqueues microtasks
'run start; run end; ' +
// First VM turn: execute enqueued microtasks
'onUnstable(executePromise); promise then; promise foo; promise bar; onMicrotaskEmpty; ' +
// First VM turn: onTurnEnd, enqueue a microtask
'onMicrotaskEmpty(begin); onMicrotaskEmpty(schedulePromise); onMicrotaskEmpty(end); ' +
// Second VM turn: execute the microtask from onTurnEnd
'onMicrotaskEmpty(executePromise); onMicrotaskEmpty; onMicrotaskEmpty(begin); onMicrotaskEmpty(end); onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty before and after each turn, respectively',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
let aResolve: (result: string) => void;
let aPromise: Promise<string>;
let bResolve: (result: string) => void;
let bPromise: Promise<string>;
runNgZoneNoLog(() => {
macroTask(() => {
aPromise = new Promise(res => { aResolve = res; });
bPromise = new Promise(res => { bResolve = res; });
aPromise.then(_log.fn('a then'));
bPromise.then(_log.fn('b then'));
_log.add('run start');
});
});
runNgZoneNoLog(() => { macroTask(() => { aResolve(null); }, 10); });
runNgZoneNoLog(() => { macroTask(() => { bResolve(null); }, 20); });
macroTask(() => {
expect(_log.result())
.toEqual(
// First VM turn
'onUnstable; run start; onMicrotaskEmpty; onStable; ' +
// Second VM turn
'onUnstable; a then; onMicrotaskEmpty; onStable; ' +
// Third VM turn
'onUnstable; b then; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty before and after (respectively) all turns in a chain',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
runNgZoneNoLog(() => {
macroTask(() => {
_log.add('run start');
scheduleMicroTask(() => {
_log.add('async1');
scheduleMicroTask(_log.fn('async2'));
});
_log.add('run end');
});
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; run start; run end; async1; async2; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
it('should call onUnstable and onMicrotaskEmpty for promises created outside of run body',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
let promise: Promise<any>;
runNgZoneNoLog(() => {
macroTask(() => {
_zone.runOutsideAngular(
() => { promise = Promise.resolve(4).then((x) => Promise.resolve(x)); });
promise.then(_log.fn('promise then'));
_log.add('zone run');
});
});
macroTask(() => {
expect(_log.result())
.toEqual(
'onUnstable; zone run; onMicrotaskEmpty; onStable; ' +
'onUnstable; promise then; onMicrotaskEmpty; onStable');
async.done();
}, resultTimer);
}), testTimeout);
});
describe('exceptions', () => {
it('should call the on error callback when it is invoked via zone.runGuarded',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => {
const exception = new Error('sync');
_zone.runGuarded(() => { throw exception; });
expect(_errors.length).toBe(1);
expect(_errors[0]).toBe(exception);
async.done();
});
}), testTimeout);
it('should not call the on error callback but rethrow when it is invoked via zone.run',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
macroTask(() => {
const exception = new Error('sync');
expect(() => _zone.run(() => { throw exception; })).toThrowError('sync');
expect(_errors.length).toBe(0);
async.done();
});
}), testTimeout);
it('should call onError for errors from microtasks',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const exception = new Error('async');
macroTask(() => { _zone.run(() => { scheduleMicroTask(() => { throw exception; }); }); });
macroTask(() => {
expect(_errors.length).toBe(1);
expect(_errors[0]).toEqual(exception);
async.done();
}, resultTimer);
}), testTimeout);
});
describe('bugs', () => {
describe('#10503', () => {
let ngZone: NgZone;
beforeEach(inject([NgZone], (_ngZone: NgZone) => {
// Create a zone outside the fakeAsync.
ngZone = _ngZone;
}));
it('should fakeAsync even if the NgZone was created outside.', fakeAsync(() => {
let result: string = null;
// try to escape the current fakeAsync zone by using NgZone which was created outside.
ngZone.run(() => {
Promise.resolve('works').then((v) => result = v);
flushMicrotasks();
});
expect(result).toEqual('works');
}));
describe('async', () => {
let asyncResult: string;
const waitLongerThenTestFrameworkAsyncTimeout = 5;
beforeEach(() => { asyncResult = null; });
it('should async even if the NgZone was created outside.', async(() => {
// try to escape the current async zone by using NgZone which was created outside.
ngZone.run(() => {
setTimeout(() => {
Promise.resolve('works').then((v) => asyncResult = v);
}, waitLongerThenTestFrameworkAsyncTimeout);
});
}));
afterEach(() => { expect(asyncResult).toEqual('works'); });
});
});
});
}