import { describe, it, expect } from 'vitest' import type { Handle } from '../lib/component.ts' import { createRangeRoot } from '../lib/vdom.ts' import { invariant } from '../lib/invariant.ts' import { on } from '../index.ts' describe('createRangeRoot', () => { describe('event forwarding', () => { it('forwards bubbling DOM error events to range root listeners', () => { let host = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') host.append(start, end) let root = createRangeRoot([start, end]) let forwarded: unknown root.addEventListener('error', (event) => { forwarded = (event as ErrorEvent).error }) let expected = new Error('createRangeRoot forwarded error') host.dispatchEvent(new ErrorEvent('error', { bubbles: true, error: expected })) expect(forwarded).toBe(expected) }) it('stops forwarding bubbling DOM error events after dispose', () => { let host = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') host.append(start, end) let root = createRangeRoot([start, end]) let forwarded: unknown root.addEventListener('error', (event) => { forwarded = (event as ErrorEvent).error }) root.dispose() host.dispatchEvent( new ErrorEvent('error', { bubbles: true, error: new Error('after dispose') }), ) expect(forwarded).toBeUndefined() }) }) describe('basic rendering', () => { it('dispose is a no-op before first render', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.dispose() root.flush() expect(container.innerHTML).toBe('') }) it('renders content between markers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(
Hello
) root.flush() expect(container.innerHTML).toBe('
Hello
') }) it('renders text between markers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render('Hello, world!') root.flush() expect(container.innerHTML).toBe('Hello, world!') }) it('renders fragments between markers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render( <>

First

Second

, ) root.flush() expect(container.innerHTML).toBe('

First

Second

') }) it('renders components between markers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) function Greeting() { return () => Hello! } let root = createRangeRoot([start, end]) root.render() root.flush() expect(container.innerHTML).toBe('Hello!') }) }) describe('updates', () => { it('updates content between markers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(
First
) root.flush() expect(container.innerHTML).toBe('
First
') root.render(
Second
) root.flush() expect(container.innerHTML).toBe('
Second
') }) it('handles adding children', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(
) root.flush() root.render(
Added
, ) root.flush() expect(container.innerHTML).toBe('
Added
') }) it('handles removing children', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(
To remove
, ) root.flush() root.render(
) root.flush() expect(container.innerHTML).toBe('
') }) it('replaces all fragment children with different elements', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render( <>
First
Second
, ) root.flush() expect(container.innerHTML).toBe('
First
Second
') // Replace divs with spans root.render( <> A B , ) root.flush() expect(container.innerHTML).toBe('AB') }) it('renders null then content', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(null) root.flush() expect(container.innerHTML).toBe('') root.render(
Now visible
) root.flush() expect(container.innerHTML).toBe('
Now visible
') }) it('renders content then null then content', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render(
Visible
) root.flush() expect(container.innerHTML).toBe('
Visible
') root.render(null) root.flush() expect(container.innerHTML).toBe('') root.render(Back again) root.flush() expect(container.innerHTML).toBe('Back again') }) it('changes fragment child count', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let root = createRangeRoot([start, end]) root.render( <>
One
Two
Three
, ) root.flush() expect(container.innerHTML).toBe( '
One
Two
Three
', ) // Reduce to one child root.render(
Only
) root.flush() expect(container.innerHTML).toBe('
Only
') // Back to multiple root.render( <> A B , ) root.flush() expect(container.innerHTML).toBe('AB') }) }) describe('events', () => { it('attaches event handlers', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) let clicked = false let root = createRangeRoot([start, end]) root.render( , ) root.flush() let button = container.querySelector('button') invariant(button) button.click() expect(clicked).toBe(true) }) }) describe('multiple range roots', () => { it('supports multiple ranges in same container', () => { let container = document.createElement('div') container.innerHTML = '
A

static

B
' let startA = container.childNodes[0] as Comment let endA = container.childNodes[2] as Comment let startB = container.childNodes[4] as Comment let endB = container.childNodes[6] as Comment let existingDivA = container.querySelector('div') let existingDivB = container.querySelectorAll('div')[1] let rootA = createRangeRoot([startA, endA]) let rootB = createRangeRoot([startB, endB]) rootA.render(
A updated
) rootB.render(
B updated
) rootA.flush() rootB.flush() // Content between markers updated, static content unchanged expect(container.innerHTML).toBe( '
A updated

static

B updated
', ) // Original nodes reused expect(container.querySelector('div')).toBe(existingDivA) expect(container.querySelectorAll('div')[1]).toBe(existingDivB) }) it('ranges are independent', () => { let container = document.createElement('div') let startA = document.createComment('a') let endA = document.createComment('/a') let startB = document.createComment('b') let endB = document.createComment('/b') container.appendChild(startA) container.appendChild(endA) container.appendChild(startB) container.appendChild(endB) let rootA = createRangeRoot([startA, endA]) let rootB = createRangeRoot([startB, endB]) rootA.render(A) rootA.flush() // Only A has content, B is empty expect(container.innerHTML).toBe('A') rootB.render(B) rootB.flush() expect(container.innerHTML).toBe( 'AB', ) }) }) describe('boundary handling', () => { it('throws when start and end markers do not share a parent node', () => { let containerA = document.createElement('div') let containerB = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') containerA.appendChild(start) containerB.appendChild(end) expect(() => createRangeRoot([start, end])).toThrow('Boundaries must share parent') }) it('does not affect content before start marker', () => { let container = document.createElement('div') container.innerHTML = '
Before
' let start = container.childNodes[1] as Comment let end = container.childNodes[2] as Comment let root = createRangeRoot([start, end]) root.render(
Inside
) root.flush() expect(container.innerHTML).toBe( '
Before
Inside
', ) }) it('does not affect content after end marker', () => { let container = document.createElement('div') // Range has existing content to hydrate container.innerHTML = '
Old
After
' let start = container.childNodes[0] as Comment let end = container.childNodes[2] as Comment let root = createRangeRoot([start, end]) root.render(
New
) root.flush() expect(container.innerHTML).toBe('
New
After
') }) it('handles empty ranges with content after end marker', () => { let container = document.createElement('div') // Empty range with content following it container.innerHTML = '
After
' let start = container.firstChild as Comment let end = container.childNodes[1] as Comment let footer = container.querySelector('footer') invariant(footer) let root = createRangeRoot([start, end]) root.render(
New content
) root.flush() // Content should be inserted inside the range, not adopt the footer expect(container.innerHTML).toBe( '
New content
After
', ) // Footer should be unchanged expect(container.querySelector('footer')).toBe(footer) }) it('preserves surrounding content during updates', () => { let container = document.createElement('div') container.innerHTML = '
H

Old

F
' let start = container.childNodes[1] as Comment let end = container.childNodes[3] as Comment let root = createRangeRoot([start, end]) root.render(

New

) root.flush() expect(container.innerHTML).toBe( '
H

New

F
', ) // Multiple updates shouldn't affect boundaries root.render(

Updated again

) root.flush() expect(container.innerHTML).toBe( '
H

Updated again

F
', ) }) }) describe('stateful components', () => { it('maintains component state across renders', () => { let container = document.createElement('div') let start = document.createComment('start') let end = document.createComment('end') container.appendChild(start) container.appendChild(end) function Counter(handle: Handle, setup: number) { let count = setup return () => ( ) } let root = createRangeRoot([start, end]) root.render() root.flush() let button = container.querySelector('button') invariant(button) expect(button.textContent).toBe('0') button.click() root.flush() expect(button.textContent).toBe('1') button.click() root.flush() expect(button.textContent).toBe('2') }) }) })