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('') }) }) })