import { describe, it, expect } from 'vitest'
import { dom } from '@remix-run/events'
import type { Props } from '../html-types.ts'
import { render } from '../render.ts'
import type { RemixNode, Handle } from '@remix-run/component'
import { flushRenderQueue as flush } from '../reconcile.ts'
describe('property vs attribute mapping', () => {
it('sets input value via property and preserves caret on rerender', () => {
let container = document.createElement('div')
let text = 'hello'
function App() {
return
}
let root = render(, container)
let input = container.querySelector('input') as HTMLInputElement
expect(input.value).toBe('hello')
// Simulate user cursor at index 3
input.focus()
input.setSelectionRange(3, 3)
// Re-render with the same value, caret should remain
root.update()
expect(input.value).toBe('hello')
expect(input.selectionStart).toBe(3)
expect(input.selectionEnd).toBe(3)
// Update value and ensure property changes
text = 'hello!'
root.update()
expect(input.value).toBe('hello!')
})
it('toggles checkbox checked via property, independent of attribute', () => {
let container = document.createElement('div')
let checked = false
function App() {
return
}
let root = render(, container)
let input = container.querySelector('input') as HTMLInputElement
expect(input.checked).toBe(false)
expect(input.getAttribute('checked')).toBeNull()
checked = true
root.update()
expect(input.checked).toBe(true)
// Attribute may not be present; property is the source of truth
expect(input.getAttribute('checked') === '' || input.getAttribute('checked') === null).toBe(
true,
)
})
it('updates select multiple and selectedIndex via properties', () => {
let container = document.createElement('div')
let multi = false
let idx = 1
function App() {
return (
)
}
let root = render(, container)
let select = container.querySelector('select') as HTMLSelectElement
expect(select.multiple).toBe(false)
expect(select.selectedIndex).toBe(1)
multi = true
idx = 2
root.update()
expect(select.multiple).toBe(true)
expect(select.selectedIndex).toBe(2)
})
it('keeps SVG attributes attribute-based', () => {
let container = document.createElement('div')
let r = 40
function App() {
return (
)
}
let root = render(, container)
let circle = container.querySelector('circle')!
expect(circle.getAttribute('r')).toBe('40')
r = 20
root.update()
expect(circle.getAttribute('r')).toBe('20')
})
it('reordering keyed inputs preserves typed values and submitted FormData (uncontrolled)', () => {
let container = document.createElement('div')
let order = ['one', 'two', 'three']
function App() {
return (
)
}
// Initial mount
let root = render(, container)
// Type into the inputs (simulate user input)
let inputOne = container.querySelector('input[name="one"]') as HTMLInputElement
let inputTwo = container.querySelector('input[name="two"]') as HTMLInputElement
let inputThree = container.querySelector('input[name="three"]') as HTMLInputElement
inputOne.value = 'alpha'
inputTwo.value = 'beta'
inputThree.value = 'gamma'
// Reorder the inputs (keys remain the same, DOM nodes should move not recreate)
order = ['three', 'one', 'two']
root.update()
// Visually (property) values should persist after reorder
inputOne = container.querySelector('input[name="one"]') as HTMLInputElement
inputTwo = container.querySelector('input[name="two"]') as HTMLInputElement
inputThree = container.querySelector('input[name="three"]') as HTMLInputElement
expect(inputOne.value).toBe('alpha')
expect(inputTwo.value).toBe('beta')
expect(inputThree.value).toBe('gamma')
// Submitted values should reflect the current input values
let form = container.querySelector('form') as HTMLFormElement
let formData = new FormData(form)
expect(formData.get('one')).toBe('alpha')
expect(formData.get('two')).toBe('beta')
expect(formData.get('three')).toBe('gamma')
})
it('reordering keyed inputs preserves typed values and submitted FormData (controlled via value prop)', () => {
let container = document.createElement('div')
let order = ['one', 'two', 'three']
let values: Record = { one: '', two: '', three: '' }
function App() {
return (
)
}
// Initial mount
let root = render(, container)
// Simulate user typing by setting values and re-rendering (controlled pattern)
values = { one: 'alpha', two: 'beta', three: 'gamma' }
root.update()
// Reorder the inputs
order = ['three', 'one', 'two']
root.update()
// Values should remain correct by name (as controlled props)
let inputOne = container.querySelector('input[name="one"]') as HTMLInputElement
let inputTwo = container.querySelector('input[name="two"]') as HTMLInputElement
let inputThree = container.querySelector('input[name="three"]') as HTMLInputElement
expect(inputOne.value).toBe('alpha')
expect(inputTwo.value).toBe('beta')
expect(inputThree.value).toBe('gamma')
// Form submission should reflect the same values
let form = container.querySelector('form') as HTMLFormElement
let formData = new FormData(form)
expect(formData.get('one')).toBe('alpha')
expect(formData.get('two')).toBe('beta')
expect(formData.get('three')).toBe('gamma')
})
})
describe('innerHTML behavior', () => {
it('innerHTML prop replaces children content', () => {
let element = document.createElement('div')
function App() {
return
Bold text'}>This content should be replaced
}
let root = render(, element)
// innerHTML should work and replace children
expect(element.innerHTML).toContain('Bold text')
expect(element.innerHTML).not.toContain('This content should be replaced')
expect(element.innerHTML).toBe('
Bold text
')
})
it('innerHTML allows unescaped HTML for intentional use', () => {
let element = document.createElement('div')
function App() {
return (
Emphasized and bold text'}>Replaced content
)
}
let root = render(, element)
expect(element.innerHTML).toContain('Emphasized')
expect(element.innerHTML).toContain('bold')
expect(element.innerHTML).not.toContain('Replaced content')
})
it('escapes text content safely', () => {
let element = document.createElement('div')
function App() {
return
{""}
}
let root = render(, element)
// Text content should be escaped and visible as text
expect(element.textContent).toBe("")
expect(element.innerHTML).toContain('<script>')
})
it('clears innerHTML', () => {
let element = document.createElement('div')
function App() {
return
This should be cleared
}
let root = render(, element)
expect(element.innerHTML).toBe('')
expect(element.innerHTML).not.toContain('This should be cleared')
})
})
describe('on prop normalization', () => {
it('normalizes on prop to an array in custom components', () => {
let element = document.createElement('div')
let consumerClickCount = 0
let componentClickCount = 0
function TestButton({ on = [], ...rest }: Props<'button'>) {
// No boilerplate needed! The type system now ensures 'on' is always an array
return (