import { describe, it, expect, beforeEach, afterEach } from 'vitest' import type { Handle } from '../lib/component.ts' import { createRoot } from '../lib/vdom.ts' import { renderToString } from '../lib/stream.ts' import { clientEntry } from '../lib/client-entries.ts' import { invariant } from '../lib/invariant.ts' import { on, ref } from '../index.ts' describe('hydration', () => { let container: HTMLDivElement beforeEach(() => { container = document.createElement('div') document.body.appendChild(container) }) afterEach(() => { document.body.innerHTML = '' for (let node of Array.from(document.head.childNodes)) { document.head.removeChild(node) } }) describe('component edge cases', () => { it('hydrates component that returns null', async () => { function NullComponent() { return () => null } let html = await renderToString(
After
, ) container.innerHTML = html let existingSpan = container.querySelector('span') invariant(existingSpan) let root = createRoot(container) root.render(
After
, ) root.flush() expect(container.querySelector('span')).toBe(existingSpan) expect(existingSpan.textContent).toBe('After') }) it('hydrates component that returns fragment', async () => { function FragmentComponent() { return () => ( <> First Second ) } let html = await renderToString(
, ) container.innerHTML = html let spans = container.querySelectorAll('span') expect(spans).toHaveLength(2) let root = createRoot(container) root.render(
, ) root.flush() let hydratedSpans = container.querySelectorAll('span') expect(hydratedSpans[0]).toBe(spans[0]) expect(hydratedSpans[1]).toBe(spans[1]) }) it('hydrates nested hydration boundaries', async () => { let Outer = clientEntry('/outer.js#Outer', function Outer() { return (props: { children: any }) =>
{props.children}
}) let Inner = clientEntry('/inner.js#Inner', function Inner() { return () => Inner content }) let html = await renderToString( , ) container.innerHTML = html // Should have hydration comment markers expect(html).toContain('') let existingOuter = container.querySelector('.outer') let existingInner = container.querySelector('.inner') invariant(existingOuter && existingInner) // For this test, we use createRoot which should handle the comment markers let root = createRoot(container) root.render( , ) root.flush() // Both should be adopted expect(container.querySelector('.outer')).toBe(existingOuter) expect(container.querySelector('.inner')).toBe(existingInner) }) it('hydrates component with state preservation', async () => { function Counter(handle: Handle, setup: number) { let count = setup return () => ( ) } let html = await renderToString() container.innerHTML = html let existingButton = container.querySelector('button') invariant(existingButton) expect(existingButton.textContent).toBe('Count: 5') let root = createRoot(container) root.render() root.flush() // Button should be adopted expect(container.querySelector('button')).toBe(existingButton) // Clicking should work existingButton.click() root.flush() expect(existingButton.textContent).toBe('Count: 6') }) }) describe('additional scenarios', () => { it('hydrates context across component boundaries', async () => { function Provider(handle: Handle<{ value: string }>) { handle.context.set({ value: 'from context' }) return (props: { children: any }) =>
{props.children}
} function Consumer(handle: Handle) { let ctx = handle.context.get(Provider) return () => {ctx?.value ?? 'no context'} } let html = await renderToString( , ) container.innerHTML = html let existingProvider = container.querySelector('.provider') let existingConsumer = container.querySelector('.consumer') invariant(existingProvider && existingConsumer) expect(existingConsumer.textContent).toBe('from context') let root = createRoot(container) root.render( , ) root.flush() expect(container.querySelector('.provider')).toBe(existingProvider) expect(container.querySelector('.consumer')).toBe(existingConsumer) expect(existingConsumer.textContent).toBe('from context') }) it('hydrates SVG elements with case-sensitive tags', async () => { let html = await renderToString( , ) container.innerHTML = html let existingSvg = container.querySelector('svg') let existingGradient = container.querySelector('linearGradient') let existingRect = container.querySelector('rect') invariant(existingSvg && existingGradient && existingRect) let root = createRoot(container) root.render( , ) root.flush() expect(container.querySelector('svg')).toBe(existingSvg) expect(container.querySelector('linearGradient')).toBe(existingGradient) expect(container.querySelector('rect')).toBe(existingRect) }) it('hydrates innerHTML prop', async () => { let html = await renderToString(
) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) expect(existingDiv.innerHTML).toBe('Raw HTML') let root = createRoot(container) root.render(
) root.flush() expect(container.querySelector('div')).toBe(existingDiv) expect(existingDiv.innerHTML).toBe('Raw HTML') }) it('hydrates style prop as object', async () => { let html = await renderToString(
Styled
, ) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let root = createRoot(container) root.render(
Styled
, ) root.flush() expect(container.querySelector('div')).toBe(existingDiv) // Style should be applied expect(existingDiv.style.color).toBe('red') expect(existingDiv.style.backgroundColor).toBe('blue') }) it('calls ref callback after hydration', async () => { let connectedNode: HTMLDivElement | null = null function WithConnect() { return () => (
{ connectedNode = node as HTMLDivElement }), ]} > Connected
) } let html = await renderToString() container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let root = createRoot(container) root.render() root.flush() // Ref should be called with the adopted node expect(connectedNode).toBe(existingDiv) }) it('attaches event handlers during hydration', async () => { let clicked = false function Clickable() { return () => ( ) } let html = await renderToString() container.innerHTML = html let existingButton = container.querySelector('button') invariant(existingButton) let root = createRoot(container) root.render() root.flush() // Button should be adopted expect(container.querySelector('button')).toBe(existingButton) // Event should work existingButton.click() expect(clicked).toBe(true) }) it('hydrates keyed elements', async () => { let items = [ { id: 'a', text: 'Item A' }, { id: 'b', text: 'Item B' }, { id: 'c', text: 'Item C' }, ] let html = await renderToString(
    {items.map((item) => (
  • {item.text}
  • ))}
