/** * @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 {RendererType2} from '../../src/render/api'; import {getLContext} from '../../src/render3/context_discovery'; import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index'; import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all'; import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer} from '../../src/sanitization/sanitizer'; import {SecurityContext} from '../../src/sanitization/security'; import {NgIf} from './common_with_def'; import {ComponentFixture, MockRendererFactory, renderToHtml} from './render_util'; describe('render3 integration test', () => { describe('render', () => { describe('text bindings', () => { it('should support creation-time values in text nodes', () => { function Template(rf: RenderFlags, value: string) { if (rf & RenderFlags.Create) { ɵɵtext(0, value); } } expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once'); expect(ngDevMode).toHaveProperties({ firstTemplatePass: 0, tNode: 2, tView: 2, // 1 for root view, 1 for template rendererSetText: 1, }); }); }); }); describe('tree', () => { interface Tree { beforeLabel?: string; subTrees?: Tree[]; afterLabel?: string; } interface ParentCtx { beforeTree: Tree; projectedTree: Tree; afterTree: Tree; } function showLabel(rf: RenderFlags, ctx: {label: string | undefined}) { if (rf & RenderFlags.Create) { ɵɵcontainer(0); } if (rf & RenderFlags.Update) { ɵɵcontainerRefreshStart(0); { if (ctx.label != null) { let rf1 = ɵɵembeddedViewStart(0, 1, 1); if (rf1 & RenderFlags.Create) { ɵɵtext(0); } if (rf1 & RenderFlags.Update) { ɵɵtextInterpolate(ctx.label); } ɵɵembeddedViewEnd(); } } ɵɵcontainerRefreshEnd(); } } function showTree(rf: RenderFlags, ctx: {tree: Tree}) { if (rf & RenderFlags.Create) { ɵɵcontainer(0); ɵɵcontainer(1); ɵɵcontainer(2); } if (rf & RenderFlags.Update) { ɵɵcontainerRefreshStart(0); { const rf0 = ɵɵembeddedViewStart(0, 1, 0); { showLabel(rf0, {label: ctx.tree.beforeLabel}); } ɵɵembeddedViewEnd(); } ɵɵcontainerRefreshEnd(); ɵɵcontainerRefreshStart(1); { for (let subTree of ctx.tree.subTrees || []) { const rf0 = ɵɵembeddedViewStart(0, 3, 0); { showTree(rf0, {tree: subTree}); } ɵɵembeddedViewEnd(); } } ɵɵcontainerRefreshEnd(); ɵɵcontainerRefreshStart(2); { const rf0 = ɵɵembeddedViewStart(0, 1, 0); { showLabel(rf0, {label: ctx.tree.afterLabel}); } ɵɵembeddedViewEnd(); } ɵɵcontainerRefreshEnd(); } } class ChildComponent { // TODO(issue/24571): remove '!'. beforeTree !: Tree; // TODO(issue/24571): remove '!'. afterTree !: Tree; static ngFactoryDef = () => new ChildComponent; static ngComponentDef = ɵɵdefineComponent({ selectors: [['child']], type: ChildComponent, consts: 3, vars: 0, template: function ChildComponentTemplate( rf: RenderFlags, ctx: {beforeTree: Tree, afterTree: Tree}) { if (rf & RenderFlags.Create) { ɵɵprojectionDef(); ɵɵcontainer(0); ɵɵprojection(1); ɵɵcontainer(2); } if (rf & RenderFlags.Update) { ɵɵcontainerRefreshStart(0); { const rf0 = ɵɵembeddedViewStart(0, 3, 0); { showTree(rf0, {tree: ctx.beforeTree}); } ɵɵembeddedViewEnd(); } ɵɵcontainerRefreshEnd(); ɵɵcontainerRefreshStart(2); { const rf0 = ɵɵembeddedViewStart(0, 3, 0); { showTree(rf0, {tree: ctx.afterTree}); } ɵɵembeddedViewEnd(); } ɵɵcontainerRefreshEnd(); } }, inputs: {beforeTree: 'beforeTree', afterTree: 'afterTree'} }); } function parentTemplate(rf: RenderFlags, ctx: ParentCtx) { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'child'); { ɵɵcontainer(1); } ɵɵelementEnd(); } if (rf & RenderFlags.Update) { ɵɵproperty('beforeTree', ctx.beforeTree); ɵɵproperty('afterTree', ctx.afterTree); ɵɵcontainerRefreshStart(1); { const rf0 = ɵɵembeddedViewStart(0, 3, 0); { showTree(rf0, {tree: ctx.projectedTree}); } ɵɵembeddedViewEnd(); } ɵɵcontainerRefreshEnd(); } } it('should work with a tree', () => { const ctx: ParentCtx = { beforeTree: {subTrees: [{beforeLabel: 'a'}]}, projectedTree: {beforeLabel: 'p'}, afterTree: {afterLabel: 'z'} }; const defs = [ChildComponent]; expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('apz'); ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]}; ctx.beforeTree.subTrees !.push({afterLabel: 'b'}); expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('abz'); ctx.projectedTree.subTrees ![1].afterLabel = 'h'; expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('abhz'); ctx.beforeTree.subTrees !.push({beforeLabel: 'c'}); expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('abchz'); // To check the context easily: // console.log(JSON.stringify(ctx)); }); }); }); describe('component styles', () => { it('should pass in the component styles directly into the underlying renderer', () => { class StyledComp { static ngFactoryDef = () => new StyledComp(); static ngComponentDef = ɵɵdefineComponent({ type: StyledComp, styles: ['div { color: red; }'], consts: 1, vars: 0, encapsulation: 100, selectors: [['foo']], template: (rf: RenderFlags, ctx: StyledComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div'); } } }); } const rendererFactory = new ProxyRenderer3Factory(); new ComponentFixture(StyledComp, {rendererFactory}); expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']); expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100); }); }); describe('component animations', () => { it('should pass in the component styles directly into the underlying renderer', () => { const animA = {name: 'a'}; const animB = {name: 'b'}; class AnimComp { static ngFactoryDef = () => new AnimComp(); static ngComponentDef = ɵɵdefineComponent({ type: AnimComp, consts: 0, vars: 0, data: { animation: [ animA, animB, ], }, selectors: [['foo']], template: (rf: RenderFlags, ctx: AnimComp) => {} }); } const rendererFactory = new ProxyRenderer3Factory(); new ComponentFixture(AnimComp, {rendererFactory}); const capturedAnimations = rendererFactory.lastCapturedType !.data !['animation']; expect(Array.isArray(capturedAnimations)).toBeTruthy(); expect(capturedAnimations.length).toEqual(2); expect(capturedAnimations).toContain(animA); expect(capturedAnimations).toContain(animB); }); it('should include animations in the renderType data array even if the array is empty', () => { class AnimComp { static ngFactoryDef = () => new AnimComp(); static ngComponentDef = ɵɵdefineComponent({ type: AnimComp, consts: 0, vars: 0, data: { animation: [], }, selectors: [['foo']], template: (rf: RenderFlags, ctx: AnimComp) => {} }); } const rendererFactory = new ProxyRenderer3Factory(); new ComponentFixture(AnimComp, {rendererFactory}); const data = rendererFactory.lastCapturedType !.data; expect(data.animation).toEqual([]); }); it('should allow [@trigger] bindings to be picked up by the underlying renderer', () => { class AnimComp { static ngFactoryDef = () => new AnimComp(); static ngComponentDef = ɵɵdefineComponent({ type: AnimComp, consts: 1, vars: 1, selectors: [['foo']], template: (rf: RenderFlags, ctx: AnimComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div', [AttributeMarker.Bindings, '@fooAnimation']); } if (rf & RenderFlags.Update) { ɵɵattribute('@fooAnimation', ctx.animationValue); } } }); animationValue = '123'; } const rendererFactory = new MockRendererFactory(['setAttribute']); const fixture = new ComponentFixture(AnimComp, {rendererFactory}); const renderer = rendererFactory.lastRenderer !; fixture.component.animationValue = '456'; fixture.update(); const spy = renderer.spies['setAttribute']; const [elm, attr, value] = spy.calls.mostRecent().args; expect(attr).toEqual('@fooAnimation'); expect(value).toEqual('456'); }); it('should allow creation-level [@trigger] properties to be picked up by the underlying renderer', () => { class AnimComp { static ngFactoryDef = () => new AnimComp(); static ngComponentDef = ɵɵdefineComponent({ type: AnimComp, consts: 1, vars: 1, selectors: [['foo']], template: (rf: RenderFlags, ctx: AnimComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div', ['@fooAnimation', '']); } } }); } const rendererFactory = new MockRendererFactory(['setProperty']); const fixture = new ComponentFixture(AnimComp, {rendererFactory}); const renderer = rendererFactory.lastRenderer !; fixture.update(); const spy = renderer.spies['setProperty']; const [elm, attr, value] = spy.calls.mostRecent().args; expect(attr).toEqual('@fooAnimation'); }); // TODO(benlesh): this test does not seem to be testing anything we could actually generate with // these instructions. ɵɵbind should be present in the ɵɵelementProperty call in the hostBindings, // however adding that causes an error because the slot has not been allocated. There is a // directive called `comp-with-anim`, that seems to want to be a component, but is defined as a // directive that is looking for a property `@fooAnim` to update. // it('should allow host binding animations to be picked up and rendered', () => { // class ChildCompWithAnim { // static ngFactoryDef = () => new ChildCompWithAnim(); // static ngDirectiveDef = ɵɵdefineDirective({ // type: ChildCompWithAnim, // selectors: [['child-comp-with-anim']], // hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { // if (rf & RenderFlags.Update) { // ɵɵelementProperty(0, '@fooAnim', ctx.exp); // } // }, // }); // exp = 'go'; // } // class ParentComp { // static ngFactoryDef = () => new ParentComp(); // static ngComponentDef = ɵɵdefineComponent({ // type: ParentComp, // consts: 1, // vars: 1, // selectors: [['foo']], // template: (rf: RenderFlags, ctx: ParentComp) => { // if (rf & RenderFlags.Create) { // ɵɵelement(0, 'child-comp-with-anim'); // } // }, // directives: [ChildCompWithAnim] // }); // } // const rendererFactory = new MockRendererFactory(['setProperty']); // const fixture = new ComponentFixture(ParentComp, {rendererFactory}); // const renderer = rendererFactory.lastRenderer !; // fixture.update(); // const spy = renderer.spies['setProperty']; // const [elm, attr, value] = spy.calls.mostRecent().args; // expect(attr).toEqual('@fooAnim'); // }); }); describe('element discovery', () => { it('should only monkey-patch immediate child nodes in a component', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'div'); ɵɵelementStart(1, 'p'); ɵɵelementEnd(); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const host = fixture.hostElement; const parent = host.querySelector('div') as any; const child = host.querySelector('p') as any; expect(parent[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); }); it('should only monkey-patch immediate child nodes in a sub component', () => { class ChildComp { static ngFactoryDef = () => new ChildComp(); static ngComponentDef = ɵɵdefineComponent({ type: ChildComp, selectors: [['child-comp']], consts: 3, vars: 0, template: (rf: RenderFlags, ctx: ChildComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div'); ɵɵelement(1, 'div'); ɵɵelement(2, 'div'); } } }); } class ParentComp { static ngFactoryDef = () => new ParentComp(); static ngComponentDef = ɵɵdefineComponent({ type: ParentComp, selectors: [['parent-comp']], directives: [ChildComp], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: ParentComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵelementStart(1, 'child-comp'); ɵɵelementEnd(); ɵɵelementEnd(); } } }); } const fixture = new ComponentFixture(ParentComp); fixture.update(); const host = fixture.hostElement; const child = host.querySelector('child-comp') as any; expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); const [kid1, kid2, kid3] = Array.from(host.querySelectorAll('child-comp > *')); expect(kid1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(kid2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(kid3[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); }); it('should only monkey-patch immediate child nodes in an embedded template container', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], directives: [NgIf], consts: 2, vars: 1, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵtemplate(1, (rf, ctx) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'div'); ɵɵelement(1, 'p'); ɵɵelementEnd(); ɵɵelement(2, 'div'); } }, 3, 0, 'ng-template', ['ngIf', '']); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { ɵɵadvance(1); ɵɵproperty('ngIf', true); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const host = fixture.hostElement; const [section, div1, p, div2] = Array.from(host.querySelectorAll('section, div, p')); expect(section.nodeName.toLowerCase()).toBe('section'); expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(div1.nodeName.toLowerCase()).toBe('div'); expect(div1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(p.nodeName.toLowerCase()).toBe('p'); expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); expect(div2.nodeName.toLowerCase()).toBe('div'); expect(div2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); }); it('should return a context object from a given dom node', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], directives: [NgIf], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'section'); ɵɵelement(1, 'div'); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const section = fixture.hostElement.querySelector('section') !; const sectionContext = getLContext(section) !; const sectionLView = sectionContext.lView !; expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET); expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); expect(sectionContext.native).toBe(section); const div = fixture.hostElement.querySelector('div') !; const divContext = getLContext(div) !; const divLView = divContext.lView !; expect(divContext.nodeIndex).toEqual(HEADER_OFFSET + 1); expect(divLView.length).toBeGreaterThan(HEADER_OFFSET); expect(divContext.native).toBe(div); expect(divLView).toBe(sectionLView); }); it('should cache the element context on a element was pre-emptively monkey-patched', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'section'); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const section = fixture.hostElement.querySelector('section') !as any; const result1 = section[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(result1)).toBeTruthy(); const context = getLContext(section) !; const result2 = section[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(result2)).toBeFalsy(); expect(result2).toBe(context); expect(result2.lView).toBe(result1); }); it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵelement(1, 'p'); ɵɵelementEnd(); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const section = fixture.hostElement.querySelector('section') !as any; expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); const p = fixture.hostElement.querySelector('p') !as any; expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); const pContext = getLContext(p) !; expect(pContext.native).toBe(p); expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext); }); it('should be able to pull in element context data even if the element is decorated using styling', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵstyling(); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { ɵɵstylingApply(); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const section = fixture.hostElement.querySelector('section') !as any; const result1 = section[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(result1)).toBeTruthy(); const elementResult = result1[HEADER_OFFSET]; // first element expect(elementResult).toBe(section); const context = getLContext(section) !; const result2 = section[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(result2)).toBeFalsy(); expect(context.native).toBe(section); }); it('should monkey-patch immediate child nodes in a content-projected region with a reference to the parent component', () => { /*
welcome

this content is projected

this content is projected also

*/ class ProjectorComp { static ngFactoryDef = () => new ProjectorComp(); static ngComponentDef = ɵɵdefineComponent({ type: ProjectorComp, selectors: [['projector-comp']], consts: 4, vars: 0, template: (rf: RenderFlags, ctx: ProjectorComp) => { if (rf & RenderFlags.Create) { ɵɵprojectionDef(); ɵɵtext(0, 'welcome'); ɵɵelementStart(1, 'header'); ɵɵelementStart(2, 'h1'); ɵɵprojection(3); ɵɵelementEnd(); ɵɵelementEnd(); } if (rf & RenderFlags.Update) { } } }); } class ParentComp { static ngFactoryDef = () => new ParentComp(); static ngComponentDef = ɵɵdefineComponent({ type: ParentComp, selectors: [['parent-comp']], directives: [ProjectorComp], consts: 5, vars: 0, template: (rf: RenderFlags, ctx: ParentComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵelementStart(1, 'projector-comp'); ɵɵelementStart(2, 'p'); ɵɵtext(3, 'this content is projected'); ɵɵelementEnd(); ɵɵtext(4, 'this content is projected also'); ɵɵelementEnd(); ɵɵelementEnd(); } } }); } const fixture = new ComponentFixture(ParentComp); fixture.update(); const host = fixture.hostElement; const textNode = host.firstChild as any; const section = host.querySelector('section') !as any; const projectorComp = host.querySelector('projector-comp') !as any; const header = host.querySelector('header') !as any; const h1 = host.querySelector('h1') !as any; const p = host.querySelector('p') !as any; const pText = p.firstChild as any; const projectedTextNode = p.nextSibling; expect(projectorComp.children).toContain(header); expect(h1.children).toContain(p); expect(textNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(projectorComp[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(header[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(h1[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); expect(p[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); expect(pText[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); expect(projectedTextNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); const parentContext = getLContext(section) !; const shadowContext = getLContext(header) !; const projectedContext = getLContext(p) !; const parentComponentData = parentContext.lView; const shadowComponentData = shadowContext.lView; const projectedComponentData = projectedContext.lView; expect(projectedComponentData).toBe(parentComponentData); expect(shadowComponentData).not.toBe(parentComponentData); }); it('should return `null` when an element context is retrieved that isn\'t situated in Angular', () => { const elm1 = document.createElement('div'); const context1 = getLContext(elm1); expect(context1).toBeFalsy(); const elm2 = document.createElement('div'); document.body.appendChild(elm2); const context2 = getLContext(elm2); expect(context2).toBeFalsy(); }); it('should return `null` when an element context is retrieved that is a DOM node that was not created by Angular', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'section'); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const section = fixture.hostElement.querySelector('section') !as any; const manuallyCreatedElement = document.createElement('div'); section.appendChild(manuallyCreatedElement); const context = getLContext(manuallyCreatedElement); expect(context).toBeFalsy(); }); it('should by default monkey-patch the bootstrap component with context details', () => { class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], consts: 0, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => {} }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const hostElm = fixture.hostElement; const component = fixture.component; const componentLView = (component as any)[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(componentLView)).toBeTruthy(); const hostLView = (hostElm as any)[MONKEY_PATCH_KEY_NAME]; expect(hostLView).toBe(componentLView); const context1 = getLContext(hostElm) !; expect(context1.lView).toBe(hostLView); expect(context1.native).toEqual(hostElm); const context2 = getLContext(component) !; expect(context2).toBe(context1); expect(context2.lView).toBe(hostLView); expect(context2.native).toEqual(hostElm); }); it('should by default monkey-patch the directives with LView so that they can be examined', () => { let myDir1Instance: MyDir1|null = null; let myDir2Instance: MyDir2|null = null; let myDir3Instance: MyDir2|null = null; class MyDir1 { static ngFactoryDef = () => myDir1Instance = new MyDir1(); static ngDirectiveDef = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]}); } class MyDir2 { static ngFactoryDef = () => myDir2Instance = new MyDir2(); static ngDirectiveDef = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]}); } class MyDir3 { static ngFactoryDef = () => myDir3Instance = new MyDir2(); static ngDirectiveDef = ɵɵdefineDirective({type: MyDir3, selectors: [['', 'my-dir-3', '']]}); } class StructuredComp { static ngFactoryDef = () => new StructuredComp(); static ngComponentDef = ɵɵdefineComponent({ type: StructuredComp, selectors: [['structured-comp']], directives: [MyDir1, MyDir2, MyDir3], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div', ['my-dir-1', '', 'my-dir-2', '']); ɵɵelement(1, 'div', ['my-dir-3']); } } }); } const fixture = new ComponentFixture(StructuredComp); fixture.update(); const hostElm = fixture.hostElement; const div1 = hostElm.querySelector('div:first-child') !as any; const div2 = hostElm.querySelector('div:last-child') !as any; const context = getLContext(hostElm) !; const componentView = context.lView[context.nodeIndex]; expect(componentView).toContain(myDir1Instance); expect(componentView).toContain(myDir2Instance); expect(componentView).toContain(myDir3Instance); expect(Array.isArray((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); expect(Array.isArray((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); expect(Array.isArray((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); const d1Context = getLContext(myDir1Instance) !; const d2Context = getLContext(myDir2Instance) !; const d3Context = getLContext(myDir3Instance) !; expect(d1Context.lView).toEqual(componentView); expect(d2Context.lView).toEqual(componentView); expect(d3Context.lView).toEqual(componentView); expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d1Context); expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d2Context); expect((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d3Context); expect(d1Context.nodeIndex).toEqual(HEADER_OFFSET); expect(d1Context.native).toBe(div1); expect(d1Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); expect(d2Context.nodeIndex).toEqual(HEADER_OFFSET); expect(d2Context.native).toBe(div1); expect(d2Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); expect(d3Context.nodeIndex).toEqual(HEADER_OFFSET + 1); expect(d3Context.native).toBe(div2); expect(d3Context.directives as any[]).toEqual([myDir3Instance]); }); it('should monkey-patch the exact same context instance of the DOM node, component and any directives on the same element', () => { let myDir1Instance: MyDir1|null = null; let myDir2Instance: MyDir2|null = null; let childComponentInstance: ChildComp|null = null; class MyDir1 { static ngFactoryDef = () => myDir1Instance = new MyDir1(); static ngDirectiveDef = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]}); } class MyDir2 { static ngFactoryDef = () => myDir2Instance = new MyDir2(); static ngDirectiveDef = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]}); } class ChildComp { static ngFactoryDef = () => childComponentInstance = new ChildComp(); static ngComponentDef = ɵɵdefineComponent({ type: ChildComp, selectors: [['child-comp']], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: ChildComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div'); } } }); } class ParentComp { static ngFactoryDef = () => new ParentComp(); static ngComponentDef = ɵɵdefineComponent({ type: ParentComp, selectors: [['parent-comp']], directives: [ChildComp, MyDir1, MyDir2], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: ParentComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'child-comp', ['my-dir-1', '', 'my-dir-2', '']); } } }); } const fixture = new ComponentFixture(ParentComp); fixture.update(); const childCompHostElm = fixture.hostElement.querySelector('child-comp') !as any; const lView = childCompHostElm[MONKEY_PATCH_KEY_NAME]; expect(Array.isArray(lView)).toBeTruthy(); expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); expect((childComponentInstance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); const childNodeContext = getLContext(childCompHostElm) !; expect(childNodeContext.component).toBeFalsy(); expect(childNodeContext.directives).toBeFalsy(); assertMonkeyPatchValueIsLView(myDir1Instance); assertMonkeyPatchValueIsLView(myDir2Instance); assertMonkeyPatchValueIsLView(childComponentInstance); expect(getLContext(myDir1Instance)).toBe(childNodeContext); expect(childNodeContext.component).toBeFalsy(); expect(childNodeContext.directives !.length).toEqual(2); assertMonkeyPatchValueIsLView(myDir1Instance, false); assertMonkeyPatchValueIsLView(myDir2Instance, false); assertMonkeyPatchValueIsLView(childComponentInstance); expect(getLContext(myDir2Instance)).toBe(childNodeContext); expect(childNodeContext.component).toBeFalsy(); expect(childNodeContext.directives !.length).toEqual(2); assertMonkeyPatchValueIsLView(myDir1Instance, false); assertMonkeyPatchValueIsLView(myDir2Instance, false); assertMonkeyPatchValueIsLView(childComponentInstance); expect(getLContext(childComponentInstance)).toBe(childNodeContext); expect(childNodeContext.component).toBeTruthy(); expect(childNodeContext.directives !.length).toEqual(2); assertMonkeyPatchValueIsLView(myDir1Instance, false); assertMonkeyPatchValueIsLView(myDir2Instance, false); assertMonkeyPatchValueIsLView(childComponentInstance, false); function assertMonkeyPatchValueIsLView(value: any, yesOrNo = true) { expect(Array.isArray((value as any)[MONKEY_PATCH_KEY_NAME])).toBe(yesOrNo); } }); it('should monkey-patch sub components with the view data and then replace them with the context result once a lookup occurs', () => { class ChildComp { static ngFactoryDef = () => new ChildComp(); static ngComponentDef = ɵɵdefineComponent({ type: ChildComp, selectors: [['child-comp']], consts: 3, vars: 0, template: (rf: RenderFlags, ctx: ChildComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'div'); ɵɵelement(1, 'div'); ɵɵelement(2, 'div'); } } }); } class ParentComp { static ngFactoryDef = () => new ParentComp(); static ngComponentDef = ɵɵdefineComponent({ type: ParentComp, selectors: [['parent-comp']], directives: [ChildComp], consts: 2, vars: 0, template: (rf: RenderFlags, ctx: ParentComp) => { if (rf & RenderFlags.Create) { ɵɵelementStart(0, 'section'); ɵɵelementStart(1, 'child-comp'); ɵɵelementEnd(); ɵɵelementEnd(); } } }); } const fixture = new ComponentFixture(ParentComp); fixture.update(); const host = fixture.hostElement; const child = host.querySelector('child-comp') as any; expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); const context = getLContext(child) !; expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); const componentData = context.lView[context.nodeIndex]; const component = componentData[CONTEXT]; expect(component instanceof ChildComp).toBeTruthy(); expect(component[MONKEY_PATCH_KEY_NAME]).toBe(context.lView); const componentContext = getLContext(component) !; expect(component[MONKEY_PATCH_KEY_NAME]).toBe(componentContext); expect(componentContext.nodeIndex).toEqual(context.nodeIndex); expect(componentContext.native).toEqual(context.native); expect(componentContext.lView).toEqual(context.lView); }); }); describe('sanitization', () => { it('should sanitize data using the provided sanitization interface', () => { class SanitizationComp { static ngFactoryDef = () => new SanitizationComp(); static ngComponentDef = ɵɵdefineComponent({ type: SanitizationComp, selectors: [['sanitize-this']], consts: 1, vars: 1, template: (rf: RenderFlags, ctx: SanitizationComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'a'); } if (rf & RenderFlags.Update) { ɵɵproperty('href', ctx.href, ɵɵsanitizeUrl); } } }); private href = ''; updateLink(href: any) { this.href = href; } } const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; }); const fixture = new ComponentFixture(SanitizationComp, {sanitizer}); fixture.component.updateLink('http://foo'); fixture.update(); const anchor = fixture.hostElement.querySelector('a') !; expect(anchor.getAttribute('href')).toEqual('http://bar'); fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo')); fixture.update(); expect(anchor.getAttribute('href')).toEqual('http://foo'); }); it('should sanitize HostBindings data using provided sanitization interface', () => { let hostBindingDir: UnsafeUrlHostBindingDir; class UnsafeUrlHostBindingDir { // @HostBinding() cite: any = 'http://cite-dir-value'; static ngFactoryDef = () => hostBindingDir = new UnsafeUrlHostBindingDir(); static ngDirectiveDef = ɵɵdefineDirective({ type: UnsafeUrlHostBindingDir, selectors: [['', 'unsafeUrlHostBindingDir', '']], hostBindings: (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { ɵɵallocHostVars(1); } if (rf & RenderFlags.Update) { ɵɵhostProperty('cite', ctx.cite, ɵɵsanitizeUrl); } } }); } class SimpleComp { static ngFactoryDef = () => new SimpleComp(); static ngComponentDef = ɵɵdefineComponent({ type: SimpleComp, selectors: [['sanitize-this']], consts: 1, vars: 0, template: (rf: RenderFlags, ctx: SimpleComp) => { if (rf & RenderFlags.Create) { ɵɵelement(0, 'blockquote', ['unsafeUrlHostBindingDir', '']); } }, directives: [UnsafeUrlHostBindingDir] }); } const sanitizer = new LocalSanitizer((value) => 'http://bar'); const fixture = new ComponentFixture(SimpleComp, {sanitizer}); hostBindingDir !.cite = 'http://foo'; fixture.update(); const anchor = fixture.hostElement.querySelector('blockquote') !; expect(anchor.getAttribute('cite')).toEqual('http://bar'); hostBindingDir !.cite = sanitizer.bypassSecurityTrustUrl('http://foo'); fixture.update(); expect(anchor.getAttribute('cite')).toEqual('http://foo'); }); }); class LocalSanitizedValue { constructor(public value: any) {} toString() { return this.value; } } class LocalSanitizer implements Sanitizer { constructor(private _interceptor: (value: string|null|any) => string) {} sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null): string|null { if (value instanceof LocalSanitizedValue) { return value.toString(); } return this._interceptor(value); } bypassSecurityTrustHtml(value: string) {} bypassSecurityTrustStyle(value: string) {} bypassSecurityTrustScript(value: string) {} bypassSecurityTrustResourceUrl(value: string) {} bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); } } class ProxyRenderer3Factory implements RendererFactory3 { lastCapturedType: RendererType2|null = null; createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 { this.lastCapturedType = rendererType; return domRendererFactory3.createRenderer(hostElement, rendererType); } }