import { describe, it, expect, 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('extra DOM nodes (browser extension injection)', () => {
it('ignores extra nodes at the end of container', async () => {
let html = await renderToString(
Our content
,
)
container.innerHTML = html
// Simulate browser extension injecting content at the end
let existingDiv = container.querySelector('div')
invariant(existingDiv)
let existingSpan = container.querySelector('span')
invariant(existingSpan)
let injected = document.createElement('aside')
injected.id = 'ext-injected'
injected.textContent = 'extension content'
existingDiv.appendChild(injected)
let root = createRoot(container)
root.render(
Our content
,
)
root.flush()
// Our content should be adopted
expect(container.querySelector('span')).toBe(existingSpan)
// Injected content should still be there
expect(existingDiv.querySelector('#ext-injected')).toBe(injected)
})
it('skips injected node at start and adopts our content', async () => {
let html = await renderToString(
Our content
,
)
container.innerHTML = html
let existingDiv = container.querySelector('div')
invariant(existingDiv)
let existingSpan = container.querySelector('span')
invariant(existingSpan)
// Simulate browser extension injecting content at the START
let injected = document.createElement('aside')
injected.id = 'ext-start'
injected.textContent = 'extension content'
existingDiv.insertBefore(injected, existingSpan)
let root = createRoot(container)
root.render(
Our content
,
)
root.flush()
// Our span should be adopted (cursor advanced past injected aside)
expect(container.querySelector('span')).toBe(existingSpan)
// Injected content should still be there
expect(existingDiv.querySelector('#ext-start')).toBe(injected)
})
it('handles injected nodes at both start and end', 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 at start
let injectedStart = document.createElement('aside')
injectedStart.id = 'ext-start'
injectedStart.textContent = 'start extension'
existingDiv.insertBefore(injectedStart, existingSpan)
// Inject at end
let injectedEnd = document.createElement('aside')
injectedEnd.id = 'ext-end'
injectedEnd.textContent = 'end extension'
existingDiv.appendChild(injectedEnd)
let root = createRoot(container)
root.render(
Our content
,
)
root.flush()
// Our span should be adopted
expect(container.querySelector('span')).toBe(existingSpan)
// Both injected elements should remain
expect(existingDiv.querySelector('#ext-start')).toBe(injectedStart)
expect(existingDiv.querySelector('#ext-end')).toBe(injectedEnd)
})
it('extra nodes survive through subsequent updates', async () => {
let html = await renderToString(
Content 1
,
)
container.innerHTML = html
let existingDiv = container.querySelector('div')
invariant(existingDiv)
// Inject at end
let injected = document.createElement('aside')
injected.id = 'extension'
injected.textContent = 'extension content'
existingDiv.appendChild(injected)
let root = createRoot(container)
root.render(
Content 1
,
)
root.flush()
expect(existingDiv.querySelector('#extension')).toBe(injected)
// Update our content
root.render(
Content 2
,
)
root.flush()
// Injected content should still be there after update
expect(existingDiv.querySelector('#extension')).toBe(injected)
expect(existingDiv.querySelector('span')?.textContent).toBe('Content 2')
})
})
})