Skip to content

Commit

Permalink
fix(synthetic-shadow): un-revert serialization fix
Browse files Browse the repository at this point in the history
Related: #2502 @W-10059718 #2540

This reverts commit 0de7ef2.

Fixes #2425
  • Loading branch information
nolanlawson committed Jan 3, 2022
1 parent caa3c72 commit 1b3854f
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 24 deletions.
9 changes: 7 additions & 2 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts
Expand Up @@ -50,6 +50,7 @@ import {
getNodeKey,
getNodeNearestOwnerKey,
getNodeOwnerKey,
isNodeOrDescendantsShadowed,
isNodeShadowed,
} from '../shared/node-ownership';
import { arrayFromCollection, isGlobalPatchingSkipped } from '../shared/utils';
Expand Down Expand Up @@ -119,7 +120,10 @@ defineProperties(Element.prototype, {
innerHTML: {
get(this: Element): string {
if (!featureFlags.ENABLE_ELEMENT_PATCH) {
if (isNodeShadowed(this) || isSyntheticShadowHost(this)) {
// If this element is in synthetic shadow, if it's a synthetic shadow host,
// or if any of its descendants are synthetic shadow hosts, then we can't
// use the native innerHTML because it would expose private node internals.
if (isNodeOrDescendantsShadowed(this)) {
return innerHTMLGetterPatched.call(this);
}

Expand All @@ -141,7 +145,8 @@ defineProperties(Element.prototype, {
outerHTML: {
get(this: Element): string {
if (!featureFlags.ENABLE_ELEMENT_PATCH) {
if (isNodeShadowed(this) || isSyntheticShadowHost(this)) {
// See notes above on get innerHTML
if (isNodeOrDescendantsShadowed(this)) {
return outerHTMLGetterPatched.call(this);
}
return outerHTMLGetter.call(this);
Expand Down
14 changes: 11 additions & 3 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/node.ts
Expand Up @@ -49,7 +49,12 @@ import {
getIE11FakeShadowRootPlaceholder,
isSyntheticShadowHost,
} from './shadow-root';
import { getNodeNearestOwnerKey, getNodeOwnerKey, isNodeShadowed } from '../shared/node-ownership';
import {
getNodeNearestOwnerKey,
getNodeOwnerKey,
isNodeOrDescendantsShadowed,
isNodeShadowed,
} from '../shared/node-ownership';
import { createStaticNodeList } from '../shared/static-node-list';
import { isGlobalPatchingSkipped } from '../shared/utils';

Expand Down Expand Up @@ -176,7 +181,9 @@ function cloneNodePatched(this: Node, deep?: boolean): Node {
function childNodesGetterPatched(this: Node): NodeListOf<Node> {
if (isSyntheticShadowHost(this)) {
const owner = getNodeOwner(this);
const childNodes = isNull(owner) ? [] : getAllMatches(owner, getFilteredChildNodes(this));
const childNodes = isNull(owner)
? getFilteredChildNodes(this)
: getAllMatches(owner, getFilteredChildNodes(this));
if (
process.env.NODE_ENV !== 'production' &&
isFalse(hasNativeSymbolSupport) &&
Expand Down Expand Up @@ -285,7 +292,8 @@ defineProperties(Node.prototype, {
textContent: {
get(this: Node): string {
if (!featureFlags.ENABLE_NODE_PATCH) {
if (isNodeShadowed(this) || isSyntheticShadowHost(this)) {
// See note on get innerHTML in faux-shadow/element.ts
if (isNodeOrDescendantsShadowed(this)) {
return textContentGetterPatched.call(this);
}

Expand Down
20 changes: 20 additions & 0 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/shadow-root.ts
Expand Up @@ -133,6 +133,26 @@ export function isSyntheticShadowRoot(node: unknown): node is ShadowRoot {
return !isUndefined(shadowRootRecord) && node === shadowRootRecord.shadowRoot;
}

// Return true if any descendant is a host element
export function containsHost(node: Node) {
// IE11 complains with "Unexpected call to method or property access." when calling walker.nextNode().
// The fix for this is to only walk trees for nodes that are Node.ELEMENT_NODE.
if (node.nodeType !== Node.ELEMENT_NODE) {
return false;
}

// IE requires all arguments
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker#browser_compatibility
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, null, false);
let descendant;
while (!isNull((descendant = walker.nextNode()))) {
if (isSyntheticShadowHost(descendant)) {
return true;
}
}
return false;
}

let uid = 0;

export function attachShadow(elm: Element, options: ShadowRootInit): ShadowRoot {
Expand Down
17 changes: 17 additions & 0 deletions packages/@lwc/synthetic-shadow/src/shared/node-ownership.ts
Expand Up @@ -6,6 +6,7 @@
*/
import { defineProperty, isNull, isUndefined } from '@lwc/shared';
import { parentNodeGetter } from '../env/node';
import { containsHost, isSyntheticShadowHost } from '../faux-shadow/shadow-root';

// Used as a back reference to identify the host element
const HostElementKey = '$$HostElementKey$$';
Expand Down Expand Up @@ -69,3 +70,19 @@ export function getNodeKey(node: Node): number | undefined {
export function isNodeShadowed(node: Node): boolean {
return !isUndefined(getNodeOwnerKey(node));
}

/**
* This function verifies if a node (with or without owner key) is contained in a shadow root.
* Use with care since has high computational cost.
*/
export function isNodeDeepShadowed(node: Node): boolean {
return !isUndefined(getNodeNearestOwnerKey(node));
}

/**
* Returns true if this node is a shadow host, is in a shadow host, or contains a shadow host
* anywhere in its tree.
*/
export function isNodeOrDescendantsShadowed(node: Node): boolean {
return isNodeShadowed(node) || isSyntheticShadowHost(node) || containsHost(node);
}
Expand Up @@ -62,10 +62,9 @@ describe('Slotting', () => {
it('shadow container, light consumer', () => {
const nodes = createTestElement('x-light-consumer', LightConsumer);

const expected = process.env.NATIVE_SHADOW // native shadow doesn't output slots in innerHTML
? '<x-shadow-container><p data-id="light-consumer-text">Hello from Light DOM</p></x-shadow-container>'
: '<x-shadow-container><slot><p data-id="light-consumer-text">Hello from Light DOM</p></slot></x-shadow-container>';
expect(nodes['x-light-consumer'].innerHTML).toEqual(expected);
expect(nodes['x-light-consumer'].innerHTML).toEqual(
'<x-shadow-container><p data-id="light-consumer-text">Hello from Light DOM</p></x-shadow-container>'
);
});

it('light container, shadow consumer', () => {
Expand Down
Expand Up @@ -3,6 +3,8 @@ import { extractDataIds } from 'test-utils';

import LightContainer from 'x/lightContainer';
import ShadowContainer from 'x/shadowContainer';
import LightContainerDeepShadow from 'x/lightContainerDeepShadow';
import LightContainerDeeperShadow from 'x/lightContainerDeeperShadow';

describe('Light DOM + Synthetic Shadow DOM', () => {
describe('light -> shadow', () => {
Expand All @@ -28,7 +30,32 @@ describe('Light DOM + Synthetic Shadow DOM', () => {

it('childNodes', () => {
expect(Array.from(nodes.slot.childNodes)).toEqual([]);
});
expect(Array.from(elm.childNodes)).toEqual([nodes.consumer]);
expect(Array.from(nodes['consumer.shadowRoot'].childNodes)).toEqual([
nodes.pInShadow,
nodes.slot,
]);
expect(Array.from(nodes.slot)).toEqual([]);
});
if (!process.env.COMPAT) {
it('childNodes - text nodes', () => {
// TreeWalker is just a convenient way of getting text nodes without using childNodes
// Sadly it throws errors in IE11 (even when adding arguments to `createTreeWalker()`)
const textNodes = {};
const walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
textNodes[node.wholeText] = node;
}
expect(Array.from(nodes.p.childNodes)).toEqual([
textNodes['I am an assigned element.'],
]);
expect(Array.from(nodes.consumer.childNodes)).toEqual([
nodes.p,
textNodes['I am an assigned text.'],
]);
});
}
it('parentNode', () => {
expect(nodes.p.parentNode).toEqual(nodes.consumer);
expect(nodes.consumer.parentNode).toEqual(elm);
Expand All @@ -49,16 +76,14 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
expect(nodes.p.getRootNode()).toEqual(expectedRootNode);
expect(nodes.consumer.getRootNode()).toEqual(document);
});
// TODO [#2425]: Incorrect serialization
xit('textContent', () => {
it('textContent', () => {
expect(nodes.p.textContent).toEqual('I am an assigned element.');
expect(nodes.consumer.textContent).toEqual(
'I am an assigned element.I am an assigned text.'
);
expect(elm.textContent).toEqual('I am an assigned element.I am an assigned text.');
});
// TODO [#2425]: Incorrect serialization
xit('innerHTML', () => {
it('innerHTML', () => {
expect(nodes.p.innerHTML).toEqual('I am an assigned element.');
expect(nodes.consumer.innerHTML).toEqual(
'<p data-id="p">I am an assigned element.</p>I am an assigned text.'
Expand All @@ -67,8 +92,7 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
);
});
// TODO [#2425]: Incorrect serialization
xit('outerHTML', () => {
it('outerHTML', () => {
expect(nodes.p.outerHTML).toEqual('<p data-id="p">I am an assigned element.</p>');
expect(nodes.consumer.outerHTML).toEqual(
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
Expand All @@ -79,6 +103,87 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
});
});

describe('light -> deep shadow', () => {
let elm, nodes;
beforeEach(() => {
elm = createElement('x-light-container-deep-shadow', {
is: LightContainerDeepShadow,
});
document.body.appendChild(elm);
nodes = extractDataIds(elm);
});
it('childNodes', () => {
expect(Array.from(elm.childNodes)).toEqual([nodes.wrapper]);
expect(Array.from(nodes.wrapper.childNodes)).toEqual([nodes.consumer]);
});
it('textContent', () => {
expect(nodes.p.textContent).toEqual('I am an assigned element.');
expect(nodes.consumer.textContent).toEqual(
'I am an assigned element.I am an assigned text.'
);
expect(elm.textContent).toEqual('I am an assigned element.I am an assigned text.');
});
it('innerHTML', () => {
expect(nodes.p.innerHTML).toEqual('I am an assigned element.');
expect(nodes.consumer.innerHTML).toEqual(
'<p data-id="p">I am an assigned element.</p>I am an assigned text.'
);
expect(elm.innerHTML).toEqual(
'<div data-id="wrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div>'
);
});
it('outerHTML', () => {
expect(nodes.p.outerHTML).toEqual('<p data-id="p">I am an assigned element.</p>');
expect(nodes.consumer.outerHTML).toEqual(
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
);
expect(elm.outerHTML).toEqual(
'<x-light-container-deep-shadow><div data-id="wrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></x-light-container-deep-shadow>'
);
});
});

describe('light -> deeper shadow', () => {
let elm, nodes;
beforeEach(() => {
elm = createElement('x-light-container-deeper-shadow', {
is: LightContainerDeeperShadow,
});
document.body.appendChild(elm);
nodes = extractDataIds(elm);
});
it('childNodes', () => {
expect(Array.from(elm.childNodes)).toEqual([nodes.wrapper]);
expect(Array.from(nodes.wrapper.childNodes)).toEqual([nodes.innerWrapper]);
expect(Array.from(nodes.innerWrapper.childNodes)).toEqual([nodes.consumer]);
});
it('textContent', () => {
expect(nodes.p.textContent).toEqual('I am an assigned element.');
expect(nodes.consumer.textContent).toEqual(
'I am an assigned element.I am an assigned text.'
);
expect(elm.textContent).toEqual('I am an assigned element.I am an assigned text.');
});
it('innerHTML', () => {
expect(nodes.p.innerHTML).toEqual('I am an assigned element.');
expect(nodes.consumer.innerHTML).toEqual(
'<p data-id="p">I am an assigned element.</p>I am an assigned text.'
);
expect(elm.innerHTML).toEqual(
'<div data-id="wrapper"><div data-id="innerWrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></div>'
);
});
it('outerHTML', () => {
expect(nodes.p.outerHTML).toEqual('<p data-id="p">I am an assigned element.</p>');
expect(nodes.consumer.outerHTML).toEqual(
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
);
expect(elm.outerHTML).toEqual(
'<x-light-container-deeper-shadow><div data-id="wrapper"><div data-id="innerWrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></div></x-light-container-deeper-shadow>'
);
});
});

describe('shadow -> light -> shadow', () => {
let elm, nodes;
beforeEach(() => {
Expand Down
@@ -1,4 +1,4 @@
<template>
<p>I am in the shadow</p>
<p data-id="pInShadow">I am in the shadow</p>
<slot data-id="slot"></slot>
</template>
</template>
@@ -0,0 +1,8 @@
<template lwc:render-mode="light">
<div data-id="wrapper">
<x-consumer data-id="consumer">
<p data-id="p">I am an assigned element.</p>
I am an assigned text.
</x-consumer>
</div>
</template>
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class LightContainerDeepShadow extends LightningElement {
static renderMode = 'light';
}
@@ -0,0 +1,10 @@
<template lwc:render-mode="light">
<div data-id="wrapper">
<div data-id="innerWrapper">
<x-consumer data-id="consumer">
<p data-id="p">I am an assigned element.</p>
I am an assigned text.
</x-consumer>
</div>
</div>
</template>
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class LightContainerDeeperShadow extends LightningElement {
static renderMode = 'light';
}
Expand Up @@ -67,11 +67,11 @@ if (!process.env.NATIVE_SHADOW) {

describe('Element.prototype API', () => {
it('should keep behavior for innerHTML', () => {
expect(elementOutsideLWC.innerHTML.length).toBe(455);
expect(elementOutsideLWC.innerHTML.length).toBe(27);
expect(rootLwcElement.innerHTML.length).toBe(0);
expect(lwcElementInsideShadow.innerHTML.length).toBe(0);

expect(divManuallyApendedToShadow.innerHTML.length).toBe(176); // <x-manually-inserted><p>slot-container text</p><x-with-slot><p>with
expect(divManuallyApendedToShadow.innerHTML.length).toBe(43); // <x-manually-inserted><p>slot-container text</p><x-with-slot><p>with

expect(cmpShadow.innerHTML.length).toBe(99);

Expand All @@ -80,11 +80,11 @@ if (!process.env.NATIVE_SHADOW) {
});

it('should keep behavior for outerHTML', () => {
expect(elementOutsideLWC.outerHTML.length).toBe(466);
expect(elementOutsideLWC.outerHTML.length).toBe(38);
expect(rootLwcElement.outerHTML.length).toBe(27);
expect(lwcElementInsideShadow.outerHTML.length).toBe(27);

expect(divManuallyApendedToShadow.outerHTML.length).toBe(206); // <div class="manual-ctx"><x-manually-inserted><p>slot-container text</p><x-with-slot><p>wi ....
expect(divManuallyApendedToShadow.outerHTML.length).toBe(73); // <div class="manual-ctx"><x-manually-inserted><p>slot-container text</p><x-with-slot><p>wi ....

expect(cmpShadow.outerHTML).toBe(undefined);

Expand Down Expand Up @@ -257,12 +257,12 @@ if (!process.env.NATIVE_SHADOW) {
});

it('should preserve behaviour for textContent', () => {
expect(elementOutsideLWC.textContent.length).toBe(117);
expect(elementOutsideLWC.textContent.length).toBe(0);
expect(rootLwcElement.textContent.length).toBe(0);
expect(lwcElementInsideShadow.textContent.length).toBe(0);

expect(elementInShadow.textContent.length).toBe(0);
expect(divManuallyApendedToShadow.textContent.length).toBe(45);
expect(divManuallyApendedToShadow.textContent.length).toBe(0);

expect(cmpShadow.textContent.length).toBe(31);

Expand Down

0 comments on commit 1b3854f

Please sign in to comment.