feat(ivy): support context discovery for containers & ICU expressions (#27644)

Context discovery was only available on elements. This PR adds support for containers and ICU expressions.
FW-378 #resolve
FW-665 #comment linker integration tests

PR Close #27644
This commit is contained in:
Olivier Combe 2018-12-17 14:19:25 +01:00 committed by Miško Hevery
parent 062c7af4f3
commit 4c1cd1bb78
6 changed files with 153 additions and 113 deletions

View File

@ -6,15 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import './ng_dev_mode'; import './ng_dev_mode';
import {assertDomNode} from './assert'; import {assertDomNode} from './assert';
import {EMPTY_ARRAY} from './definition'; import {EMPTY_ARRAY} from './definition';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node'; import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer'; import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; import {getComponentViewByIndex, getNativeByTNode, getTNode, readElementValue, readPatchedData} from './util';
/** Returns the matching `LContext` data for a given DOM node, directive or component instance. /** Returns the matching `LContext` data for a given DOM node, directive or component instance.
@ -67,8 +65,8 @@ export function getLContext(target: any): LContext|null {
} }
// the goal is not to fill the entire context full of data because the lookups // the goal is not to fill the entire context full of data because the lookups
// are expensive. Instead, only the target data (the element, compontent or // are expensive. Instead, only the target data (the element, component, container, ICU
// directive details) are filled into the context. If called multiple times // expression or directive details) are filled into the context. If called multiple times
// with different target values then the missing target data will be filled in. // with different target values then the missing target data will be filled in.
const native = readElementValue(lView[nodeIndex]); const native = readElementValue(lView[nodeIndex]);
const existingCtx = readPatchedData(native); const existingCtx = readPatchedData(native);
@ -208,10 +206,15 @@ function traverseNextElement(tNode: TNode): TNode|null {
return tNode.child; return tNode.child;
} else if (tNode.next) { } else if (tNode.next) {
return tNode.next; return tNode.next;
} else if (tNode.parent) { } else {
return tNode.parent.next || null; // Let's take the following template: <div><span>text</span></div><component/>
// After checking the text node, we need to find the next parent that has a "next" TNode,
// in this case the parent `div`, so that we can find the component.
while (tNode.parent && !tNode.parent.next) {
tNode = tNode.parent;
}
return tNode.parent && tNode.parent.next;
} }
return null;
} }
/** /**

View File

@ -9,8 +9,8 @@
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body'; import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {assertDefined, assertEqual, assertGreaterThan} from './assert'; import {assertDefined, assertEqual, assertGreaterThan} from './assert';
import {attachPatchData} from './context_discovery';
import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions'; import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions';
import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container'; import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
@ -337,9 +337,7 @@ const parentIndexStack: number[] = [];
export function i18nStart(index: number, message: string, subTemplateIndex?: number): void { export function i18nStart(index: number, message: string, subTemplateIndex?: number): void {
const tView = getLView()[TVIEW]; const tView = getLView()[TVIEW];
ngDevMode && assertDefined(tView, `tView should be defined`); ngDevMode && assertDefined(tView, `tView should be defined`);
ngDevMode && i18nIndexStack[++i18nIndexStackPointer] = index;
assertEqual(
tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`);
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
i18nStartFirstPass(tView, index, message, subTemplateIndex); i18nStartFirstPass(tView, index, message, subTemplateIndex);
} }
@ -350,7 +348,6 @@ export function i18nStart(index: number, message: string, subTemplateIndex?: num
*/ */
function i18nStartFirstPass( function i18nStartFirstPass(
tView: TView, index: number, message: string, subTemplateIndex?: number) { tView: TView, index: number, message: string, subTemplateIndex?: number) {
i18nIndexStack[++i18nIndexStackPointer] = index;
const viewData = getLView(); const viewData = getLView();
const expandoStartIndex = tView.blueprint.length - HEADER_OFFSET; const expandoStartIndex = tView.blueprint.length - HEADER_OFFSET;
const previousOrParentTNode = getPreviousOrParentTNode(); const previousOrParentTNode = getPreviousOrParentTNode();
@ -484,7 +481,6 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
// Nodes that inject ViewContainerRef also have a comment node that should be moved // Nodes that inject ViewContainerRef also have a comment node that should be moved
appendChild(slotValue[NATIVE], tNode, viewData); appendChild(slotValue[NATIVE], tNode, viewData);
} }
return tNode; return tNode;
} }
@ -566,12 +562,7 @@ export function i18nPostprocess(
export function i18nEnd(): void { export function i18nEnd(): void {
const tView = getLView()[TVIEW]; const tView = getLView()[TVIEW];
ngDevMode && assertDefined(tView, `tView should be defined`); ngDevMode && assertDefined(tView, `tView should be defined`);
ngDevMode && i18nEndFirstPass(tView);
assertEqual(
tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`);
if (tView.firstTemplatePass) {
i18nEndFirstPass(tView);
}
} }
/** /**
@ -675,6 +666,7 @@ function readCreateOpCodes(
previousTNode = currentTNode; previousTNode = currentTNode;
currentTNode = createNodeAtIndex( currentTNode = createNodeAtIndex(
expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null); expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null);
attachPatchData(commentRNode, viewData);
(currentTNode as TIcuContainerNode).activeCaseIndex = null; (currentTNode as TIcuContainerNode).activeCaseIndex = null;
// We will add the case nodes later, during the update phase // We will add the case nodes later, during the update phase
setIsParent(false); setIsParent(false);

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {resolveForwardRef} from '../di/forward_ref'; import {resolveForwardRef} from '../di/forward_ref';
import {InjectionToken} from '../di/injection_token'; import {InjectionToken} from '../di/injection_token';
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
@ -44,7 +43,6 @@ import {NO_CHANGE} from './tokens';
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util';
/** /**
* A permanent marker promise which signifies that the current CD tree is * A permanent marker promise which signifies that the current CD tree is
* clean. * clean.
@ -212,24 +210,26 @@ export function createNodeAtIndex(
let tNode = tView.data[adjustedIndex] as TNode; let tNode = tView.data[adjustedIndex] as TNode;
if (tNode == null) { if (tNode == null) {
const previousOrParentTNode = getPreviousOrParentTNode();
const isParent = getIsParent();
// TODO(misko): Refactor createTNode so that it does not depend on LView. // TODO(misko): Refactor createTNode so that it does not depend on LView.
tNode = tView.data[adjustedIndex] = createTNode(lView, type, adjustedIndex, name, attrs, null); tNode = tView.data[adjustedIndex] = createTNode(lView, type, adjustedIndex, name, attrs, null);
}
// Now link ourselves into the tree. // Now link ourselves into the tree.
if (previousOrParentTNode) { // We need this even if tNode exists, otherwise we might end up pointing to unexisting tNodes when
if (isParent && previousOrParentTNode.child == null && // we use i18n (especially with ICU expressions that update the DOM during the update phase).
(tNode.parent !== null || previousOrParentTNode.type === TNodeType.View)) { const previousOrParentTNode = getPreviousOrParentTNode();
// We are in the same view, which means we are adding content node to the parent view. const isParent = getIsParent();
previousOrParentTNode.child = tNode; if (previousOrParentTNode) {
} else if (!isParent) { if (isParent && previousOrParentTNode.child == null &&
previousOrParentTNode.next = tNode; (tNode.parent !== null || previousOrParentTNode.type === TNodeType.View)) {
} // We are in the same view, which means we are adding content node to the parent view.
previousOrParentTNode.child = tNode;
} else if (!isParent) {
previousOrParentTNode.next = tNode;
} }
} }
if (tView.firstChild == null && type === TNodeType.Element) { if (tView.firstChild == null) {
tView.firstChild = tNode; tView.firstChild = tNode;
} }
@ -504,6 +504,7 @@ export function elementContainerStart(
appendChild(native, tNode, lView); appendChild(native, tNode, lView);
createDirectivesAndLocals(tView, lView, localRefs); createDirectivesAndLocals(tView, lView, localRefs);
attachPatchData(native, lView);
} }
/** Mark the end of the <ng-container>. */ /** Mark the end of the <ng-container>. */
@ -1921,6 +1922,8 @@ export function template(
createDirectivesAndLocals(tView, lView, localRefs, localRefExtractor); createDirectivesAndLocals(tView, lView, localRefs, localRefExtractor);
const currentQueries = lView[QUERIES]; const currentQueries = lView[QUERIES];
const previousOrParentTNode = getPreviousOrParentTNode(); const previousOrParentTNode = getPreviousOrParentTNode();
const native = getNativeByTNode(previousOrParentTNode, lView);
attachPatchData(native, lView);
if (currentQueries) { if (currentQueries) {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode); lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
} }

View File

@ -284,7 +284,7 @@ export interface TView {
/** /**
* Pointer to the `TNode` that represents the root of the view. * Pointer to the `TNode` that represents the root of the view.
* *
* If this is a `TNode` for an `LViewNode`, this is an embedded view of a container. * If this is a `TViewNode` for an `LViewNode`, this is an embedded view of a container.
* We need this pointer to be able to efficiently find this node when inserting the view * We need this pointer to be able to efficiently find this node when inserting the view
* into an anchor. * into an anchor.
* *

View File

@ -399,29 +399,30 @@ function declareTests(config?: {useJit: boolean}) {
expect(fixture.nativeElement).toHaveText('baz'); expect(fixture.nativeElement).toHaveText('baz');
}); });
fixmeIvy( it('should not detach views in ViewContainers when the parent view is destroyed.', () => {
'FW-665: Discovery util fails with "Unable to find the given context data for the given target"') TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
.it('should not detach views in ViewContainers when the parent view is destroyed.', () => { const template =
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]}); '<div *ngIf="ctxBoolProp"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>';
const template = TestBed.overrideComponent(MyComp, {set: {template}});
'<div *ngIf="ctxBoolProp"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>'; const fixture = TestBed.createComponent(MyComp);
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true; fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges(); fixture.detectChanges();
const ngIfEl = fixture.debugElement.children[0]; const ngIfEl = fixture.debugElement.children[0];
const someViewport: SomeViewport = ngIfEl.childNodes[0].injector.get(SomeViewport); const someViewport: SomeViewport =
expect(someViewport.container.length).toBe(2); ngIfEl.childNodes
expect(ngIfEl.children.length).toBe(2); .find(debugElement => debugElement.nativeNode.nodeType === Node.COMMENT_NODE)
.injector.get(SomeViewport);
expect(someViewport.container.length).toBe(2);
expect(ngIfEl.children.length).toBe(2);
fixture.componentInstance.ctxBoolProp = false; fixture.componentInstance.ctxBoolProp = false;
fixture.detectChanges(); fixture.detectChanges();
expect(someViewport.container.length).toBe(2); expect(someViewport.container.length).toBe(2);
expect(fixture.debugElement.children.length).toBe(0); expect(fixture.debugElement.children.length).toBe(0);
}); });
it('should use a comment while stamping out `<ng-template>` elements.', () => { it('should use a comment while stamping out `<ng-template>` elements.', () => {
const fixture = const fixture =
@ -798,40 +799,37 @@ function declareTests(config?: {useJit: boolean}) {
emitter.fireEvent('fired !'); emitter.fireEvent('fired !');
})); }));
fixmeIvy( it('should support events via EventEmitter on template elements', async(() => {
'FW-665: Discovery util fails with Unable to find the given context data for the given target') const fixture =
.it('should support events via EventEmitter on template elements', async(() => { TestBed
const fixture = .configureTestingModule(
TestBed {declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]})
.configureTestingModule({ .overrideComponent(MyComp, {
declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent] set: {
}) template:
.overrideComponent(MyComp, { '<ng-template emitter listener (event)="ctxProp=$event"></ng-template>'
set: { }
template: })
'<ng-template emitter listener (event)="ctxProp=$event"></ng-template>' .createComponent(MyComp);
} const tc = fixture.debugElement.childNodes.find(
}) debugElement => debugElement.nativeNode.nodeType === Node.COMMENT_NODE);
.createComponent(MyComp);
const tc = fixture.debugElement.childNodes[0]; const emitter = tc.injector.get(DirectiveEmittingEvent);
const myComp = fixture.debugElement.injector.get(MyComp);
const listener = tc.injector.get(DirectiveListeningEvent);
const emitter = tc.injector.get(DirectiveEmittingEvent); myComp.ctxProp = '';
const myComp = fixture.debugElement.injector.get(MyComp); expect(listener.msg).toEqual('');
const listener = tc.injector.get(DirectiveListeningEvent);
myComp.ctxProp = ''; emitter.event.subscribe({
expect(listener.msg).toEqual(''); next: () => {
expect(listener.msg).toEqual('fired !');
expect(myComp.ctxProp).toEqual('fired !');
}
});
emitter.event.subscribe({ emitter.fireEvent('fired !');
next: () => { }));
expect(listener.msg).toEqual('fired !');
expect(myComp.ctxProp).toEqual('fired !');
}
});
emitter.fireEvent('fired !');
}));
it('should support [()] syntax', async(() => { it('should support [()] syntax', async(() => {
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTwoWayBinding]}); TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTwoWayBinding]});

View File

@ -8,17 +8,15 @@
import {createInjector} from '@angular/core'; import {createInjector} from '@angular/core';
import {StaticInjector} from '../../src/di/injector'; import {StaticInjector} from '../../src/di/injector';
import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent} from '../../src/render3/discovery_utils'; import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent, loadLContext} from '../../src/render3/discovery_utils';
import {ProvidersFeature, RenderFlags, defineComponent, defineDirective, getHostElement} from '../../src/render3/index'; import {ProvidersFeature, RenderFlags, defineComponent, defineDirective, elementContainerEnd, elementContainerStart, getHostElement, i18n, i18nApply, i18nExp} from '../../src/render3/index';
import {element, elementEnd, elementStart, elementStyling, elementStylingApply, template, bind, elementProperty, text, textBinding, markDirty, listener} from '../../src/render3/instructions'; import {element, elementEnd, elementStart, elementStyling, elementStylingApply, template, bind, elementProperty, text, textBinding, markDirty, listener} from '../../src/render3/instructions';
import {ComponentFixture} from './render_util'; import {ComponentFixture} from './render_util';
import {NgIf} from './common_with_def'; import {NgIf} from './common_with_def';
describe('discovery utils', () => { describe('discovery utils', () => {
let fixture: ComponentFixture<MyApp>; let fixture: ComponentFixture<MyApp>;
let myApp: MyApp[]; let myApp: MyApp;
let dirA: DirectiveA[]; let dirA: DirectiveA[];
let childComponent: DirectiveA[]; let childComponent: DirectiveA[];
let child: NodeListOf<Element>; let child: NodeListOf<Element>;
@ -29,7 +27,6 @@ describe('discovery utils', () => {
beforeEach(() => { beforeEach(() => {
log = []; log = [];
myApp = [];
dirA = []; dirA = [];
childComponent = []; childComponent = [];
fixture = new ComponentFixture( fixture = new ComponentFixture(
@ -63,6 +60,7 @@ describe('discovery utils', () => {
* <p></p> * <p></p>
* <VIEW> * <VIEW>
* </child> * </child>
* <i18n>ICU expression</i18n>
* </#VIEW> * </#VIEW>
* </my-app> * </my-app>
* ``` * ```
@ -96,15 +94,19 @@ describe('discovery utils', () => {
}); });
} }
const MSG_DIV = `{<7B>0<EFBFBD>, select,
other {ICU expression}
}`;
class MyApp { class MyApp {
text: string = 'INIT'; text: string = 'INIT';
constructor() { myApp.push(this); } constructor() { myApp = this; }
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
type: MyApp, type: MyApp,
selectors: [['my-app']], selectors: [['my-app']],
factory: () => new MyApp(), factory: () => new MyApp(),
consts: 9, consts: 13,
vars: 1, vars: 1,
directives: [Child, DirectiveA, NgIf], directives: [Child, DirectiveA, NgIf],
template: (rf: RenderFlags, ctx: MyApp) => { template: (rf: RenderFlags, ctx: MyApp) => {
@ -121,10 +123,18 @@ describe('discovery utils', () => {
element(0, 'child'); element(0, 'child');
} }
}, 1, 0, 'child', ['ngIf', '']); }, 1, 0, 'child', ['ngIf', '']);
elementStart(9, 'i18n');
i18n(10, MSG_DIV);
elementEnd();
elementContainerStart(11);
{ text(12, 'content'); }
elementContainerEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
textBinding(1, bind(ctx.text)); textBinding(1, bind(ctx.text));
elementProperty(8, 'ngIf', bind(true)); elementProperty(8, 'ngIf', bind(true));
i18nExp(bind(ctx.text));
i18nApply(10);
} }
} }
}); });
@ -141,7 +151,7 @@ describe('discovery utils', () => {
expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/); expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
}); });
it('should return component from element', () => { it('should return component from element', () => {
expect(getComponent<MyApp>(fixture.hostElement)).toEqual(myApp[0]); expect(getComponent<MyApp>(fixture.hostElement)).toEqual(myApp);
expect(getComponent<Child>(child[0])).toEqual(childComponent[0]); expect(getComponent<Child>(child[0])).toEqual(childComponent[0]);
expect(getComponent<Child>(child[1])).toEqual(childComponent[1]); expect(getComponent<Child>(child[1])).toEqual(childComponent[1]);
}); });
@ -153,7 +163,7 @@ describe('discovery utils', () => {
expect(() => getContext(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/); expect(() => getContext(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
}); });
it('should return context from element', () => { it('should return context from element', () => {
expect(getContext<MyApp>(child[0])).toEqual(myApp[0]); expect(getContext<MyApp>(child[0])).toEqual(myApp);
expect(getContext<{$implicit: boolean}>(child[2]) !.$implicit).toEqual(true); expect(getContext<{$implicit: boolean}>(child[2]) !.$implicit).toEqual(true);
expect(getContext<Child>(p[0])).toEqual(childComponent[0]); expect(getContext<Child>(p[0])).toEqual(childComponent[0]);
}); });
@ -161,7 +171,7 @@ describe('discovery utils', () => {
describe('getHostElement', () => { describe('getHostElement', () => {
it('should return element on component', () => { it('should return element on component', () => {
expect(getHostElement(myApp[0])).toEqual(fixture.hostElement); expect(getHostElement(myApp)).toEqual(fixture.hostElement);
expect(getHostElement(childComponent[0])).toEqual(child[0]); expect(getHostElement(childComponent[0])).toEqual(child[0]);
expect(getHostElement(childComponent[1])).toEqual(child[1]); expect(getHostElement(childComponent[1])).toEqual(child[1]);
}); });
@ -181,7 +191,7 @@ describe('discovery utils', () => {
expect(getInjector(p[0]).get(String)).toEqual('Child'); expect(getInjector(p[0]).get(String)).toEqual('Child');
}); });
it('should return node-injector from component with providers', () => { it('should return node-injector from component with providers', () => {
expect(getInjector(myApp[0]).get(String)).toEqual('Module'); expect(getInjector(myApp).get(String)).toEqual('Module');
expect(getInjector(childComponent[0]).get(String)).toEqual('Child'); expect(getInjector(childComponent[0]).get(String)).toEqual('Child');
expect(getInjector(childComponent[1]).get(String)).toEqual('Child'); expect(getInjector(childComponent[1]).get(String)).toEqual('Child');
}); });
@ -206,34 +216,34 @@ describe('discovery utils', () => {
describe('getViewComponent', () => { describe('getViewComponent', () => {
it('should return null when called on root component', () => { it('should return null when called on root component', () => {
expect(getViewComponent(fixture.hostElement)).toEqual(null); expect(getViewComponent(fixture.hostElement)).toEqual(null);
expect(getViewComponent(myApp[0])).toEqual(null); expect(getViewComponent(myApp)).toEqual(null);
}); });
it('should return containing component of child component', () => { it('should return containing component of child component', () => {
expect(getViewComponent<MyApp>(child[0])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(child[0])).toEqual(myApp);
expect(getViewComponent<MyApp>(child[1])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(child[1])).toEqual(myApp);
expect(getViewComponent<MyApp>(child[2])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(child[2])).toEqual(myApp);
expect(getViewComponent<MyApp>(childComponent[0])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(childComponent[0])).toEqual(myApp);
expect(getViewComponent<MyApp>(childComponent[1])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(childComponent[1])).toEqual(myApp);
expect(getViewComponent<MyApp>(childComponent[2])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(childComponent[2])).toEqual(myApp);
}); });
it('should return containing component of any view element', () => { it('should return containing component of any view element', () => {
expect(getViewComponent<MyApp>(span[0])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(span[0])).toEqual(myApp);
expect(getViewComponent<MyApp>(div[0])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(div[0])).toEqual(myApp);
expect(getViewComponent<Child>(p[0])).toEqual(childComponent[0]); expect(getViewComponent<Child>(p[0])).toEqual(childComponent[0]);
expect(getViewComponent<Child>(p[1])).toEqual(childComponent[1]); expect(getViewComponent<Child>(p[1])).toEqual(childComponent[1]);
expect(getViewComponent<Child>(p[2])).toEqual(childComponent[2]); expect(getViewComponent<Child>(p[2])).toEqual(childComponent[2]);
}); });
it('should return containing component of child directive', () => { it('should return containing component of child directive', () => {
expect(getViewComponent<MyApp>(dirA[0])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(dirA[0])).toEqual(myApp);
expect(getViewComponent<MyApp>(dirA[1])).toEqual(myApp[0]); expect(getViewComponent<MyApp>(dirA[1])).toEqual(myApp);
}); });
}); });
describe('getLocalRefs', () => { describe('getLocalRefs', () => {
it('should retrieve empty map', () => { it('should retrieve empty map', () => {
expect(getLocalRefs(fixture.hostElement)).toEqual({}); expect(getLocalRefs(fixture.hostElement)).toEqual({});
expect(getLocalRefs(myApp[0])).toEqual({}); expect(getLocalRefs(myApp)).toEqual({});
expect(getLocalRefs(span[0])).toEqual({}); expect(getLocalRefs(span[0])).toEqual({});
expect(getLocalRefs(child[0])).toEqual({}); expect(getLocalRefs(child[0])).toEqual({});
}); });
@ -249,8 +259,8 @@ describe('discovery utils', () => {
describe('getRootComponents', () => { describe('getRootComponents', () => {
it('should return root components from component', () => { it('should return root components from component', () => {
const rootComponents = [myApp[0]]; const rootComponents = [myApp];
expect(getRootComponents(myApp[0])).toEqual(rootComponents); expect(getRootComponents(myApp)).toEqual(rootComponents);
expect(getRootComponents(childComponent[0])).toEqual(rootComponents); expect(getRootComponents(childComponent[0])).toEqual(rootComponents);
expect(getRootComponents(childComponent[1])).toEqual(rootComponents); expect(getRootComponents(childComponent[1])).toEqual(rootComponents);
expect(getRootComponents(dirA[0])).toEqual(rootComponents); expect(getRootComponents(dirA[0])).toEqual(rootComponents);
@ -289,12 +299,46 @@ describe('discovery utils', () => {
describe('markDirty', () => { describe('markDirty', () => {
it('should re-render component', () => { it('should re-render component', () => {
expect(span[0].textContent).toEqual('INIT'); expect(span[0].textContent).toEqual('INIT');
myApp[0].text = 'WORKS'; myApp.text = 'WORKS';
markDirty(myApp[0]); markDirty(myApp);
fixture.requestAnimationFrame.flush(); fixture.requestAnimationFrame.flush();
expect(span[0].textContent).toEqual('WORKS'); expect(span[0].textContent).toEqual('WORKS');
}); });
}); });
describe('loadLContext', () => {
it('should work on components', () => {
const lContext = loadLContext(child[0]);
expect(lContext).toBeDefined();
expect(lContext.native as any).toBe(child[0]);
});
it('should work on templates', () => {
const templateComment = Array.from(fixture.hostElement.childNodes)
.find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE);
const lContext = loadLContext(templateComment);
expect(lContext).toBeDefined();
expect(lContext.native as any).toBe(templateComment);
});
it('should work on ICU expressions', () => {
const icuComment = Array.from(fixture.hostElement.querySelector('i18n') !.childNodes)
.find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE);
const lContext = loadLContext(icuComment);
expect(lContext).toBeDefined();
expect(lContext.native as any).toBe(icuComment);
});
it('should work on ng-container', () => {
const ngContainerComment = Array.from(fixture.hostElement.childNodes)
.find(
(node: ChildNode) => node.nodeType === Node.COMMENT_NODE &&
node.textContent === `ng-container`);
const lContext = loadLContext(ngContainerComment);
expect(lContext).toBeDefined();
expect(lContext.native as any).toBe(ngContainerComment);
});
});
}); });
describe('discovery utils deprecated', () => { describe('discovery utils deprecated', () => {