import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createRoot } from '../lib/vdom.ts' import { renderToString } from '../lib/stream.ts' import { invariant } from '../lib/invariant.ts' describe('hydration', () => { let container: HTMLDivElement beforeEach(() => { container = document.createElement('div') document.body.appendChild(container) }) afterEach(() => { document.body.innerHTML = '' }) describe('attribute mismatch handling', () => { it('adopts element and patches mismatched attributes', async () => { let html = await renderToString(
) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let root = createRoot(container) root.render(
) root.flush() // Same DOM node should be adopted (not recreated) expect(container.querySelector('div')).toBe(existingDiv) // Attributes should be patched to client values expect(existingDiv.getAttribute('class')).toBe('client-class') expect(existingDiv.getAttribute('data-value')).toBe('client') }) it('adds missing attributes during hydration', async () => { let html = await renderToString(
) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let root = createRoot(container) root.render(
) root.flush() expect(container.querySelector('div')).toBe(existingDiv) expect(existingDiv.getAttribute('data-new')).toBe('added') expect(existingDiv.getAttribute('title')).toBe('hello') }) it('leaves extra attributes alone during hydration', async () => { let html = await renderToString(
) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) expect(existingDiv.getAttribute('data-extra')).toBe('yes') expect(existingDiv.getAttribute('title')).toBe('extra') let root = createRoot(container) root.render(
) root.flush() // Element should be adopted expect(container.querySelector('div')).toBe(existingDiv) expect(existingDiv.getAttribute('class')).toBe('keep') // Extra attributes left alone during hydration (not tracked, so not removed) expect(existingDiv.hasAttribute('data-extra')).toBe(true) expect(existingDiv.hasAttribute('title')).toBe(true) }) it('preserves DOM node identity when only attributes differ', async () => { let html = await renderToString(
Child
, ) container.innerHTML = html let existingDiv = container.querySelector('#test') let existingSpan = container.querySelector('span') invariant(existingDiv && existingSpan) let root = createRoot(container) root.render(
Child
, ) root.flush() // Both parent and child should be the same DOM nodes expect(container.querySelector('#test')).toBe(existingDiv) expect(container.querySelector('span')).toBe(existingSpan) }) }) describe('type mismatch handling', () => { it('advances cursor once on type mismatch to find our element', async () => { let html = await renderToString(
Our content
, ) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let existingSpan = container.querySelector('span') invariant(existingSpan) // Inject different element type at start let injected = document.createElement('div') injected.className = 'injected' existingDiv.insertBefore(injected, existingSpan) // Suppress console.error for expected hydration mismatch log let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) let root = createRoot(container) root.render(
Our content
, ) root.flush() errorSpy.mockRestore() // Our span should be adopted after advancing past injected div expect(container.querySelector('span')).toBe(existingSpan) }) it('recreates element if retry also fails', async () => { let html = await renderToString(
Original
, ) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let existingSpan = container.querySelector('span') invariant(existingSpan) // Replace span with completely different structure existingDiv.innerHTML = '
Wrong

Also wrong

' let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) let root = createRoot(container) root.render(
Original
, ) root.flush() errorSpy.mockRestore() // Should have created a new span since no match found let newSpan = container.querySelector('span') expect(newSpan).not.toBe(existingSpan) expect(newSpan?.textContent).toBe('Original') }) it('leaves skipped nodes in place', async () => { let html = await renderToString(
Our content
, ) container.innerHTML = html let existingDiv = container.querySelector('div') invariant(existingDiv) let existingSpan = container.querySelector('span') invariant(existingSpan) // Inject element that will be skipped let skipped = document.createElement('aside') skipped.id = 'skipped' skipped.textContent = 'Extension content' existingDiv.insertBefore(skipped, existingSpan) let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) let root = createRoot(container) root.render(
Our content
, ) root.flush() errorSpy.mockRestore() // Skipped element should still be in the DOM expect(existingDiv.querySelector('#skipped')).toBe(skipped) }) }) })