refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
598
packages/core/test/animation/animation_integration_spec.ts
Normal file
598
packages/core/test/animation/animation_integration_spec.ts
Normal 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();
|
||||
}
|
||||
}
|
51
packages/core/test/application_init_spec.ts
Normal file
51
packages/core/test/application_init_spec.ts
Normal 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);
|
||||
});
|
||||
})));
|
||||
});
|
||||
});
|
||||
}
|
17
packages/core/test/application_module_spec.ts
Normal file
17
packages/core/test/application_module_spec.ts
Normal 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'); }));
|
||||
});
|
||||
}
|
515
packages/core/test/application_ref_spec.ts
Normal file
515
packages/core/test/application_ref_spec.ts
Normal 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); }
|
||||
}
|
@ -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); });
|
||||
});
|
||||
});
|
||||
}
|
@ -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']
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
@ -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\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
16
packages/core/test/change_detection/iterable.ts
Normal file
16
packages/core/test/change_detection/iterable.ts
Normal 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()](); }
|
||||
}
|
37
packages/core/test/change_detection/util.ts
Normal file
37
packages/core/test/change_detection/util.ts
Normal 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';
|
||||
}
|
324
packages/core/test/component_fixture_spec.ts
Normal file
324
packages/core/test/component_fixture_spec.ts
Normal 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)');
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
357
packages/core/test/debug/debug_node_spec.ts
Normal file
357
packages/core/test/debug/debug_node_spec.ts
Normal 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);
|
||||
|
||||
});
|
||||
});
|
||||
}
|
15
packages/core/test/dev_mode_spec.ts
Normal file
15
packages/core/test/dev_mode_spec.ts
Normal 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); });
|
||||
});
|
||||
}
|
21
packages/core/test/di/forward_ref_spec.ts
Normal file
21
packages/core/test/di/forward_ref_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
27
packages/core/test/di/injector_spec.ts
Normal file
27
packages/core/test/di/injector_spec.ts
Normal 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'); });
|
||||
});
|
||||
}
|
529
packages/core/test/di/reflective_injector_spec.ts
Normal file
529
packages/core/test/di/reflective_injector_spec.ts
Normal 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" ])');
|
||||
});
|
||||
});
|
||||
}
|
27
packages/core/test/di/reflective_key_spec.ts
Normal file
27
packages/core/test/di/reflective_key_spec.ts
Normal 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')); });
|
||||
|
||||
});
|
||||
}
|
86
packages/core/test/directive_lifecycle_integration_spec.ts
Normal file
86
packages/core/test/directive_lifecycle_integration_spec.ts
Normal 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 {
|
||||
}
|
108
packages/core/test/dom/dom_adapter_spec.ts
Normal file
108
packages/core/test/dom/dom_adapter_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
27
packages/core/test/dom/shim_spec.ts
Normal file
27
packages/core/test/dom/shim_spec.ts
Normal 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;
|
||||
}
|
106
packages/core/test/error_handler_spec.ts
Normal file
106
packages/core/test/error_handler_spec.ts
Normal 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;
|
||||
}
|
133
packages/core/test/event_emitter_spec.ts
Normal file
133
packages/core/test/event_emitter_spec.ts
Normal 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
|
||||
});
|
||||
}
|
318
packages/core/test/fake_async_spec.ts
Normal file
318
packages/core/test/fake_async_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
66
packages/core/test/forward_ref_integration_spec.ts
Normal file
66
packages/core/test/forward_ref_integration_spec.ts
Normal 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';
|
||||
}
|
1665
packages/core/test/linker/change_detection_integration_spec.ts
Normal file
1665
packages/core/test/linker/change_detection_integration_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
118
packages/core/test/linker/entry_components_integration_spec.ts
Normal file
118
packages/core/test/linker/entry_components_integration_spec.ts
Normal 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 {
|
||||
}
|
2287
packages/core/test/linker/integration_spec.ts
Normal file
2287
packages/core/test/linker/integration_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
185
packages/core/test/linker/ng_container_integration_spec.ts
Normal file
185
packages/core/test/linker/ng_container_integration_spec.ts
Normal 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;
|
||||
}
|
1160
packages/core/test/linker/ng_module_integration_spec.ts
Normal file
1160
packages/core/test/linker/ng_module_integration_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
703
packages/core/test/linker/projection_integration_spec.ts
Normal file
703
packages/core/test/linker/projection_integration_spec.ts
Normal 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 {
|
||||
}
|
818
packages/core/test/linker/query_integration_spec.ts
Normal file
818
packages/core/test/linker/query_integration_spec.ts
Normal 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(); }
|
||||
}
|
152
packages/core/test/linker/query_list_spec.ts
Normal file
152
packages/core/test/linker/query_list_spec.ts
Normal 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);
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
287
packages/core/test/linker/regression_integration_spec.ts
Normal file
287
packages/core/test/linker/regression_integration_spec.ts
Normal 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 {
|
||||
}
|
252
packages/core/test/linker/security_integration_spec.ts
Normal file
252
packages/core/test/linker/security_integration_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
};
|
732
packages/core/test/linker/view_injector_integration_spec.ts
Normal file
732
packages/core/test/linker/view_injector_integration_spec.ts
Normal 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) {}
|
||||
}
|
32
packages/core/test/metadata/decorators_spec.ts
Normal file
32
packages/core/test/metadata/decorators_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
101
packages/core/test/metadata/di_spec.ts
Normal file
101
packages/core/test/metadata/di_spec.ts
Normal 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>;
|
||||
}
|
503
packages/core/test/reflection/reflector_spec.ts
Normal file
503
packages/core/test/reflection/reflector_spec.ts
Normal 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')]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
30
packages/core/test/spies.ts
Normal file
30
packages/core/test/spies.ts
Normal 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); }
|
||||
}
|
285
packages/core/test/testability/testability_spec.ts
Normal file
285
packages/core/test/testability/testability_spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
117
packages/core/test/testing_internal_spec.ts
Normal file
117
packages/core/test/testing_internal_spec.ts
Normal 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(); });
|
||||
});
|
||||
});
|
||||
}
|
163
packages/core/test/util/decorators_spec.ts
Normal file
163
packages/core/test/util/decorators_spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
47
packages/core/test/util/lang_spec.ts
Normal file
47
packages/core/test/util/lang_spec.ts
Normal 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));
|
||||
});
|
||||
}
|
62
packages/core/test/view/anchor_spec.ts
Normal file
62
packages/core/test/view/anchor_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
312
packages/core/test/view/component_view_spec.ts
Normal file
312
packages/core/test/view/component_view_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
302
packages/core/test/view/element_spec.ts
Normal file
302
packages/core/test/view/element_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
189
packages/core/test/view/embedded_view_spec.ts
Normal file
189
packages/core/test/view/embedded_view_spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
}
|
41
packages/core/test/view/helper.ts
Normal file
41
packages/core/test/view/helper.ts
Normal 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)); });
|
139
packages/core/test/view/ng_content_spec.ts
Normal file
139
packages/core/test/view/ng_content_spec.ts
Normal 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]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
554
packages/core/test/view/provider_spec.ts
Normal file
554
packages/core/test/view/provider_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
149
packages/core/test/view/pure_expression_spec.ts
Normal file
149
packages/core/test/view/pure_expression_spec.ts
Normal 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]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
431
packages/core/test/view/query_spec.ts
Normal file
431
packages/core/test/view/query_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
90
packages/core/test/view/services_spec.ts
Normal file
90
packages/core/test/view/services_spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
84
packages/core/test/view/text_spec.ts
Normal file
84
packages/core/test/view/text_spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
219
packages/core/test/view/view_def_spec.ts
Normal file
219
packages/core/test/view/view_def_spec.ts
Normal 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 {}
|
795
packages/core/test/zone/ng_zone_spec.ts
Normal file
795
packages/core/test/zone/ng_zone_spec.ts
Normal 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'); });
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user