/* @vitest-environment happy-dom */ /* Hydration runtime tests using happy-dom. * * Each test renders a component to a string via the SSR, then parses it into a happy-dom * Document, runs `hydrate()`, and asserts that the resulting custom elements behave like a * fully client-rendered instance (events fire, state updates, attributes reactive). */ import { describe, expect, it, beforeEach } from 'vitest'; import { execSync } from 'node:child_process'; import { writeFileSync, unlinkSync } from 'node:fs'; import { resolve } from 'node:path'; // @ts-ignore — dist import { defineWompo, html, useState, useRef, // @ts-ignore } from '../../dist/wompo.js'; // @ts-ignore — dist import { renderToString } from '../../dist/ssr/index.js'; // @ts-ignore — dist import { hydrate } from '../../dist/wompo/hydrate.js'; // Side-effect import: registers `hydr-link`/`hydr-menu` in the client registry so `hydrate()` can // find them. The SAME module is rendered server-side in a Node subprocess (see `ssrFromFixture`). import './fixtures/nested-island.mjs'; // Side-effect import: registers `cb-parent`/`cb-child` for the cross-island callback-prop test. import './fixtures/island-callback.mjs'; // Side-effect import: registers `sh-host`/`sh-shell` for the island-with-children re-render test. import './fixtures/island-children.mjs'; // Side-effect import: registers `hydr-host`/`hydr-chip` for the non-island dynamic-tag child test. import './fixtures/static-child.mjs'; // Side-effect import: registers `fwd-page`/`fwd-btn`/`fwd-inner` for the forwarded-children test. import './fixtures/forwarded-children.mjs'; // Wait for a microtask tick so `requestRender` (batched) flushes. const tick = () => new Promise((r) => setTimeout(r, 0)); const DIST_SSR = resolve(process.cwd(), 'dist/ssr/index.js'); const NESTED_ISLAND_FIXTURE = resolve(process.cwd(), 'test/ssr/fixtures/nested-island.mjs'); const CALLBACK_ISLAND_FIXTURE = resolve(process.cwd(), 'test/ssr/fixtures/island-callback.mjs'); const CHILDREN_ISLAND_FIXTURE = resolve(process.cwd(), 'test/ssr/fixtures/island-children.mjs'); const STATIC_CHILD_FIXTURE = resolve(process.cwd(), 'test/ssr/fixtures/static-child.mjs'); const FORWARDED_CHILDREN_FIXTURE = resolve( process.cwd(), 'test/ssr/fixtures/forwarded-children.mjs', ); /* Render the `rootExport` of a fixture module to a string in a *real* Node process (no `document`). * * This test file runs under happy-dom, which defines `document` — that flips wompo's `IS_SERVER` * (`typeof document === 'undefined'`) to false, so `html` takes its client branch and DROPS the * value at each closing dynamic tag (``). That misaligns `parts`/`values` and corrupts * the SSR output (the dynamic-tag subtree gets truncated). Production SSR always runs in Node where * `IS_SERVER` is true, so to exercise hydration against *real* server output we render in a Node * subprocess. The fixture module is also imported by the test itself so the client-side registry * has the same components available for `hydrate()`. */ function ssrFromFixture( fixturePath: string, rootExport: string, props: Record, ): string { const src = `import { ${rootExport} } from ${JSON.stringify(fixturePath)}; import { renderToString } from ${JSON.stringify(DIST_SSR)}; const r = await renderToString(${rootExport}, ${JSON.stringify(props)}); process.stdout.write(r.html);`; const tmp = `/tmp/wompo-hydrate-ssr-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`; writeFileSync(tmp, src); try { return execSync(`node ${JSON.stringify(tmp)}`, { encoding: 'utf8' }); } finally { unlinkSync(tmp); } } beforeEach(() => { // Clean DOM between tests document.body.innerHTML = ''; }); describe('hydrate', () => { it('binds click events to an SSR\'d island', async () => { function Counter({ start = 0 }: any) { const [count, setCount] = useState(start); const inc = () => setCount(count + 1); return html``; } defineWompo(Counter, { name: 'hydr-counter', island: 'load' }); const r = await renderToString(Counter, { start: 5 }); document.body.innerHTML = r.html; expect(document.body.innerHTML).toContain('data-wompo-island="0"'); hydrate(document); await tick(); const btn = document.querySelector('button')!; expect(btn.textContent?.trim()).toBe('5'); btn.dispatchEvent(new Event('click')); await tick(); expect(btn.textContent?.trim()).toBe('6'); }); it('hydration removes data-wompo-island marker', async () => { function C() { return html`hi`; } defineWompo(C, { name: 'hydr-cleanup', island: 'load' }); const r = await renderToString(C, {}); document.body.innerHTML = r.html; hydrate(document); await tick(); const el = document.querySelector('hydr-cleanup'); expect(el?.hasAttribute('data-wompo-island')).toBe(false); expect(el?.hasAttribute('data-wompo-mode')).toBe(false); }); it('non-island components are not hydrated', async () => { function C() { return html`plain`; } defineWompo(C, { name: 'hydr-plain' }); const r = await renderToString(C, {}); document.body.innerHTML = r.html; hydrate(document); await tick(); // Element renders, no marker expect(document.body.innerHTML).toContain('plain'); expect(document.body.innerHTML).not.toContain('data-wompo-island'); }); it('passes island props from JSON payload', async () => { function Greet({ name }: any) { return html`

Hello ${name}

`; } defineWompo(Greet, { name: 'hydr-greet', island: 'load' }); const r = await renderToString(Greet, { name: 'Wompo' }); document.body.innerHTML = r.html; hydrate(document); await tick(); const p = document.querySelector('p')!; expect(p.textContent?.replace(/\s+/g, '')).toBe('HelloWompo'); }); it('non-string props (numbers, arrays) survive hydration', async () => { function List({ items }: any) { return html``; } defineWompo(List, { name: 'hydr-list', island: 'load' }); const r = await renderToString(List, { items: [1, 2, 3] }); document.body.innerHTML = r.html; hydrate(document); await tick(); const items = document.querySelectorAll('li'); expect(items.length).toBe(3); expect(items[0].textContent?.trim()).toBe('1'); expect(items[2].textContent?.trim()).toBe('3'); }); it('hydrates a dynamic-tag nested island with structured children + a node interp', async () => { // Regression for: `[wompo] hydration mismatch in : expected '' at node // index 2, got — falling back to client render`. A parent island uses `<${Link}>` (itself // an island) with structured children that contain a parent-owned NODE interp (`${t}` inside a // ). The Link component re-homes those children into its own `${children}` render // output, interposing a `