1067 lines
37 KiB
TypeScript
1067 lines
37 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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 {RElement} from '@angular/core/src/render3/interfaces/renderer_dom';
|
|
import {RendererType2} from '../../src/render/api_flags';
|
|
import {getLContext} from '../../src/render3/context_discovery';
|
|
import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index';
|
|
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
|
|
import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context';
|
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
|
import {domRendererFactory3, Renderer3, RendererFactory3} 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({
|
|
firstCreatePass: 0,
|
|
tNode: 2,
|
|
tView: 2, // 1 for root view, 1 for template
|
|
rendererSetText: 1,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('component styles', () => {
|
|
it('should pass in the component styles directly into the underlying renderer', () => {
|
|
class StyledComp {
|
|
static ɵfac = () => new StyledComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new AnimComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new AnimComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new AnimComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new AnimComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ChildCompWithAnim();
|
|
// static ɵdir = ɵɵ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 ɵfac = () => new ParentComp();
|
|
// static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ChildComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ParentComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ProjectorComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ParentComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => myDir1Instance = new MyDir1();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]});
|
|
}
|
|
|
|
class MyDir2 {
|
|
static ɵfac = () => myDir2Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]});
|
|
}
|
|
|
|
class MyDir3 {
|
|
static ɵfac = () => myDir3Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir3, selectors: [['', 'my-dir-3', '']]});
|
|
}
|
|
|
|
class StructuredComp {
|
|
static ɵfac = () => new StructuredComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => myDir1Instance = new MyDir1();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]});
|
|
}
|
|
|
|
class MyDir2 {
|
|
static ɵfac = () => myDir2Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]});
|
|
}
|
|
|
|
class ChildComp {
|
|
static ɵfac = () => childComponentInstance = new ChildComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ParentComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ChildComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new ParentComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => new SanitizationComp();
|
|
static ɵcmp = ɵɵ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 ɵfac = () => hostBindingDir = new UnsafeUrlHostBindingDir();
|
|
static ɵdir = ɵɵdefineDirective({
|
|
type: UnsafeUrlHostBindingDir,
|
|
selectors: [['', 'unsafeUrlHostBindingDir', '']],
|
|
hostVars: 1,
|
|
hostBindings:
|
|
(rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵhostProperty('cite', ctx.cite, ɵɵsanitizeUrl);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class SimpleComp {
|
|
static ɵfac = () => new SimpleComp();
|
|
static ɵcmp = ɵɵ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);
|
|
}
|
|
}
|