import { describe, it, expect, afterEach } from 'vitest'
import { createRoot } from '../lib/vdom.ts'
import { invariant } from '../lib/invariant.ts'
import type { Handle } from '../lib/component.ts'
describe('vnode rendering', () => {
afterEach(() => {
document.body.innerHTML = ''
for (let node of Array.from(document.head.childNodes)) {
document.head.removeChild(node)
}
})
describe('components', () => {
it.todo('warns when render is called after component is removed')
it('inserts a component', () => {
let container = document.createElement('div')
function App() {
return () =>
Hello, world!
}
let { render } = createRoot(container)
render()
expect(container.innerHTML).toBe('Hello, world!
')
})
it('updates a component', () => {
let container = document.createElement('div')
let capturedUpdate = () => {}
function App(handle: Handle) {
let count = 1
capturedUpdate = () => {
count++
handle.update()
}
return () => {count}
}
let root = createRoot(container)
root.render()
expect(container.innerHTML).toBe('1
')
let div = container.querySelector('div')
invariant(div instanceof HTMLDivElement)
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe('2
')
expect(container.querySelector('div')).toBe(div)
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe('3
')
expect(container.querySelector('div')).toBe(div)
})
it('updates a component with a fragment', () => {
let container = document.createElement('div')
let capturedUpdate = () => {}
function App(handle: Handle) {
let count = 1
capturedUpdate = () => {
count++
handle.update()
}
return () => (
<>
{Array.from({ length: count }).map((_, i) => (
{i}
))}
>
)
}
let root = createRoot(container)
root.render()
expect(container.innerHTML).toBe('0')
let span = container.querySelector('span')
invariant(span)
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe('01')
let newSpanTags = container.querySelectorAll('span')
expect(newSpanTags.length).toBe(2)
expect(newSpanTags[0]).toBe(span)
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe('012')
})
it('renders head-like elements in place on client updates', () => {
let container = document.createElement('div')
document.body.appendChild(container)
let rerender = () => {}
function App(handle: Handle) {
let phase = 0
rerender = () => {
phase++
handle.update()
}
return () => {
if (phase === 0) {
return (
<>
Page A
Phase A
>
)
}
if (phase === 1) {
return (
<>
Page B
Phase B
>
)
}
return Phase C
}
}
let root = createRoot(container)
root.render()
root.flush()
expect(container.querySelector('title')?.textContent).toBe('Page A')
expect(container.querySelector('meta[name="description"]')?.getAttribute('content')).toBe('A')
expect(container.querySelector('script[type="application/ld+json"]')?.textContent).toBe(
'{"name":"A"}',
)
expect(container.querySelector('script[type="text/javascript"]')).toBeTruthy()
expect(document.head.querySelector('title')).toBeNull()
expect(document.head.querySelector('meta[name="description"]')).toBeNull()
expect(document.head.querySelector('script[type="application/ld+json"]')).toBeNull()
rerender()
root.flush()
expect(container.querySelector('title')?.textContent).toBe('Page B')
expect(container.querySelector('meta[name="description"]')?.getAttribute('content')).toBe('B')
expect(container.querySelectorAll('meta[name="description"]')).toHaveLength(1)
expect(container.querySelector('script[type="application/ld+json"]')?.textContent).toBe(
'{"name":"B"}',
)
expect(container.querySelector('script[type="text/javascript"]')).toBeNull()
expect(container.querySelector('div')?.textContent).toBe('Phase B')
expect(document.head.querySelector('title')).toBeNull()
expect(document.head.querySelector('meta[name="description"]')).toBeNull()
expect(document.head.querySelector('script[type="application/ld+json"]')).toBeNull()
rerender()
root.flush()
expect(container.querySelector('title')).toBeNull()
expect(container.querySelector('meta[name="description"]')).toBeNull()
expect(container.querySelector('script[type="application/ld+json"]')).toBeNull()
expect(container.innerHTML).toBe('Phase C
')
})
it('dispose cleans up explicit head subtree', () => {
let container = document.createElement('div')
document.body.appendChild(container)
let root = createRoot(container)
root.render(
<>
Dispose title
Content
>,
)
root.flush()
expect(document.head.querySelector('title')?.textContent).toBe('Dispose title')
expect(document.head.querySelector('meta[name="dispose-description"]')).toBeTruthy()
expect(document.head.querySelector('script[type="application/ld+json"]')?.textContent).toBe(
'{"dispose":true}',
)
root.dispose()
expect(document.head.querySelector('title')).toBeNull()
expect(document.head.querySelector('meta[name="dispose-description"]')).toBeNull()
expect(document.head.querySelector('script[type="application/ld+json"]')).toBeNull()
expect(container.innerHTML).toBe('')
})
})
})