angular/packages/core/test/render3/integration_spec.ts
crisbeto d5b87d32b0 perf(ivy): move attributes array into component def (#32798)
Currently Ivy stores the element attributes into an array above the component def and passes it into the relevant instructions, however the problem is that upon minification the array will get a unique name which won't compress very well. These changes move the attributes array into the component def and pass in the index into the instructions instead.

Before:
```
const _c0 = ['foo', 'bar'];

SomeComp.ngComponentDef = defineComponent({
  template: function() {
    element(0, 'div', _c0);
  }
});
```

After:
```
SomeComp.ngComponentDef = defineComponent({
  consts: [['foo', 'bar']],
  template: function() {
    element(0, 'div', 0);
  }
});
```

A couple of cases that this PR doesn't handle:
* Template references are still in a separate array.
* i18n attributes are still in a separate array.

PR Close #32798
2019-10-09 13:16:55 -07:00

1195 lines
41 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 {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, ɵɵ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,
decls: 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('<child>apz</child>');
ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]};
ctx.beforeTree.subTrees !.push({afterLabel: 'b'});
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abz</child>');
ctx.projectedTree.subTrees ![1].afterLabel = 'h';
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abhz</child>');
ctx.beforeTree.subTrees !.push({beforeLabel: 'c'});
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abchz</child>');
// 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; }'],
decls: 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,
decls: 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,
decls: 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,
decls: 1,
vars: 1,
selectors: [['foo']],
consts: [[AttributeMarker.Bindings, '@fooAnimation']],
template: (rf: RenderFlags, ctx: AnimComp) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div', 0);
}
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,
decls: 1,
vars: 1,
selectors: [['foo']],
consts: [['@fooAnimation', '']],
template: (rf: RenderFlags, ctx: AnimComp) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div', 0);
}
}
});
}
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,
// decls: 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']],
decls: 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']],
decls: 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],
decls: 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],
decls: 2,
vars: 1,
consts: [['ngIf', '']],
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', 0);
ɵɵ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],
decls: 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']],
decls: 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']],
decls: 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']],
decls: 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 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',
() => {
/*
<!-- DOM view -->
<section>
<projection-comp>
welcome
<header>
<h1>
<p>this content is projected</p>
this content is projected also
</h1>
</header>
</projection-comp>
</section>
*/
class ProjectorComp {
static ngFactoryDef = () => new ProjectorComp();
static ngComponentDef = ɵɵdefineComponent({
type: ProjectorComp,
selectors: [['projector-comp']],
decls: 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],
decls: 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']],
decls: 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']],
decls: 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],
decls: 2,
vars: 0,
consts: [['my-dir-1', '', 'my-dir-2', ''], ['my-dir-3']],
template: (rf: RenderFlags, ctx: StructuredComp) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div', 0);
ɵɵelement(1, 'div', 1);
}
}
});
}
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']],
decls: 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],
decls: 1,
vars: 0,
consts: [['my-dir-1', '', 'my-dir-2', '']],
template: (rf: RenderFlags, ctx: ParentComp) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'child-comp', 0);
}
}
});
}
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']],
decls: 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],
decls: 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']],
decls: 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']],
decls: 1,
vars: 0,
consts: [['unsafeUrlHostBindingDir', '']],
template: (rf: RenderFlags, ctx: SimpleComp) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'blockquote', 0);
}
},
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);
}
}