import { describe, it, expect } from 'vitest' import { createRoot } from '../lib/vdom.ts' import type { Handle } from '../lib/component.ts' describe('vnode rendering', () => { describe('conditional rendering and DOM order', () => { it('maintains DOM order when component switches element types via self-update', () => { let container = document.createElement('div') let root = createRoot(container) let showB = false let capturedUpdate = () => {} function A(handle: Handle) { capturedUpdate = () => handle.update() return () => (showB ? B :
A
) } root.render(

C

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

C

') showB = true capturedUpdate() root.flush() expect(container.innerHTML).toBe('
B

C

') showB = false capturedUpdate() root.flush() expect(container.innerHTML).toBe('
A

C

') }) it('maintains DOM order when component switches from component to element via self-update', () => { let container = document.createElement('div') let root = createRoot(container) let showB = false let capturedUpdate = () => {} function B() { return () => B } function A(handle: Handle) { capturedUpdate = () => handle.update() return () => (showB ? :
A
) } root.render(

C

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

C

') showB = true capturedUpdate() root.flush() expect(container.innerHTML).toBe('
B

C

') showB = false capturedUpdate() root.flush() expect(container.innerHTML).toBe('
A

C

') }) it('updates correctly when replaced component self-updates from component to element', () => { // This tests the stale anchor bug: when component A is replaced by B, // the anchor for B is captured from A's DOM. If B then self-updates // to change its content type, the stale anchor must not be used. let container = document.createElement('div') let root = createRoot(container) function Loading() { return () =>
Loading...
} let loaded = false let capturedUpdate = () => {} function PageB(handle: Handle) { capturedUpdate = () => handle.update() return () => (loaded ?
Loaded!
: ) } function PageA() { return () =>
Page A
} let Page: typeof PageA | typeof PageB = PageA function App() { return () => (
) } root.render() expect(container.innerHTML).toBe('
Page A
') // Switch to PageB (captures anchor from PageA's div) Page = PageB root.render() expect(container.innerHTML).toBe('
Loading...
') // PageB self-updates: Loading -> div (must not use stale PageA anchor) loaded = true capturedUpdate() root.flush() expect(container.innerHTML).toBe('
Loaded!
') }) it('updates correctly when component switches from element to component via self-update', () => { let container = document.createElement('div') let root = createRoot(container) function Loading() { return () => Loading... } let showLoading = false let capturedUpdate = () => {} function A(handle: Handle) { capturedUpdate = () => handle.update() return () => (showLoading ? :
Content
) } root.render(

Footer

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

Footer

') showLoading = true capturedUpdate() root.flush() expect(container.innerHTML).toBe('
Loading...

Footer

') showLoading = false capturedUpdate() root.flush() expect(container.innerHTML).toBe('
Content

Footer

') }) it('updates correctly with deeply nested component type changes', () => { let container = document.createElement('div') let root = createRoot(container) function Inner() { return () => Inner } function Middle() { return () => } let useNested = true let capturedUpdate = () => {} function Outer(handle: Handle) { capturedUpdate = () => handle.update() return () => (useNested ? :
Direct
) } root.render(
Footer
, ) expect(container.innerHTML).toBe('
Inner
Footer
') useNested = false capturedUpdate() root.flush() expect(container.innerHTML).toBe('
Direct
Footer
') useNested = true capturedUpdate() root.flush() expect(container.innerHTML).toBe('
Inner
Footer
') }) it('updates correctly when multiple components are replaced and self-update', () => { let container = document.createElement('div') let root = createRoot(container) function LoadingA() { return () => Loading A... } function LoadingB() { return () => Loading B... } let loadedA = false let loadedB = false let capturedUpdateA = () => {} let capturedUpdateB = () => {} function CompA(handle: Handle) { capturedUpdateA = () => handle.update() return () => (loadedA ?
A Done
: ) } function CompB(handle: Handle) { capturedUpdateB = () => handle.update() return () => (loadedB ?
B Done
: ) } root.render(
, ) expect(container.innerHTML).toBe( '
Loading A...Loading B...
', ) // Update A first loadedA = true capturedUpdateA() root.flush() expect(container.innerHTML).toBe('
A Done
Loading B...
') // Then update B loadedB = true capturedUpdateB() root.flush() expect(container.innerHTML).toBe('
A Done
B Done
') }) it('maintains DOM order when replaced component self-updates with same element type', () => { // Tests that anchor calculation works for same-type updates (element->element) // after a component replacement. The anchor should be the next sibling, not the // component's own content. let container = document.createElement('div') let root = createRoot(container) let count = 0 let capturedUpdate = () => {} function PageB(handle: Handle) { capturedUpdate = () => handle.update() return () =>
Count: {count}
} function PageA() { return () =>
Page A
} let Page: typeof PageA | typeof PageB = PageA function App() { return () => (
Footer
) } root.render() expect(container.innerHTML).toBe( '
Page A
Footer
', ) // Replace PageA with PageB (anchor is captured from PageA's div) Page = PageB root.render() expect(container.innerHTML).toBe( '
Count: 0
Footer
', ) // PageB self-updates: same element type (div->div), should maintain position count = 1 capturedUpdate() root.flush() expect(container.innerHTML).toBe( '
Count: 1
Footer
', ) // Another self-update count = 2 capturedUpdate() root.flush() expect(container.innerHTML).toBe( '
Count: 2
Footer
', ) }) it('maintains DOM order when fragment component adds children via self-update with siblings', () => { // Critical test: a component renders a fragment, has siblings after it, // and grows the fragment via self-update. Without proper anchor calculation, // new children would be appended after the siblings. let container = document.createElement('div') let root = createRoot(container) let items = [0] let capturedUpdate = () => {} function List(handle: Handle) { capturedUpdate = () => handle.update() // No keys - uses index-based diff return () => ( <> {items.map((i) => ( {i} ))} ) } root.render(
Footer
, ) expect(container.innerHTML).toBe('
0
Footer
') // Add more items - new spans must appear BEFORE footer items = [0, 1] capturedUpdate() root.flush() expect(container.innerHTML).toBe( '
01
Footer
', ) // Add even more items = [0, 1, 2] capturedUpdate() root.flush() expect(container.innerHTML).toBe( '
012
', ) }) }) })