/* Unit tests for the SSR serializer. * * These tests intentionally import from the dist/ build so module-instance sharing * (server-runtime ↔ render-context) matches production behavior. */ import { describe, expect, it } from 'vitest'; // @ts-ignore — JS dist import { defineWompo, html, attrs, useState, useMemo, useRef, useId, useContext, createContext, Suspense, useAsync, // @ts-ignore } from '../../dist/wompo.js'; // @ts-ignore — JS dist import { renderToString } from '../../dist/ssr/index.js'; // Helper: strip extra whitespace between tags for stable assertions. const compact = (s: string) => s.replace(/\n\s*/g, '').trim(); // The serializer emits `...` markers around node-position interpolations (and // `` around re-homed children regions) so the hydration runtime can locate the // dynamic-node regions, and ` data-wompo-ssr=""` on every wompo component element so the // client's connectedCallback skips a destructive re-render. Tests that assert on rendered content // usually don't care about either; strip them for stable assertions. const stripMarkers = (s: string) => s.replace(//g, '').replace(/ data-wompo-ssr(?:="\d+")?/g, ''); /* ---------- Primitives & escape ---------- */ describe('serializer: primitives', () => { it('renders text + numbers + escapes text content', async () => { function Hello({ name }: any) { return html`

Hi, ${name}! <${'span'}>${42}

`; } defineWompo(Hello, { name: 'prim-hello' }); const r = await renderToString(Hello, { name: '' }); expect(stripMarkers(r.html)).toContain('Hi, <bob>!'); expect(stripMarkers(r.html)).toContain('42'); }); it('skips falsy node values', async () => { function F() { return html`

${null}${undefined}${false}${0}

`; } defineWompo(F, { name: 'prim-falsy' }); const r = await renderToString(F, {}); expect(stripMarkers(r.html)).toContain('

0

'); }); it('escapes attribute values', async () => { function A({ v }: any) { return html`

x

`; } defineWompo(A, { name: 'prim-attr' }); const r = await renderToString(A, { v: 'a"b { it('emits static + interpolated attrs on native elements', async () => { function A({ cls, id }: any) { return html`
x
`; } defineWompo(A, { name: 'attr-native' }); const r = await renderToString(A, { cls: 'foo', id: 'bar' }); expect(stripMarkers(r.html)).toContain('
x
'); }); it('skips @event handlers on native elements', async () => { function A() { const cb = () => {}; return html``; } defineWompo(A, { name: 'attr-event-native' }); const r = await renderToString(A, {}); expect(r.html).not.toMatch(/@?click=/); expect(stripMarkers(r.html)).toContain('type="button"'); }); it('emits a primitive prop on a component as an HTML attribute', async () => { function Child({ n }: any) { return html`${n}`; } defineWompo(Child, { name: 'attr-child' }); function Parent() { return html`<${Child} n=${7} />`; } defineWompo(Parent, { name: 'attr-parent' }); const r = await renderToString(Parent, {}); expect(stripMarkers(r.html)).toContain('7'); }); it('bare boolean attribute on a component: prop is true AND the attribute survives on the host tag', async () => { function Child({ text }: any) { return html`x`; } defineWompo(Child, { name: 'attr-bare-child' }); function Parent() { return html`<${Child} text href="/projects" />`; } defineWompo(Parent, { name: 'attr-bare-parent' }); const r = await renderToString(Parent, {}); const out = stripMarkers(r.html); // The component saw text=true during SSR… expect(out).toContain('class="is-text"'); // …and the host tag keeps the bare attribute, so the client upgrade // (getAttribute → '' → true) re-derives the same prop after hydration. expect(out).toMatch(//); }); it('attrs() spread on a native element', async () => { function A() { const bag = attrs({ 'data-a': '1', 'data-b': 'two', disabled: true }); return html``; } defineWompo(A, { name: 'attr-spread-native' }); const r = await renderToString(A, {}); expect(r.html).toMatch(/data-a="1"/); expect(r.html).toMatch(/data-b="two"/); expect(r.html).toMatch(/ disabled/); }); it('composed attribute (prefix + interp + suffix)', async () => { function A({ n }: any) { return html`

x

`; } defineWompo(A, { name: 'attr-composed' }); const r = await renderToString(A, { n: 3 }); expect(stripMarkers(r.html)).toContain('title="count=3!"'); }); }); /* ---------- Children and nested components ---------- */ describe('serializer: nested components', () => { it('passes children to a component via ${children}', async () => { function Card({ children }: any) { return html`
${children}
`; } defineWompo(Card, { name: 'nest-card' }); function App() { return html`<${Card}>

title

body

`; } defineWompo(App, { name: 'nest-app' }); const r = await renderToString(App, {}); expect(stripMarkers(r.html)).toContain( '

title

body

', ); }); it('wraps re-homed dynamic-tag children in an owner-tagged marker', async () => { // When a parent passes structured children INTO a component via a dynamic tag // (`<${Comp}>…children…`), the component re-homes them through `${children}`. A // parent-owned NODE interp inside those children still needs its own `` marker, but the // whole re-homed region is wrapped in ``/`` — the id matching the // component's `data-wompo-ssr` value — so the parent's hydration adopt() can tell the // dynamic-tag children apart from the component's own node interpolations AND from regions // re-homed further down the chain (children forwarded into another component). function Link({ children, href }: any) { return html`${children}`; } defineWompo(Link, { name: 'wc-link' }); function Nav({ label }: any) { return html`<${Link} href="/x">${label}`; } defineWompo(Nav, { name: 'wc-nav' }); const r = await renderToString(Nav, { label: 'hello' }); // The Link element carries an ssr id, and its re-homed children region is tagged with it; // the parent-owned ${label} interp keeps its own inside that region. const idMatch = r.html.match(/]* data-wompo-ssr="(\d+)"/); expect(idMatch).not.toBeNull(); expect(r.html).toContain(``); expect(r.html).toContain('hello'); expect(r.html).toContain(''); }); it('renders an array of templates', async () => { function Item({ v }: any) { return html`
  • ${v}
  • `; } defineWompo(Item, { name: 'nest-item' }); function List({ items }: any) { return html`
      ${items.map((v: any) => html`<${Item} v=${v} />`)}
    `; } defineWompo(List, { name: 'nest-list' }); const r = await renderToString(List, { items: ['a', 'b', 'c'] }); expect(stripMarkers(r.html)).toContain('
  • a
  • '); expect(stripMarkers(r.html)).toContain('
  • b
  • '); expect(stripMarkers(r.html)).toContain('
  • c
  • '); }); }); /* ---------- Shadow DOM ---------- */ describe('serializer: shadow DOM', () => { it('wraps rendered output in