diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 1ee34bb35..8060f9475 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -69,9 +69,12 @@ const isSVGContainer = (container: Element) => const isMathMLContainer = (container: Element) => container.namespaceURI!.includes('MathML') -const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => { - if (isSVGContainer(container)) return 'svg' - if (isMathMLContainer(container)) return 'mathml' +const getContainerType = ( + container: Element | ShadowRoot, +): 'svg' | 'mathml' | undefined => { + if (container.nodeType !== DOMNodeTypes.ELEMENT) return undefined + if (isSVGContainer(container as Element)) return 'svg' + if (isMathMLContainer(container as Element)) return 'mathml' return undefined } diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 26e595cb7..efee4d8a9 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -250,8 +250,6 @@ export class VueElement super() if (this.shadowRoot && _createApp !== createApp) { this._root = this.shadowRoot - // TODO hydration needs to be reworked - this._mount(_def) } else { if (__DEV__ && this.shadowRoot) { warn( @@ -265,10 +263,11 @@ export class VueElement } else { this._root = this } - if (!(this._def as ComponentOptions).__asyncLoader) { - // for sync component defs we can immediately resolve props - this._resolveProps(this._def) - } + } + + if (!(this._def as ComponentOptions).__asyncLoader) { + // for sync component defs we can immediately resolve props + this._resolveProps(this._def) } } diff --git a/packages/vue/__tests__/e2e/e2eUtils.ts b/packages/vue/__tests__/e2e/e2eUtils.ts index fd4abc56e..87e99ac7b 100644 --- a/packages/vue/__tests__/e2e/e2eUtils.ts +++ b/packages/vue/__tests__/e2e/e2eUtils.ts @@ -5,7 +5,7 @@ import puppeteer, { type PuppeteerLaunchOptions, } from 'puppeteer' -export const E2E_TIMEOUT = 30 * 1000 +export const E2E_TIMEOUT: number = 30 * 1000 const puppeteerOptions: PuppeteerLaunchOptions = { args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [], @@ -13,12 +13,13 @@ const puppeteerOptions: PuppeteerLaunchOptions = { } const maxTries = 30 -export const timeout = (n: number) => new Promise(r => setTimeout(r, n)) +export const timeout = (n: number): Promise => + new Promise(r => setTimeout(r, n)) export async function expectByPolling( poll: () => Promise, expected: string, -) { +): Promise { for (let tries = 0; tries < maxTries; tries++) { const actual = (await poll()) || '' if (actual.indexOf(expected) > -1 || tries === maxTries - 1) { @@ -55,10 +56,7 @@ export function setupPuppeteer(args?: string[]) { page.on('console', e => { if (e.type() === 'error') { const err = e.args()[0] - console.error( - `Error from Puppeteer-loaded page:\n`, - err.remoteObject().description, - ) + console.error(`Error from Puppeteer-loaded page:\n`, err.remoteObject()) } }) }) diff --git a/packages/vue/__tests__/e2e/ssr-custom-element.html b/packages/vue/__tests__/e2e/ssr-custom-element.html new file mode 100644 index 000000000..14139c2d5 --- /dev/null +++ b/packages/vue/__tests__/e2e/ssr-custom-element.html @@ -0,0 +1,44 @@ + + + + + + diff --git a/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts b/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts new file mode 100644 index 000000000..0c8413d17 --- /dev/null +++ b/packages/vue/__tests__/e2e/ssr-custom-element.spec.ts @@ -0,0 +1,35 @@ +import path from 'node:path' +import { setupPuppeteer } from './e2eUtils' + +const { page, click, text } = setupPuppeteer() + +// this must be tested in actual Chrome because jsdom does not support +// declarative shadow DOM +test('ssr custom element hydration', async () => { + await page().goto( + `file://${path.resolve(__dirname, './ssr-custom-element.html')}`, + ) + + function getColor() { + return page().evaluate(() => { + return [ + (document.querySelector('my-element') as any).style.border, + (document.querySelector('my-element-async') as any).style.border, + ] + }) + } + + expect(await getColor()).toMatchObject(['1px solid red', '']) + await page().evaluate(() => (window as any).resolve()) // exposed by test + expect(await getColor()).toMatchObject(['1px solid red', '1px solid red']) + + async function assertInteraction(el: string) { + const selector = `${el} >>> button` + expect(await text(selector)).toBe('1') + await click(selector) + expect(await text(selector)).toBe('2') + } + + await assertInteraction('my-element') + await assertInteraction('my-element-async') +})