feat(ivy): add support for resolving view data from a DOM node (#25627)

PR Close #25627
This commit is contained in:
Matias Niemelä 2018-08-22 16:57:40 -07:00
parent 317d40d879
commit 0024d68add
9 changed files with 558 additions and 13 deletions

View File

@ -159,4 +159,9 @@ export {
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
} from './sanitization/bypass';
export {
ElementContext as ɵElementContext,
getElementContext as ɵgetElementContext
} from './render3/element_discovery';
// clang-format on

View File

@ -0,0 +1,87 @@
/**
* @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 {RElement} from './interfaces/renderer';
import {HEADER_OFFSET, LViewData} from './interfaces/view';
import {StylingIndex} from './styling';
export const MONKEY_PATCH_KEY_NAME = '__ng_data__';
/** The internal element context which is specific to a given DOM node */
export interface ElementContext {
/** The component\'s view data */
lViewData: LViewData;
/** The index of the element within the view data array */
index: number;
/** The instance of the DOM node */
native: RElement;
}
/** Returns the matching `ElementContext` data for a given DOM node.
*
* This function will examine the provided DOM element's monkey-patched property to figure out the
* associated index and view data (`LViewData`).
*
* If the monkey-patched value is the `LViewData` instance then the element context for that
* element will be created and the monkey-patch reference will be updated. Therefore when this
* function is called it may mutate the provided element\'s monkey-patch value.
*
* If the monkey-patch value is not detected then the code will walk up the DOM until an element
* is found which contains a monkey-patch reference. When that occurs then the provided element
* will be updated with a new context (which is then returned).
*/
export function getElementContext(element: RElement): ElementContext|null {
let context = (element as any)[MONKEY_PATCH_KEY_NAME] as ElementContext | LViewData | null;
if (context) {
if (Array.isArray(context)) {
const lViewData = context as LViewData;
const index = findMatchingElement(element, lViewData);
context = {index, native: element, lViewData};
attachLViewDataToNode(element, context);
}
} else {
let parent = element as any;
while (parent = parent.parentNode) {
const parentContext =
(parent as any)[MONKEY_PATCH_KEY_NAME] as ElementContext | LViewData | null;
if (parentContext) {
const lViewData =
Array.isArray(parentContext) ? (parentContext as LViewData) : parentContext.lViewData;
const index = findMatchingElement(element, lViewData);
if (index >= 0) {
context = {index, native: element, lViewData};
attachLViewDataToNode(element, context);
break;
}
}
}
}
return (context as ElementContext) || null;
}
/** Locates the element within the given LViewData and returns the matching index */
function findMatchingElement(element: RElement, lViewData: LViewData): number {
for (let i = HEADER_OFFSET; i < lViewData.length; i++) {
let result = lViewData[i];
if (result) {
// special case for styling since when [class] and [style] bindings
// are used they will wrap the element into a StylingContext array
if (Array.isArray(result)) {
result = result[StylingIndex.ElementPosition];
}
if (result.native === element) return i;
}
}
return -1;
}
/** Assigns the given data to a DOM element using monkey-patching */
export function attachLViewDataToNode(node: any, data: LViewData | ElementContext) {
node[MONKEY_PATCH_KEY_NAME] = data;
}

View File

@ -7,7 +7,7 @@
*/
import {assertEqual, assertLessThan} from './assert';
import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, load, resetApplicationState} from './instructions';
import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createLNode, getPreviousOrParentNode, getRenderer, load, resetComponentState} from './instructions';
import {RENDER_PARENT} from './interfaces/container';
import {LContainerNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
@ -305,7 +305,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]):
const renderer = getRenderer();
let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode();
let localPreviousNode: LNode = localParentNode;
resetApplicationState(); // We don't want to add to the tree with the wrong previous node
resetComponentState(); // We don't want to add to the tree with the wrong previous node
for (let i = 0; i < instructions.length; i++) {
const instruction = instructions[i] as number;
@ -335,7 +335,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]):
const textLNode =
createLNode(lastNodeIndex - HEADER_OFFSET, TNodeType.Element, textRNode, null, null);
localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode);
resetApplicationState();
resetComponentState();
break;
case I18nInstructions.CloseNode:
localPreviousNode = localParentNode;

View File

