Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ssr): stop stripping comment nodes #6123

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/runtime/client-hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,6 @@ export const initializeClientHydrate = (
for (rnIdex; rnIdex < rnLen; rnIdex++) {
shadowRoot.appendChild(shadowRootNodes[rnIdex] as any);
}

// Tidy up left-over / unnecessary comments to stop frameworks complaining about DOM mismatches
Array.from(hostElm.childNodes).forEach((node) => {
if (node.nodeType === NODE_TYPE.CommentNode && typeof (node as d.RenderNode)['s-sn'] !== 'string') {
node.parentNode.removeChild(node);
}
});
}

hostRef.$hostElement$ = hostElm;
Expand Down
66 changes: 55 additions & 11 deletions src/runtime/test/hydrate-shadow-child.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ describe('hydrate, shadow child', () => {
);
}
}
// @ts-ignore

const serverHydrated = await newSpecPage({
components: [CmpA, CmpB, CmpC],
html: `
Expand All @@ -467,6 +467,7 @@ describe('hydrate, shadow child', () => {
`,
hydrateServerSide: true,
});

expect(serverHydrated.root).toEqualHtml(`
<cmp-a class="hydrated" s-id="1">
<!--r.1-->
Expand Down Expand Up @@ -522,35 +523,78 @@ describe('hydrate, shadow child', () => {
`);
});

it('test shadow root innerHTML', async () => {
it('preserves all nodes', async () => {
@Component({
tag: 'cmp-a',
shadow: true,
})
class CmpA {
render() {
return <div>Shadow Content</div>;
return <slot>Shadow Content</slot>;
}
}

const page = await newSpecPage({
const serverHydrated = await newSpecPage({
components: [CmpA],
html: `
<cmp-a>
Light Content
A text node
<!-- a comment -->
<div>An element</div>
<!-- another comment -->
Another text node
</cmp-a>
`,
hydrateServerSide: true,
});

expect(page.root).toEqualHtml(`
<cmp-a>
expect(serverHydrated.root).toEqualHtml(`
<cmp-a class=\"hydrated\" s-id=\"1\">
<!--r.1-->
<!--o.0.1.-->
<!--o.0.2.-->
<!--o.0.4.-->
<!--o.0.6.-->
<!--o.0.7.-->
<slot-fb c-id=\"1.0.0.0\" hidden=\"\" s-sn=\"\">
<!--t.1.1.1.0-->
Shadow Content
</slot-fb>
<!--t.0.1-->
A text node
<!--c.0.2-->
<!-- a comment -->
<div c-id=\"0.4\" s-sn=\"\">
An element
</div>
<!--c.0.6-->
<!-- another comment -->
<!--t.0.7-->
Another text node
</cmp-a>
`);

const clientHydrated = await newSpecPage({
components: [CmpA],
html: serverHydrated.root.outerHTML,
hydrateClientSide: true,
});

expect(clientHydrated.root).toEqualHtml(`
<cmp-a class=\"hydrated\">
<mock:shadow-root>
<div>
<slot>
Shadow Content
</div>
</slot>
</mock:shadow-root>
Light Content
</cmp-a>
A text node
<!-- a comment -->
<div>
An element
</div>
<!-- another comment -->
Another text node
</cmp-a>
`);
});
});
1 change: 1 addition & 0 deletions src/runtime/test/hydrate-shadow-in-shadow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('hydrate, shadow in shadow', () => {
<mock:shadow-root>
<slot></slot>
</mock:shadow-root>
<!---->
<slot></slot>
</cmp-b>
</mock:shadow-root>
Expand Down
32 changes: 32 additions & 0 deletions src/runtime/test/shadow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,36 @@ describe('shadow', () => {
expect(page.root).toEqualHtml(expected);
expect(page.root).toEqualLightHtml(expected);
});

it('test shadow root innerHTML', async () => {
@Component({
tag: 'cmp-a',
shadow: true,
})
class CmpA {
render() {
return <div>Shadow Content</div>;
}
}

const page = await newSpecPage({
components: [CmpA],
html: `
<cmp-a>
Light Content
</cmp-a>
`,
});

expect(page.root).toEqualHtml(`
<cmp-a>
<mock:shadow-root>
<div>
Shadow Content
</div>
</mock:shadow-root>
Light Content
</cmp-a>
`);
});
});
59 changes: 59 additions & 0 deletions test/wdio/ssr-hydration/cmp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { renderToString } from '../hydrate/index.mjs';

describe('ssr-shadow-cmp', () => {
function getNodeNames(chidNodes: NodeListOf<ChildNode>) {
return Array.from(chidNodes)
.flatMap((node) => {
if (node.nodeType === 3) {
if (node.textContent?.trim()) {
return 'text';
} else {
return [];
}
} else if (node.nodeType === 8) {
return 'comment';
} else {
return node.nodeName.toLowerCase();
}
})
.join(' ');
}

it('verifies all nodes are preserved during hydration', async () => {
if (!document.querySelector('#stage')) {
const { html } = await renderToString(
`
<ssr-shadow-cmp>
A text node
<!-- a comment -->
<div>An element</div>
<!-- another comment -->
Another text node
</ssr-shadow-cmp>
`,
{
fullDocument: true,
serializeShadowRoot: true,
constrainTimeouts: false,
},
);
const stage = document.createElement('div');
stage.setAttribute('id', 'stage');
stage.setHTMLUnsafe(html);
document.body.appendChild(stage);
}

// @ts-expect-error resolved through WDIO
const { defineCustomElements } = await import('/dist/loader/index.js');
defineCustomElements().catch(console.error);

// wait for Stencil to take over and reconcile
await browser.waitUntil(async () => customElements.get('ssr-shadow-cmp'));
expect(typeof customElements.get('ssr-shadow-cmp')).toBe('function');
await expect(getNodeNames(document.querySelector('ssr-shadow-cmp').childNodes)).toBe(
`text comment div comment text`,
);

document.querySelector('#stage')?.remove();
});
});
15 changes: 15 additions & 0 deletions test/wdio/ssr-hydration/cmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, h } from '@stencil/core';

@Component({
tag: 'ssr-shadow-cmp',
shadow: true,
})
export class SsrShadowCmp {
render() {
return (
<div>
<slot />
</div>
);
}
}
Loading