import { describe, it, expect } from 'vitest' import { createRoot } from './vdom.ts' import type { Remix } from './component.ts' import { invariant } from './invariant.ts' import { dom } from '@remix-run/events' import { Catch } from './component.ts' describe('vnode rendering', () => { describe('special attributes', () => { it.todo('className') it.todo('htmlFor') it.todo('acceptCharset') it.todo('httpEquiv') it.todo('xlinkHref') it.todo('xmlLang') it.todo('xmlSpace') it.todo('data-*') it.todo('aria-*') }) describe('special props', () => { it.todo('style') it.todo('value') it.todo('defaultValue') it.todo('checked') it.todo('defaultChecked') it.todo('disabled') }) describe('framework props', () => { it.todo('does not render key') it.todo('does not render on') it.todo('does not render css') it.todo('does not render children') it.todo('does not render tabIndex') it.todo('does not render acceptCharset') }) describe('css props', () => { it('adds class and styles', async () => { const container = document.createElement('div') let root = createRoot(container) root.render(
Hello
) let div = container.querySelector('div') invariant(div instanceof HTMLDivElement) expect(div.className).toMatch(/^rmx-/) document.body.appendChild(container) expect(getComputedStyle(div).color).toBe('rgb(255, 0, 0)') }) }) describe('svg', () => { it('renders SVG root and children with SVG namespace and attributes', () => { const container = document.createElement('div') let { render } = createRoot(container) render( , ) let svg = container.querySelector('svg') let path = container.querySelector('path') invariant(svg instanceof SVGSVGElement) invariant(path instanceof SVGPathElement) expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg') expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg') // Attribute casing: preserve exceptions and kebab-case general SVG attrs expect(svg.getAttribute('viewBox')).toBe('0 0 24 24') expect(path.getAttribute('stroke-linecap')).toBe('round') expect(path.getAttribute('stroke-linejoin')).toBe('round') }) it('supports xlinkHref -> xlink:href on SVG elements', () => { const container = document.createElement('div') let { render } = createRoot(container) render( , ) let useEl = container.querySelector('use') invariant(useEl instanceof SVGUseElement) expect(useEl.getAttribute('xlink:href')).toBe('#my-id') }) it('renders HTML subtree inside foreignObject with HTML namespace', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello
, ) let div = container.querySelector('#x') invariant(div) expect(div instanceof HTMLDivElement).toBe(true) expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml') }) it('updates and removes SVG attributes', () => { const container = document.createElement('div') let root = createRoot(container) root.render( , ) let path = container.querySelector('#p') invariant(path instanceof SVGPathElement) // Update value root.render( , ) let updated = container.querySelector('#p') invariant(updated instanceof SVGPathElement) expect(updated).toBe(path) expect(updated.getAttribute('stroke-linecap')).toBe('square') // Remove attribute root.render( , ) let removed = container.querySelector('#p') invariant(removed instanceof SVGPathElement) expect(removed.hasAttribute('stroke-linecap')).toBe(false) }) it('attaches events on SVG elements', () => { const container = document.createElement('div') let root = createRoot(container) let clicked = false root.render( { clicked = true })} /> , ) root.flush() // attach events let circle = container.querySelector('#c') invariant(circle instanceof SVGCircleElement) circle.dispatchEvent(new MouseEvent('click', { bubbles: true })) expect(clicked).toBe(true) }) it('hydrates existing SVG subtree and preserves nodes', () => { const container = document.createElement('div') container.innerHTML = '' let root = createRoot(container) let preSvg = container.querySelector('svg') let prePath = container.querySelector('#p') invariant(preSvg instanceof SVGSVGElement && prePath instanceof SVGPathElement) root.render( , ) let postSvg = container.querySelector('svg') let postPath = container.querySelector('#p') invariant(postSvg instanceof SVGSVGElement && postPath instanceof SVGPathElement) expect(postSvg).toBe(preSvg) expect(postPath).toBe(prePath) // Attribute should remain correct post-hydration expect(postPath.getAttribute('stroke-linecap')).toBe('round') }) }) describe('inserts', () => { it('renders text', () => { const container = document.createElement('div') let { render } = createRoot(container) render('Hello, world!') expect(container.innerHTML).toBe('Hello, world!') }) it('renders number', () => { const container = document.createElement('div') let { render } = createRoot(container) render(42) expect(container.innerHTML).toBe('42') }) it('renders 0', () => { const container = document.createElement('div') let { render } = createRoot(container) render(0) expect(container.innerHTML).toBe('0') }) it('renders bigint', () => { const container = document.createElement('div') let { render } = createRoot(container) render(BigInt(9007199254740991)) expect(container.innerHTML).toBe('9007199254740991') }) it('renders true', () => { const container = document.createElement('div') let { render } = createRoot(container) render(true) expect(container.innerHTML).toBe('') }) it('renders false', () => { const container = document.createElement('div') let { render } = createRoot(container) render(false) expect(container.innerHTML).toBe('') }) it('renders null', () => { const container = document.createElement('div') let { render } = createRoot(container) render(null) expect(container.innerHTML).toBe('') }) it('renders undefined', () => { const container = document.createElement('div') let { render } = createRoot(container) render(undefined) expect(container.innerHTML).toBe('') }) }) describe('elements', () => { it('renders basic elements', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') }) it('renders nested elements', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') }) it('renders attributes', () => { const container = document.createElement('div') let { render } = createRoot(container) render() expect(container.innerHTML).toBe('') }) it('renders 0 as a child', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
{0}
) expect(container.innerHTML).toBe('
0
') }) it('renders style object via DOM properties; hydration leaves string in place', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
X
, ) let div = container.querySelector('div') invariant(div instanceof HTMLDivElement) expect(div.style.marginTop).toBe('12px') expect(div.style.display).toBe('block') expect(div.getAttribute('style') || '').toContain('--size: 10') expect(div.style.lineHeight).toBe('') const container2 = document.createElement('div') container2.innerHTML = '
X
' let root2 = createRoot(container2) root2.render(
X
) let div2 = container2.querySelector('div') invariant(div2 instanceof HTMLDivElement) expect(div2.style.color).toBe('blue') }) }) describe('fragments', () => { it('inserts fragments', () => { const container = document.createElement('div') let { render } = createRoot(container) render( <>

Hello

world!

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

Hello

world!

') }) it('inserts nested fragments', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
<>

Hello

world!

Goodbye

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

Hello

world!

Goodbye

') }) it('inserts new nodes in a parent', () => { const container = document.createElement('div') let { render } = createRoot(container) render(

Hello

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

Hello

') let p = container.querySelector('p') invariant(p) render(

Hello

Goodbye

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

Hello

Goodbye

') expect(container.querySelector('p')).toBe(p) }) }) describe('type<-->type updates', () => { it('updates a text node', () => { const container = document.createElement('div') let { render } = createRoot(container) render('Hello, world!') expect(container.innerHTML).toBe('Hello, world!') render('Hello, world! 2') expect(container.innerHTML).toBe('Hello, world! 2') }) it('updates an element', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') let div = container.querySelector('div') render(
Hello, world! 2
) expect(container.innerHTML).toBe('
Hello, world! 2
') expect(container.querySelector('div')).toBe(div) }) it('updates an element with attributes', () => { const container = document.createElement('div') let { render } = createRoot(container) render() expect(container.innerHTML).toBe('') let input = container.querySelector('input') render() expect(container.innerHTML).toBe('') expect(container.querySelector('input')).toBe(input) }) it('updates a fragment', () => { const container = document.createElement('div') let { render } = createRoot(container) render( <>

Hello

world!

, ) let pTags = container.querySelectorAll('p') invariant(pTags.length === 2) expect(container.innerHTML).toBe('

Hello

world!

') render( <>

Goodbye

Universe

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

Goodbye

Universe

') let newPTags = container.querySelectorAll('p') expect(newPTags.length).toBe(2) expect(newPTags[0]).toBe(pTags[0]) expect(newPTags[1]).toBe(pTags[1]) }) it('updates a component', () => { const container = document.createElement('div') let setupCalls = 0 function App(this: Remix.Handle) { let state = ++setupCalls return ({ title }: { title: string }) => (
{title} {state}
) } let root = createRoot(container) root.render() expect(container.innerHTML).toBe('
Hello 1
') root.render() expect(container.innerHTML).toBe('
Goodbye 1
') }) it('updates a component with a fragment', () => { const container = document.createElement('div') let setupCalls = 0 function App(this: Remix.Handle) { let state = ++setupCalls return ({ title }: { title: string }) => ( <> {title} {state} ) } let root = createRoot(container) root.render() expect(container.innerHTML).toBe('Hello1') root.render() expect(container.innerHTML).toBe('Goodbye1') }) }) describe('removals', () => { it('removes a text node', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') render(
) expect(container.innerHTML).toBe('
') }) it('removes an element', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') render(
) expect(container.innerHTML).toBe('
') }) it('removes attributes', () => { const container = document.createElement('div') let { render } = createRoot(container) render() expect(container.innerHTML).toBe('') render() expect(container.innerHTML).toBe('') }) it('removes a fragment', () => { const container = document.createElement('div') let { render } = createRoot(container) render(
<>

Hello

world!

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

Hello

world!

') render(
) expect(container.innerHTML).toBe('
') }) it('removes a component', () => { const container = document.createElement('div') let { render } = createRoot(container) function App() { return
Hello, world!
} render(
, ) expect(container.innerHTML).toBe('
Hello, world!
') render(
) expect(container.innerHTML).toBe('
') }) }) describe('components', () => { it.todo('warns when render is called after component is removed') it('inserts a component', () => { const container = document.createElement('div') function App() { return
Hello, world!
} let { render } = createRoot(container) render() expect(container.innerHTML).toBe('
Hello, world!
') }) it('updates a component', () => { const container = document.createElement('div') let capturedUpdate = () => {} function App(this: Remix.Handle) { let count = 1 capturedUpdate = () => { count++ this.render() } 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', () => { const container = document.createElement('div') let capturedUpdate = () => {} function App(this: Remix.Handle) { let count = 1 capturedUpdate = () => { count++ this.render() } 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('raises errors to catch boundary', () => { const container = document.createElement('div') let root = createRoot(container) let capturedRaise: Remix.Handle['raise'] = () => {} function App(this: Remix.Handle) { capturedRaise = this.raise return () =>
App
} root.render( Error
}> , ) expect(container.innerHTML).toBe('
App
') capturedRaise(new Error('Test')) root.flush() expect(container.innerHTML).toBe('
Error
') }) }) it('runs setup tasks in a microtask', async () => { const container = document.createElement('div') let root = createRoot(container) let taskRan = false function App(this: Remix.Handle) { this.queueTask(() => { taskRan = true }) return () =>
Hello, world!
} root.render() expect(taskRan).toBe(false) await Promise.resolve() expect(taskRan).toBe(true) }) it('runs update tasks after updates', () => { const container = document.createElement('div') let root = createRoot(container) let taskRan = false let capturedUpdate = () => {} function App(this: Remix.Handle) { capturedUpdate = () => { this.queueTask(() => { taskRan = true }) this.render() } return () =>
Hello, world!
} root.render() root.flush() expect(taskRan).toBe(false) capturedUpdate() expect(taskRan).toBe(false) root.flush() expect(taskRan).toBe(true) }) it('runs task provided to render', () => { const container = document.createElement('div') let root = createRoot(container) let taskRan = false let capturedUpdate = () => {} function App(this: Remix.Handle) { capturedUpdate = () => { this.render(() => { taskRan = true }) } return () =>
Hello, world!
} root.render() root.flush() expect(taskRan).toBe(false) capturedUpdate() expect(taskRan).toBe(false) root.flush() expect(taskRan).toBe(true) }) describe('simple replacement', () => { it('replaces element -> text', () => { const container = document.createElement('div') let root = createRoot(container) root.render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') root.render('Goodbye, element!') expect(container.innerHTML).toBe('Goodbye, element!') }) it('replaces text -> element', () => { const container = document.createElement('div') let root = createRoot(container) root.render('Hello, world!') expect(container.innerHTML).toBe('Hello, world!') root.render(
Goodbye, world!
) expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces element -> component', () => { const container = document.createElement('div') let root = createRoot(container) root.render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') function App() { return
Goodbye, world!
} root.render() expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces component -> element', () => { const container = document.createElement('div') let root = createRoot(container) function App() { return
Hello, world!
} root.render() expect(container.innerHTML).toBe('
Hello, world!
') root.render(
Goodbye, world!
) expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces element -> element', () => { const container = document.createElement('div') let root = createRoot(container) root.render(
Hello, world!
) expect(container.innerHTML).toBe('
Hello, world!
') root.render() expect(container.innerHTML).toBe('') }) it('replaces component -> component', () => { const container = document.createElement('div') let root = createRoot(container) function App() { return
Hello, world!
} root.render() expect(container.innerHTML).toBe('
Hello, world!
') function App2() { return
Goodbye, world!
} root.render() expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces component -> fragment', () => { const container = document.createElement('div') let root = createRoot(container) function App() { return
Hello, world!
} root.render() expect(container.innerHTML).toBe('
Hello, world!
') root.render( <>

Goodbye

world!

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

Goodbye

world!

') }) it('replaces fragment -> component', () => { const container = document.createElement('div') let root = createRoot(container) root.render( <>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') function App() { return
Goodbye, world!
} root.render() expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces fragment -> element', () => { const container = document.createElement('div') let root = createRoot(container) root.render( <>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') root.render(
Goodbye, world!
) expect(container.innerHTML).toBe('
Goodbye, world!
') }) it('replaces fragment -> text', () => { const container = document.createElement('div') let root = createRoot(container) root.render( <>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') root.render('Goodbye, world!') expect(container.innerHTML).toBe('Goodbye, world!') }) it('replaces text -> component', () => { const container = document.createElement('div') let root = createRoot(container) root.render('Hello, world!') expect(container.innerHTML).toBe('Hello, world!') function App() { return
Goodbye, world!
} root.render() expect(container.innerHTML).toBe('
Goodbye, world!
') }) }) describe('complex replacements', () => { it('preserves siblings', () => { const container = document.createElement('div') let root = createRoot(container) root.render(
div
span

p

, ) expect(container.innerHTML).toBe('
div
span

p

') let div = container.querySelector('div') let p = container.querySelector('p') invariant(div && p) root.render(
div

p

, ) expect(container.innerHTML).toBe('
div

p

') expect(container.querySelector('div')).toBe(div) expect(container.querySelector('p')).toBe(p) }) it('replaces null children', () => { const container = document.createElement('div') let root = createRoot(container) root.render(
div
{null}

p

, ) expect(container.innerHTML).toBe('
div

p

') let div = container.querySelector('div') let p = container.querySelector('p') invariant(div && p) root.render(
div
span

p

, ) expect(container.innerHTML).toBe('
div
span

p

') expect(container.querySelector('div')).toBe(div) expect(container.querySelector('p')).toBe(p) }) it('replaces fragment components', () => { const container = document.createElement('div') let root = createRoot(container) function Frag() { return ( <> A B ) } root.render(
main
, ) expect(container.innerHTML).toBe('
AB
main
') let main = container.querySelector('main') invariant(main) root.render(
one
main
, ) expect(container.innerHTML).toBe('
one
main
') expect(container.querySelector('main')).toBe(main) }) it('replaces components within elements', () => { const container = document.createElement('div') let root = createRoot(container) function App() { return
Hello, world!
} root.render(
, ) expect(container.innerHTML).toBe('
Hello, world!
') function App2() { return
Goodbye, world!
} root.render(
, ) expect(container.innerHTML).toBe('
Goodbye, world!
') }) }) describe('Catch', () => { it('renders as a fragment', () => { const container = document.createElement('div') let root = createRoot(container) root.render( Error
}>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') }) it('removes', () => { const container = document.createElement('div') let root = createRoot(container) root.render( Error
}>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') root.render(
) expect(container.innerHTML).toBe('
') }) it('renders when descendant throws', () => { const container = document.createElement('div') let root = createRoot(container) function Throws() { throw new Error('Test') } root.render( Error
}> , ) expect(container.innerHTML).toBe('
Error
') }) it('removes in progress nodes', () => { const container = document.createElement('div') let root = createRoot(container) function Throws() { throw new Error('Test') } root.render( Error}>

orphan

orphan

, ) expect(container.innerHTML).toBe('
Error
') }) it('renders fallback from deeply thrown errors', () => { const container = document.createElement('div') let root = createRoot(container) function Throws() { throw new Error('Test') } function App() { return (

App

) } root.render( Error}> , ) expect(container.innerHTML).toBe('
Error
') }) it('is cleared by a parent update', () => { const container = document.createElement('div') let root = createRoot(container) let count = 0 function MaybeThrow() { count++ if (count === 1) { throw new Error('Test') } return
All good
} root.render( Error}> , ) expect(container.innerHTML).toBe('
Error
') root.render( Error}> , ) expect(container.innerHTML).toBe('
All good
') }) it('retains nodes on happy path updates', () => { let container = document.createElement('div') let root = createRoot(container) root.render( Error}>
Hello, world!
, ) expect(container.innerHTML).toBe('
Hello, world!
') let div = container.querySelector('div') invariant(div) root.render( Error}>
Goodbye, world!
, ) expect(container.innerHTML).toBe('
Goodbye, world!
') expect(container.querySelector('div')).toBe(div) }) it('preserves siblings when Catch trips and resets on parent re-render', () => { const container = document.createElement('div') let root = createRoot(container) function ThrowsOnce() { throw new Error('boom') } root.render(
A E}> B
, ) expect(container.innerHTML).toBe( '
AEB
', ) let left = container.querySelector('#left') let right = container.querySelector('#right') invariant(left instanceof HTMLSpanElement && right instanceof HTMLSpanElement) function Ok() { return
OK
} root.render(
A E}> B
, ) expect(container.innerHTML).toBe( '
A
OK
B
', ) expect(container.querySelector('#left')).toBe(left) expect(container.querySelector('#right')).toBe(right) }) it('uses nearest Catch on event errors (nested boundaries)', () => { const container = document.createElement('div') let root = createRoot(container) function App() { return ( Outer}>
Inner
}>
) } root.render() expect(container.innerHTML).toBe('
') root.flush() let button = container.querySelector('#btn') invariant(button instanceof HTMLButtonElement) button.click() root.flush() // Only the inner boundary should trip expect(container.innerHTML).toBe('
Inner
') }) it('preserves siblings when Catch trips and resets on parent re-render from events', () => { const container = document.createElement('div') document.body.appendChild(container) let root = createRoot(container) root.render(
left Error}>
, ) expect(container.innerHTML).toBe( '
left
', ) root.flush() // flush for events to attach let button = container.querySelector('button') invariant(button) button.click() expect(container.innerHTML).toBe( '
leftError
', ) let left = container.querySelector('span') let right = container.querySelector('aside') invariant(left && right) root.render(
left E}>
, ) expect(container.innerHTML).toBe( '
left
', ) expect(container.querySelector('span')).toBe(left) expect(container.querySelector('aside')).toBe(right) }) it('clears after an event error', () => { const container = document.createElement('div') let root = createRoot(container) root.render( Error}> , ) root.flush() let button = container.querySelector('button') invariant(button) button.click() root.flush() expect(container.innerHTML).toBe('
Error
') root.render( Error}> , ) root.flush() expect(container.innerHTML).toBe('') }) }) describe('events integration', () => { it('attaches events', () => { const container = document.createElement('div') let root = createRoot(container) let clicked = false root.render( , ) expect(container.innerHTML).toBe('') root.flush() // events attachment happens after rendering let button = container.querySelector('button') invariant(button) button.click() expect(clicked).toBe(true) }) it('reuses the event container', () => { const container = document.createElement('div') let root = createRoot(container) let clickCount = 0 function App() { return ( ) } root.render() root.flush() let button = container.querySelector('button') invariant(button) button.click() expect(clickCount).toBe(1) root.render() root.flush() button.click() expect(clickCount).toBe(2) }) it('cleans up the event container', () => { const container = document.createElement('div') let root = createRoot(container) let clickCount = 0 root.render( , ) root.flush() let button = container.querySelector('button') invariant(button) button.click() expect(clickCount).toBe(1) // remove on prop root.render() root.flush() button.click() expect(clickCount).toBe(1) }) it('renders catch fallback with event errors', () => { const container = document.createElement('div') let root = createRoot(container) let clicked = false root.render( Error}> , ) expect(container.innerHTML).toBe('') root.flush() let button = container.querySelector('button') invariant(button) button.click() root.flush() expect(clicked).toBe(true) expect(container.innerHTML).toBe('
Error
') }) }) describe('context', () => { it('provides and reads context', () => { let container = document.createElement('div') function App(this: Remix.Handle<{ value: string }>) { this.context.set({ value: 'test' }) return ({ children }: { children: Remix.RemixNode }) =>
{children}
} function Child(this: Remix.Handle) { let { value } = this.context.get(App) return () =>
Child: {value}
} let root = createRoot(container) root.render( , ) expect(container.innerHTML).toContain('Child: test') }) it('provides context on updates', () => { let container = document.createElement('div') let capturedUpdate = () => {} function App(this: Remix.Handle<{ value: string }>) { this.context.set({ value: 'test' }) capturedUpdate = () => { this.context.set({ value: 'test2' }) this.render() } return ({ children }: { children: Remix.RemixNode }) =>
{children}
} function Child(this: Remix.Handle) { let { value } = this.context.get(App) return
Child: {value}
} let root = createRoot(container) root.render( , ) expect(container.innerHTML).toContain('Child: test') capturedUpdate() root.flush() expect(container.innerHTML).toContain('Child: test2') }) it('renders descendants in order of appearance', () => { let container = document.createElement('div') let options: string[] = [] let renderListbox = () => {} function Listbox(this: Remix.Handle<{ registerOption: (option: string) => void }>) { this.context.set({ registerOption: (option: string) => { options.push(option) }, }) renderListbox = this.render return ({ children }: { children: Remix.RemixNode }) => { options = [] return
{children}
} } function Option(this: Remix.Handle) { let { registerOption } = this.context.get(Listbox) return ({ value }: { value: string }) => { registerOption(value) return
Option
} } function App(this: Remix.Handle) { return () => ( ) } let root = createRoot(container) root.render() expect(options).toEqual(['Option 1', 'Option 2', 'Option 3']) renderListbox() root.flush() expect(options).toEqual(['Option 1', 'Option 2', 'Option 3']) }) }) describe('scheduling', () => { it('skips descendant updates if ancestor is scheduled', () => { let container = document.createElement('div') let root = createRoot(container) let capturedParentUpdate = () => {} let appRenderCount = 0 function Parent(this: Remix.Handle) { capturedParentUpdate = () => { this.render() } return ({ children }: { children: Remix.RemixNode }) => { appRenderCount++ return children } } let childRenderCount = 0 let capturedChildUpdate = () => {} function Child(this: Remix.Handle) { capturedChildUpdate = () => { this.render() } return () => { childRenderCount++ return
Hello, world!
} } root.render( , ) expect(container.innerHTML).toBe('
Hello, world!
') expect(appRenderCount).toBe(1) expect(childRenderCount).toBe(1) capturedChildUpdate() capturedParentUpdate() root.flush() expect(appRenderCount).toBe(2) expect(childRenderCount).toBe(2) // swap order capturedParentUpdate() capturedChildUpdate() root.flush() expect(appRenderCount).toBe(3) expect(childRenderCount).toBe(3) }) it('only runs tasks once', async () => { let container = document.createElement('div') let root = createRoot(container) let taskCount = 0 let capturedUpdate = () => {} function App(this: Remix.Handle) { this.queueTask(() => { taskCount++ }) capturedUpdate = () => { this.render(() => { taskCount++ }) } return () => null } root.render() root.flush() expect(taskCount).toBe(1) capturedUpdate() root.flush() expect(taskCount).toBe(2) }) }) describe('signals', () => { it('passes signal to render callback and aborts on reentry', () => { let container = document.createElement('div') let root = createRoot(container) let capturedSignals: AbortSignal[] = [] function App(this: Remix.Handle) { return (_props: unknown, signal: AbortSignal) => { capturedSignals.push(signal) return null } } root.render() expect(capturedSignals.length).toBe(1) expect(capturedSignals[0].aborted).toBe(false) root.render() expect(capturedSignals.length).toBe(2) expect(capturedSignals[0].aborted).toBe(true) expect(capturedSignals[1].aborted).toBe(false) }) it('provides mounted signal on this.signal', () => { let container = document.createElement('div') let root = createRoot(container) let capturedSignal: AbortSignal | undefined function App(this: Remix.Handle) { capturedSignal = this.disconnectedSignal return () => null } root.render() invariant(capturedSignal) expect(capturedSignal).toBeInstanceOf(AbortSignal) expect(capturedSignal.aborted).toBe(false) root.render(null) expect(capturedSignal.aborted).toBe(true) }) }) })