From 5a7ab241cbdfdb40de427297d513c24b67c40565 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Mon, 27 Jan 2025 17:06:06 +0000 Subject: [PATCH] fix(ssr): stop stripping comment nodes (#6123) * fix(ssr): stop stripping comment nodes * Update test/wdio/ssr-hydration/cmp.test.tsx Co-authored-by: Christian Bromann --------- Co-authored-by: John Jenkins Co-authored-by: Christian Bromann --- src/runtime/client-hydrate.ts | 7 -- .../test/hydrate-shadow-child.spec.tsx | 66 +++++++++++++++---- .../test/hydrate-shadow-in-shadow.spec.tsx | 1 + src/runtime/test/shadow.spec.tsx | 32 +++++++++ test/wdio/ssr-hydration/cmp.test.tsx | 59 +++++++++++++++++ test/wdio/ssr-hydration/cmp.tsx | 15 +++++ 6 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 test/wdio/ssr-hydration/cmp.test.tsx create mode 100644 test/wdio/ssr-hydration/cmp.tsx diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index f63deaff8a8..45498e8c990 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -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; diff --git a/src/runtime/test/hydrate-shadow-child.spec.tsx b/src/runtime/test/hydrate-shadow-child.spec.tsx index e25c8db5197..3d9e191f7fa 100644 --- a/src/runtime/test/hydrate-shadow-child.spec.tsx +++ b/src/runtime/test/hydrate-shadow-child.spec.tsx @@ -454,7 +454,7 @@ describe('hydrate, shadow child', () => { ); } } - // @ts-ignore + const serverHydrated = await newSpecPage({ components: [CmpA, CmpB, CmpC], html: ` @@ -467,6 +467,7 @@ describe('hydrate, shadow child', () => { `, hydrateServerSide: true, }); + expect(serverHydrated.root).toEqualHtml(` @@ -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
Shadow Content
; + return Shadow Content; } } - const page = await newSpecPage({ + const serverHydrated = await newSpecPage({ components: [CmpA], html: ` - Light Content + A text node + +
An element
+ + Another text node
`, + hydrateServerSide: true, }); - expect(page.root).toEqualHtml(` - + expect(serverHydrated.root).toEqualHtml(` + + + + + + + + + + A text node + + +
+ An element +
+ + + + Another text node +
+ `); + + const clientHydrated = await newSpecPage({ + components: [CmpA], + html: serverHydrated.root.outerHTML, + hydrateClientSide: true, + }); + + expect(clientHydrated.root).toEqualHtml(` + -
+ Shadow Content -
+
- Light Content -
+ A text node + +
+ An element +
+ + Another text node +
`); }); }); diff --git a/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx b/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx index 1ff3c9c3576..7db0a01f1a9 100644 --- a/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx +++ b/src/runtime/test/hydrate-shadow-in-shadow.spec.tsx @@ -60,6 +60,7 @@ describe('hydrate, shadow in shadow', () => { + diff --git a/src/runtime/test/shadow.spec.tsx b/src/runtime/test/shadow.spec.tsx index 4b9fa10a47d..05bef8d39d1 100644 --- a/src/runtime/test/shadow.spec.tsx +++ b/src/runtime/test/shadow.spec.tsx @@ -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
Shadow Content
; + } + } + + const page = await newSpecPage({ + components: [CmpA], + html: ` + + Light Content + + `, + }); + + expect(page.root).toEqualHtml(` + + +
+ Shadow Content +
+
+ Light Content +
+ `); + }); }); diff --git a/test/wdio/ssr-hydration/cmp.test.tsx b/test/wdio/ssr-hydration/cmp.test.tsx new file mode 100644 index 00000000000..5306f203dd5 --- /dev/null +++ b/test/wdio/ssr-hydration/cmp.test.tsx @@ -0,0 +1,59 @@ +import { renderToString } from '../hydrate/index.mjs'; + +describe('ssr-shadow-cmp', () => { + function getNodeNames(chidNodes: NodeListOf) { + 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( + ` + + A text node + +
An element
+ + Another text node +
+ `, + { + 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(); + }); +}); diff --git a/test/wdio/ssr-hydration/cmp.tsx b/test/wdio/ssr-hydration/cmp.tsx new file mode 100644 index 00000000000..551d7c04d17 --- /dev/null +++ b/test/wdio/ssr-hydration/cmp.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'ssr-shadow-cmp', + shadow: true, +}) +export class SsrShadowCmp { + render() { + return ( +
+ +
+ ); + } +}