]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(custom-element): test custom element hydration w/ declarative shadow dom
authorEvan You <evan@vuejs.org>
Sat, 10 Aug 2024 08:39:20 +0000 (16:39 +0800)
committerEvan You <evan@vuejs.org>
Sat, 10 Aug 2024 08:39:20 +0000 (16:39 +0800)
packages/runtime-core/src/hydration.ts
packages/runtime-dom/src/apiCustomElement.ts
packages/vue/__tests__/e2e/e2eUtils.ts
packages/vue/__tests__/e2e/ssr-custom-element.html [new file with mode: 0644]
packages/vue/__tests__/e2e/ssr-custom-element.spec.ts [new file with mode: 0644]

index 1ee34bb3556e5884dd38b85b25df5b6017d9d98b..8060f9475b7dce9ceb339d114f8a7f320052a138 100644 (file)
@@ -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
 }
 
index 26e595cb70c99e15dc16bf98555db6803a61068e..efee4d8a9c152f1542330501fc64ea3da87729fd 100644 (file)
@@ -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)
     }
   }
 
index fd4abc56e73ee1928378689f74fd35aad06042c3..87e99ac7b5ddae05f980ae4e5b47a0352165c541 100644 (file)
@@ -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<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) {
@@ -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 (file)
index 0000000..14139c2
--- /dev/null
@@ -0,0 +1,44 @@
+<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>
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 (file)
index 0000000..0c8413d
--- /dev/null
@@ -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')
+})