Victor Berchet 0833b59aab refactor(core): add a checkIndex to the compiler view nodes
Each node now has two index: nodeIndex and checkIndex.

nodeIndex is the index in both the view definition and the view data.
checkIndex is the index in in the update function (update directives and update
renderer).

While nodeIndex and checkIndex have the same value for now, having both of them
will allow changing the structure of view definition after compilation (ie for
runtime translations).
2017-10-04 14:55:54 -07:00

551 lines
21 KiB
TypeScript

/**
* @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, ErrorHandler, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, Renderer, Renderer2, SimpleChange, TemplateRef, ViewContainerRef,} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors';
import {ArgumentType, DepFlags, NodeFlags, Services, anchorDef, asElementData, directiveDef, elementDef, providerDef, textDef} from '@angular/core/src/view/index';
import {TestBed, withModule} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetRootNodes, compViewDef, compViewDefFactory} from './helper';
export function main() {
describe(`View Providers`, () => {
describe('create', () => {
let instance: SomeService;
class SomeService {
constructor(public dep: any) { instance = this; }
}
beforeEach(() => { instance = null !; });
it('should create providers eagerly', () => {
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, NodeFlags.None, null, 0, SomeService, [])
]));
expect(instance instanceof SomeService).toBe(true);
});
it('should create providers lazily', () => {
let lazy: LazyService = undefined !;
class LazyService {
constructor() { lazy = this; }
}
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
providerDef(
NodeFlags.TypeClassProvider | NodeFlags.LazyProvider, null, LazyService, LazyService,
[]),
directiveDef(2, 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(0, NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
directiveDef(2, NodeFlags.None, null, 0, SomeService, ['someToken']),
]));
expect(instance.dep).toBe('someValue');
});
it('should create factory providers', () => {
function someFactory() { return 'someValue'; }
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeFactoryProvider, null, 'someToken', someFactory, []),
directiveDef(2, NodeFlags.None, null, 0, SomeService, ['someToken']),
]));
expect(instance.dep).toBe('someValue');
});
it('should create useExisting providers', () => {
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 3, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someExistingToken', 'someValue', []),
providerDef(
NodeFlags.TypeUseExistingProvider, null, 'someToken', null, ['someExistingToken']),
directiveDef(3, 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(
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([textDef(0, null, ['a'])])),
directiveDef(1, 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(0, NodeFlags.None, null, null, 2, 'span'),
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
directiveDef(2, NodeFlags.None, null, 0, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should inject deps from a parent element', () => {
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 3, 'span'),
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
directiveDef(3, NodeFlags.None, null, 0, SomeService, [Dep])
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should not inject deps from sibling root elements', () => {
const rootElNodes = [
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, NodeFlags.None, null, 0, Dep, []),
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
directiveDef(3, NodeFlags.None, null, 0, SomeService, [Dep]),
];
expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
.toThrowError(
'StaticInjectorError[Dep]: \n' +
' StaticInjectorError[Dep]: \n' +
' NullInjectorError: No provider for Dep!');
const nonRootElNodes = [
elementDef(0, NodeFlags.None, null, null, 4, 'span'),
elementDef(1, NodeFlags.None, null, null, 1, 'span'),
directiveDef(2, NodeFlags.None, null, 0, Dep, []),
elementDef(3, NodeFlags.None, null, null, 1, 'span'),
directiveDef(4, NodeFlags.None, null, 0, SomeService, [Dep]),
];
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
.toThrowError(
'StaticInjectorError[Dep]: \n' +
' StaticInjectorError[Dep]: \n' +
' NullInjectorError: No provider for Dep!');
});
it('should inject from a parent element in a parent view', () => {
createAndGetRootNodes(compViewDef([
elementDef(
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, NodeFlags.None, null, 0, SomeService, [Dep])
])),
directiveDef(1, NodeFlags.Component, null, 0, Dep, []),
]));
expect(instance.dep instanceof Dep).toBeTruthy();
});
it('should throw for missing dependencies', () => {
expect(() => createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
])))
.toThrowError(
'StaticInjectorError[nonExistingDep]: \n' +
' StaticInjectorError[nonExistingDep]: \n' +
' NullInjectorError: No provider for nonExistingDep!');
});
it('should use null for optional missing dependencies', () => {
createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(
1, 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(0, NodeFlags.None, null, null, 4, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someParentValue', []),
elementDef(2, NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.TypeValueProvider, null, 'someToken', 'someValue', []),
directiveDef(
4, 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(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, 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(1, NodeFlags.None, null, 0, SomeService, [ViewContainerRef]),
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject TemplateRef', () => {
createAndGetRootNodes(compViewDef([
anchorDef(NodeFlags.None, null, null, 1, null, compViewDefFactory([anchorDef(
NodeFlags.None, null, null, 0)])),
directiveDef(1, NodeFlags.None, null, 0, SomeService, [TemplateRef]),
]));
expect(instance.dep.createEmbeddedView).toBeTruthy();
});
it('should inject ElementRef', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, NodeFlags.None, null, 0, SomeService, [ElementRef]),
]));
expect(instance.dep.nativeElement).toBe(asElementData(view, 0).renderElement);
});
it('should inject Injector', () => {
const {view} = createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, 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(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, 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(
0, NodeFlags.None, null, null, 1, 'div', null, null, null, null,
() => compViewDef([
elementDef(0, NodeFlags.None, null, null, 0, 'span'),
])),
directiveDef(1, 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(
0, NodeFlags.None, null, null, 1, 'span', null, null, null, null,
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(1, NodeFlags.Component, null, 0, SomeService, [Renderer])
]));
expect(instance.dep.createElement).toBeTruthy();
});
it('should inject Renderer2', () => {
createAndGetRootNodes(compViewDef([
elementDef(
0, NodeFlags.None, null, null, 1, 'span', null, null, null, null,
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
directiveDef(1, 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 = undefined !;
class SomeService {
a: any;
b: any;
constructor() { instance = this; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(
1, 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 {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(0, NodeFlags.None, null, null, 1, 'span', null, null, null, handleEvent),
directiveDef(
1, 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', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
let emitter = new EventEmitter<any>();
class SomeService {
emitter = emitter;
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(
0, NodeFlags.None, null, null, 1, 'span', null, null, null,
() => { throw new Error('Test'); }),
directiveDef(
1, NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'})
]));
emitter.emit('someEventInstance');
const err = handleErrorSpy.calls.mostRecent().args[0];
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(0, NodeFlags.None, null, null, 3, 'span'),
directiveDef(1, allFlags, null, 0, SomeService, [], {a: [0, 'a']}),
elementDef(2, NodeFlags.None, null, null, 1, 'span'),
directiveDef(3, 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(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(
1, 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(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, 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(0, NodeFlags.None, null, null, 1, 'span'),
directiveDef(1, 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);
});
});
});
}