, ) container.innerHTML = html let existingItems = container.querySelectorAll('li') expect(existingItems).toHaveLength(3) let root = createRoot(container) root.render(
    {items.map((item) => (
  • {item.text}
  • ))}
, ) root.flush() let hydratedItems = container.querySelectorAll('li') expect(hydratedItems[0]).toBe(existingItems[0]) expect(hydratedItems[1]).toBe(existingItems[1]) expect(hydratedItems[2]).toBe(existingItems[2]) }) it('hydrates deeply nested elements', async () => { let html = await renderToString(
Deep content
, ) container.innerHTML = html let level1 = container.querySelector('.level-1') let level2 = container.querySelector('.level-2') let level3 = container.querySelector('.level-3') let level4 = container.querySelector('.level-4') let span = container.querySelector('span') invariant(level1 && level2 && level3 && level4 && span) let root = createRoot(container) root.render(
Deep content
, ) root.flush() // All levels should be adopted expect(container.querySelector('.level-1')).toBe(level1) expect(container.querySelector('.level-2')).toBe(level2) expect(container.querySelector('.level-3')).toBe(level3) expect(container.querySelector('.level-4')).toBe(level4) expect(container.querySelector('span')).toBe(span) }) it('hydrates head-like elements in place', () => { container.innerHTML = 'Hydrated title' + '' + '' + '
Body content
' let existingTitle = container.querySelector('title') let existingMeta = container.querySelector('meta[name="description"]') let existingLdJson = container.querySelector('script[type="application/ld+json"]') let existingContent = container.querySelector('#content') invariant(existingTitle && existingMeta && existingLdJson && existingContent) let root = createRoot(container) root.render( <> Hydrated title
Body content
, ) root.flush() expect(container.querySelector('title')).toBe(existingTitle) expect(container.querySelector('meta[name="description"]')).toBe(existingMeta) expect(container.querySelector('script[type="application/ld+json"]')).toBe(existingLdJson) expect(container.querySelector('#content')).toBe(existingContent) expect(document.head.querySelector('title')).toBeNull() expect(document.head.querySelector('meta[name="description"]')).toBeNull() expect(document.head.querySelector('script[type="application/ld+json"]')).toBeNull() }) }) })