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 :
')
})
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 ? :
')
})
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 ?
')
// 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 ? :
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 () =>
',
)
// Replace PageA with PageB (anchor is captured from PageA's div)
Page = PageB
root.render()
expect(container.innerHTML).toBe(
'
Count: 0
',
)
// PageB self-updates: same element type (div->div), should maintain position
count = 1
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe(
'
',
)
})
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(
,
)
expect(container.innerHTML).toBe('0')
// Add more items - new spans must appear BEFORE footer
items = [0, 1]
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe(
'01',
)
// Add even more
items = [0, 1, 2]
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe(
'012',
)
})
})
})