@ -13,6 +13,7 @@ import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert';
import {attachLViewDataToNode} from './element_discovery';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
@ -102,6 +103,12 @@ export function getCurrentSanitizer(): Sanitizer|null {
return viewData && viewData[SANITIZER];
}
/**
* Store the element depth count. This is used to identify the root elements of the template
* so that we can than attach `LViewData` to only those elements.
*/
let elementDepthCount !: number;
/**
* Returns the current OpaqueViewState instance.
*
@ -515,9 +522,10 @@ export function adjustBlueprintForNewNode(view: LViewData) {
/**
* Resets the application state.
*/
export function resetApplicationState() {
export function resetComponentState() {
isParent = false;
previousOrParentNode = null !;
elementDepthCount = 0;
}
/**
@ -537,7 +545,7 @@ export function renderTemplate<T>(
directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null,
sanitizer?: Sanitizer | null): LElementNode {
if (host == null) {
resetApplicationState();
resetComponentState();
rendererFactory = providedRendererFactory;
const tView =
getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null);
@ -796,6 +804,14 @@ export function elementStart(
}
appendChild(getParentLNode(node), native, viewData);
createDirectivesAndLocals(node, localRefs);
// any immediate children of a component or template container must be pre-emptively
// monkey-patched with the component view data so that the element can be inspected
// later on using any element discovery utility methods (see `element_discovery.ts`)
if (elementDepthCount === 0) {
attachLViewDataToNode(native, viewData);
}
elementDepthCount++;
}
/**
@ -1171,7 +1187,7 @@ export function locateHostElement(
export function hostElement(
tag: string, rNode: RElement | null, def: ComponentDefInternal<any>,
sanitizer?: Sanitizer | null): LElementNode {
resetApplicationState();
resetComponentState();
const node = createLNode(
0, TNodeType.Element, rNode, null, null,
createLViewData(
@ -1296,6 +1312,7 @@ export function elementEnd(): void {
currentQueries && (currentQueries = currentQueries.addNode(previousOrParentNode));
queueLifecycleHooks(previousOrParentNode.tNode.flags, tView);
currentElementNode = null;
elementDepthCount--;
}
/**
@ -2244,6 +2261,7 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?:
grandparent.data[RENDER_PARENT] ! :
parent as LElementNode;
const parentView = viewData[HOST_NODE].view;
while (nodeToProject) {
if (nodeToProject.type === TNodeType.Projection) {
// This node is re-projected, so we must go up the tree to get its projected nodes.
@ -2261,7 +2279,8 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?:
const lNode = projectedView[nodeToProject.index];
lNode.tNode.flags |= TNodeFlags.isProjected;
appendProjectedNode(
lNode as LTextNode | LElementNode | LContainerNode, parent, viewData, renderParent);
lNode as LTextNode | LElementNode | LContainerNode, parent, viewData, renderParent,
parentView);
}
// If we are finished with a list of re-projected nodes, we need to get

View File

@ -7,6 +7,7 @@
*/
import {assertDefined} from './assert';
import {attachLViewDataToNode} from './element_discovery';
import {callHooks} from './hooks';
import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
import {LContainerNode, LElementContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
@ -694,8 +695,14 @@ export function removeChild(parent: LNode, child: RNode | null, currentView: LVi
export function appendProjectedNode(
node: LElementNode | LElementContainerNode | LTextNode | LContainerNode,
currentParent: LElementNode | LElementContainerNode | LViewNode, currentView: LViewData,
renderParent: LElementNode): void {
renderParent: LElementNode, parentView: LViewData): void {
appendChild(currentParent, node.native, currentView);
// the projected contents are processed while in the shadow view (which is the currentView)
// therefore we need to extract the view where the host element lives since it's the
// logical container of the content projected views
attachLViewDataToNode(node.native, parentView);
if (node.tNode.type === TNodeType.Container) {
// The node we are adding is a container and we are adding it to an element which
// is not a component (no more re-projection).
@ -710,10 +717,11 @@ export function appendProjectedNode(
}
} else if (node.tNode.type === TNodeType.ElementContainer) {
let ngContainerChild = getChildLNode(node as LElementContainerNode);
const parentView = currentView[HOST_NODE].view;
while (ngContainerChild) {
appendProjectedNode(
ngContainerChild as LElementNode | LElementContainerNode | LTextNode | LContainerNode,
currentParent, currentView, renderParent);
currentParent, currentView, renderParent, parentView);
ngContainerChild = getNextLNode(ngContainerChild);
}
}

View File

@ -273,7 +273,7 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "resetApplicationState"
"name": "resetComponentState"
},
{
"name": "setHostBindings"

View File

@ -74,6 +74,9 @@
{
"name": "IterableDiffers"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
{
"name": "NEXT"
},
@ -326,6 +329,9 @@
{
"name": "assertTemplate"
},
{
"name": "attachLViewDataToNode"
},
{
"name": "baseDirectiveCreate"
},
@ -837,7 +843,7 @@
"name": "renderStyling"
},
{
"name": "resetApplicationState"
"name": "resetComponentState"
},
{
"name": "resolveDirective"

View File

@ -422,6 +422,9 @@
{
"name": "MODIFIER_KEY_GETTERS"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
{
"name": "MULTI_PROVIDER_FN"
},
@ -1157,6 +1160,9 @@
{
"name": "assertTemplate"
},
{
"name": "attachLViewDataToNode"
},
{
"name": "baseDirectiveCreate"
},
@ -2163,7 +2169,7 @@
"name": "renderStyling"
},
{
"name": "resetApplicationState"
"name": "resetComponentState"
},
{
"name": "resolveDirective"

View File

@ -21,6 +21,8 @@ import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
import {NgIf} from './common_with_def';
import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util';
import {MONKEY_PATCH_KEY_NAME, getElementContext} from '../../src/render3/element_discovery';
import {StylingIndex} from '../../src/render3/styling';
describe('render3 integration test', () => {
@ -1404,7 +1406,6 @@ describe('render3 integration test', () => {
}
});
}
const rendererFactory = new MockRendererFactory();
new ComponentFixture(StyledComp, {rendererFactory});
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
@ -1412,6 +1413,419 @@ describe('render3 integration test', () => {
});
});
describe('element discovery', () => {
it('should only monkey-patch immediate child nodes in a component', () => {
class StructuredComp {
static ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
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 ngComponentDef = defineComponent({
type: ChildComp,
selectors: [['child-comp']],
factory: () => new ChildComp(),
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 ngComponentDef = defineComponent({
type: ParentComp,
selectors: [['parent-comp']],
directives: [ChildComp],
factory: () => new ParentComp(),
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]).toBeFalsy();
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 ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
directives: [NgIf],
factory: () => new StructuredComp(),
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, null, ['ngIf', '']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(1, '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 ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
directives: [NgIf],
factory: () => new StructuredComp(),
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 = getElementContext(section) !;
const sectionLView = sectionContext.lViewData;
expect(sectionContext.index).toEqual(HEADER_OFFSET);
expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET);
expect(sectionContext.native).toBe(section);
const div = fixture.hostElement.querySelector('div') !;
const divContext = getElementContext(div) !;
const divLView = divContext.lViewData;
expect(divContext.index).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 ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
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 = getElementContext(section) !;
const result2 = section[MONKEY_PATCH_KEY_NAME];
expect(Array.isArray(result2)).toBeFalsy();
expect(result2).toBe(context);
expect(result2.lViewData).toBe(result1);
});
it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched',
() => {
class StructuredComp {
static ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
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 = getElementContext(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 ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: StructuredComp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'section');
elementStyling(['class-foo']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStylingApply(0);
}
}
});
}
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(Array.isArray(elementResult)).toBeTruthy();
expect(elementResult[StylingIndex.ElementPosition].native).toBe(section);
const context = getElementContext(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 ngComponentDef = defineComponent({
type: ProjectorComp,
selectors: [['projector-comp']],
factory: () => new ProjectorComp(),
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 ngComponentDef = defineComponent({
type: ParentComp,
selectors: [['parent-comp']],
directives: [ProjectorComp],
factory: () => new ParentComp(),
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]).toBeFalsy();
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 = getElementContext(section) !;
const shadowContext = getElementContext(header) !;
const projectedContext = getElementContext(p) !;
const parentComponentData = parentContext.lViewData;
const shadowComponentData = shadowContext.lViewData;
const projectedComponentData = projectedContext.lViewData;
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 = getElementContext(elm1);
expect(context1).toBeFalsy();
const elm2 = document.createElement('div');
document.body.appendChild(elm2);
const context2 = getElementContext(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 ngComponentDef = defineComponent({
type: StructuredComp,
selectors: [['structured-comp']],
factory: () => new StructuredComp(),
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 = getElementContext(manuallyCreatedElement);
expect(context).toBeFalsy();
});
});
describe('sanitization', () => {
it('should sanitize data using the provided sanitization interface', () => {
class SanitizationComp {