diff --git a/src/mock-doc/serialize-node.ts b/src/mock-doc/serialize-node.ts index 23540a6a316..c94707d5743 100644 --- a/src/mock-doc/serialize-node.ts +++ b/src/mock-doc/serialize-node.ts @@ -1,4 +1,11 @@ -import { CONTENT_REF_ID, ORG_LOCATION_ID, SLOT_NODE_ID, TEXT_NODE_ID, XLINK_NS } from '../runtime/runtime-constants'; +import { + CONTENT_REF_ID, + HYDRATE_ID, + ORG_LOCATION_ID, + SLOT_NODE_ID, + TEXT_NODE_ID, + XLINK_NS, +} from '../runtime/runtime-constants'; import { cloneAttributes } from './attribute'; import { NODE_TYPES } from './constants'; import { type MockDocument } from './document'; @@ -274,6 +281,21 @@ function* streamToHtml( } for (let i = 0; i < childNodeLength; i++) { + /** + * In cases where a user would pass in a declarative shadow dom of a + * Stencil component, we want to skip over the template tag as we + * will be parsing the shadow root of the component again. + * + * We know it is a hydrated Stencil component by checking if the `HYDRATE_ID` + * is set on the node. + */ + const sId = (node as HTMLElement).attributes.getNamedItem(HYDRATE_ID); + const isStencilDeclarativeShadowDOM = childNodes[i].nodeName.toLowerCase() === 'template' && sId; + if (isStencilDeclarativeShadowDOM) { + yield `\n${' '.repeat(output.indent)}`; + continue; + } + yield* streamToHtml(childNodes[i], opts, output); } diff --git a/test/end-to-end/src/components.d.ts b/test/end-to-end/src/components.d.ts index 64e705faa31..5989dbb0e24 100644 --- a/test/end-to-end/src/components.d.ts +++ b/test/end-to-end/src/components.d.ts @@ -100,6 +100,10 @@ export namespace Components { "someMethodWithArgs": (unit: string, value: number) => Promise; "someProp": number; } + interface NestedCmpChild { + } + interface NestedCmpParent { + } interface PathAliasCmp { } interface PrerenderCmp { @@ -334,6 +338,18 @@ declare global { prototype: HTMLMethodCmpElement; new (): HTMLMethodCmpElement; }; + interface HTMLNestedCmpChildElement extends Components.NestedCmpChild, HTMLStencilElement { + } + var HTMLNestedCmpChildElement: { + prototype: HTMLNestedCmpChildElement; + new (): HTMLNestedCmpChildElement; + }; + interface HTMLNestedCmpParentElement extends Components.NestedCmpParent, HTMLStencilElement { + } + var HTMLNestedCmpParentElement: { + prototype: HTMLNestedCmpParentElement; + new (): HTMLNestedCmpParentElement; + }; interface HTMLPathAliasCmpElement extends Components.PathAliasCmp, HTMLStencilElement { } var HTMLPathAliasCmpElement: { @@ -427,6 +443,8 @@ declare global { "import-assets": HTMLImportAssetsElement; "listen-cmp": HTMLListenCmpElement; "method-cmp": HTMLMethodCmpElement; + "nested-cmp-child": HTMLNestedCmpChildElement; + "nested-cmp-parent": HTMLNestedCmpParentElement; "path-alias-cmp": HTMLPathAliasCmpElement; "prerender-cmp": HTMLPrerenderCmpElement; "prop-cmp": HTMLPropCmpElement; @@ -507,6 +525,10 @@ declare namespace LocalJSX { interface MethodCmp { "someProp"?: number; } + interface NestedCmpChild { + } + interface NestedCmpParent { + } interface PathAliasCmp { } interface PrerenderCmp { @@ -564,6 +586,8 @@ declare namespace LocalJSX { "import-assets": ImportAssets; "listen-cmp": ListenCmp; "method-cmp": MethodCmp; + "nested-cmp-child": NestedCmpChild; + "nested-cmp-parent": NestedCmpParent; "path-alias-cmp": PathAliasCmp; "prerender-cmp": PrerenderCmp; "prop-cmp": PropCmp; @@ -609,6 +633,8 @@ declare module "@stencil/core" { "import-assets": LocalJSX.ImportAssets & JSXBase.HTMLAttributes; "listen-cmp": LocalJSX.ListenCmp & JSXBase.HTMLAttributes; "method-cmp": LocalJSX.MethodCmp & JSXBase.HTMLAttributes; + "nested-cmp-child": LocalJSX.NestedCmpChild & JSXBase.HTMLAttributes; + "nested-cmp-parent": LocalJSX.NestedCmpParent & JSXBase.HTMLAttributes; "path-alias-cmp": LocalJSX.PathAliasCmp & JSXBase.HTMLAttributes; "prerender-cmp": LocalJSX.PrerenderCmp & JSXBase.HTMLAttributes; "prop-cmp": LocalJSX.PropCmp & JSXBase.HTMLAttributes; diff --git a/test/end-to-end/src/declarative-shadow-dom/nested-child-cmp.tsx b/test/end-to-end/src/declarative-shadow-dom/nested-child-cmp.tsx new file mode 100644 index 00000000000..bc6061d6fad --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/nested-child-cmp.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'nested-cmp-child', + shadow: true, +}) +export class NestedCmpChild { + render() { + return ( +
+ +
+ ); + } +} diff --git a/test/end-to-end/src/declarative-shadow-dom/parent-cmp.tsx b/test/end-to-end/src/declarative-shadow-dom/parent-cmp.tsx new file mode 100644 index 00000000000..144ebbeac51 --- /dev/null +++ b/test/end-to-end/src/declarative-shadow-dom/parent-cmp.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'nested-cmp-parent', + shadow: true, +}) +export class NestedCmpParent { + render() { + return ( +
+ +
+ ); + } +} diff --git a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts index b3d1ef54a01..5159bce3e75 100644 --- a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts +++ b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts @@ -319,4 +319,43 @@ describe('renderToString', () => { expect(color).toBe('rgb(0, 0, 0)'); }); }); + + it('does not render the shadow root twice', async () => { + const { html } = await renderToString( + ` + + + + + Hello World + + + `, + { + fullDocument: false, + prettyHtml: true, + }, + ); + expect(html).toBe(` + + + + + + Hello World + +`); + }); });