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
}
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(
} 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)
}
}
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'] : [],
}
const maxTries = 30
-export const timeout = (n: number) => new Promise(r => setTimeout(r, n))
+export const timeout = (n: number): Promise<any> =>
+ new Promise(r => setTimeout(r, n))
export async function expectByPolling(
poll: () => Promise<any>,
expected: string,
-) {
+): Promise<void> {
for (let tries = 0; tries < maxTries; tries++) {
const actual = (await poll()) || ''
if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
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())
}
})
})
--- /dev/null
+<script src="../../dist/vue.global.js"></script>
+
+<my-element
+ ><template shadowrootmode="open"><button>1</button></template></my-element
+>
+<my-element-async
+ ><template shadowrootmode="open"
+ ><button>1</button></template
+ ></my-element-async
+>
+
+<script>
+ const {
+ h,
+ ref,
+ defineSSRCustomElement,
+ defineAsyncComponent,
+ onMounted,
+ useHost,
+ } = Vue
+
+ const def = {
+ setup() {
+ const count = ref(1)
+ const el = useHost()
+ onMounted(() => (el.style.border = '1px solid red'))
+
+ return () => h('button', { onClick: () => count.value++ }, count.value)
+ },
+ }
+
+ customElements.define('my-element', defineSSRCustomElement(def))
+ customElements.define(
+ 'my-element-async',
+ defineSSRCustomElement(
+ defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ window.resolve = () => r(def)
+ }),
+ ),
+ ),
+ )
+</script>
--- /dev/null
+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')
+})