Server pages legitimately carry scripts (state serialization, analytics) that are spent by hydration time and that client views never render — they are noise to the detector, and morph dropping them is harmless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
// Hydration: adoption semantics and mismatch detection.
|
|
|
|
import { test, before } from 'node:test'
|
|
import assert from 'node:assert'
|
|
import { Window } from 'happy-dom'
|
|
|
|
let html, hydrate
|
|
|
|
before(async () => {
|
|
const win = new Window()
|
|
globalThis.window = win
|
|
globalThis.document = win.document
|
|
;({ default: html } = await import('../browser.js'))
|
|
;({ default: hydrate } = await import('../hydrate.js'))
|
|
})
|
|
|
|
test('agreeing trees hydrate without a mismatch report', () => {
|
|
const a = html`<div class="x"><h1>hi</h1><p>body</p></div>`
|
|
const b = html`<div class="x"><h1>hi</h1><p>body</p></div>`
|
|
let reported = null
|
|
hydrate(a, b, { onMismatch: (d) => { reported = d } })
|
|
assert.strictEqual(reported, null)
|
|
})
|
|
|
|
test('adoption: matching child nodes keep their identity through hydrate', () => {
|
|
const server = html`<div><h1>hi</h1><input value="typed by user" /></div>`
|
|
const serverH1 = server.querySelector('h1')
|
|
const serverInput = server.querySelector('input')
|
|
|
|
const client = html`<div><h1>hi</h1><input value="typed by user" /></div>`
|
|
hydrate(server, client)
|
|
|
|
assert.strictEqual(server.querySelector('h1'), serverH1, 'h1 adopted in place')
|
|
assert.strictEqual(server.querySelector('input'), serverInput, 'input adopted in place')
|
|
})
|
|
|
|
test('text mismatches are reported with a path, client wins', () => {
|
|
const server = html`<div><h1>count is 0</h1></div>`
|
|
const client = html`<div><h1>count is 7</h1></div>`
|
|
let reported = null
|
|
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
|
|
|
assert.strictEqual(reported.reason, 'text')
|
|
assert.match(reported.path, /div > h1/)
|
|
assert.strictEqual(reported.server, 'count is 0')
|
|
assert.strictEqual(reported.client, 'count is 7')
|
|
assert.strictEqual(server.textContent, 'count is 7', 'client render won')
|
|
})
|
|
|
|
test('attribute mismatches are reported', () => {
|
|
const server = html`<div><a href="/old">go</a></div>`
|
|
const client = html`<div><a href="/new">go</a></div>`
|
|
let reported = null
|
|
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
|
|
|
assert.strictEqual(reported.reason, 'attribute')
|
|
assert.match(reported.server, /\/old/)
|
|
assert.match(reported.client, /\/new/)
|
|
})
|
|
|
|
test('child count mismatches are reported', () => {
|
|
const server = html`<ul><li>a</li></ul>`
|
|
const client = html`<ul><li>a</li><li>b</li></ul>`
|
|
let reported = null
|
|
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
|
|
|
assert.strictEqual(reported.reason, 'children')
|
|
})
|
|
|
|
test('server-only <script> elements are not reported as mismatches', () => {
|
|
// server pages carry state/analytics scripts the client never renders;
|
|
// they are spent by hydration time (morph may drop them — harmless)
|
|
const wrap = document.createElement('div')
|
|
wrap.innerHTML = '<div><h1>hi</h1><script>window.x=1</script></div>'
|
|
const server = wrap.firstChild
|
|
const client = html`<div><h1>hi</h1></div>`
|
|
let reported = null
|
|
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
|
assert.strictEqual(reported, null)
|
|
})
|
|
|
|
test('mismatch detection is optional and hydrate still morphs without it', () => {
|
|
const server = html`<p>old</p>`
|
|
const client = html`<p>new</p>`
|
|
hydrate(server, client)
|
|
assert.strictEqual(server.textContent, 'new')
|
|
})
|