import { describe, expect, it } from 'vitest' import { userEvent } from '@vitest/browser/context' import type { Handle } from '../lib/component.ts' import { createRoot } from '../lib/vdom.ts' import { on } from '../index.ts' describe('vdom controlled props', () => { it('restores controlled value on native input when no update happens', async () => { let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement input.value = 'hello123' input.dispatchEvent(new Event('input', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(input.value).toBe('hello') }) it('restores controlled checked on native change when no update happens', async () => { let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement input.checked = false input.dispatchEvent(new Event('change', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(input.checked).toBe(true) }) it('allows controlled value changes when an input event calls handle.update()', () => { function App(handle: Handle) { let value = 'hello' let renderCount = 0 function rerender() { renderCount++ handle.update() } return () => ( <> { let nextValue = event.currentTarget.value if (/\d/.test(nextValue)) return value = nextValue rerender() }), ]} /> {`${renderCount}:${value}`} ) } let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement let output = container.querySelector('output') as HTMLOutputElement input.value = 'helloa' input.dispatchEvent(new Event('input', { bubbles: true })) root.flush() expect(input.value).toBe('helloa') expect(output.textContent).toBe('1:helloa') }) it('preserves controlled updates from real typing while rejecting invalid input', async () => { function App(handle: Handle) { let value = 'hello' let renderCount = 0 function rerender() { renderCount++ handle.update() } return () => ( <> { let nextValue = event.currentTarget.value if (/\d/.test(nextValue)) return value = nextValue rerender() }), ]} /> {`${renderCount}:${value}`} ) } let container = document.createElement('div') document.body.appendChild(container) let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement let output = container.querySelector('output') as HTMLOutputElement await userEvent.type(input, 'a') root.flush() expect(input.value).toBe('helloa') expect(output.textContent).toBe('1:helloa') await userEvent.type(input, '1') await Promise.resolve() await Promise.resolve() expect(input.value).toBe('helloa') expect(output.textContent).toBe('1:helloa') await userEvent.type(input, 'b') root.flush() expect(input.value).toBe('helloab') expect(output.textContent).toBe('2:helloab') container.remove() }) it('does not clobber controlled value when input event commits a new value', async () => { function App(handle: Handle) { let value = 'hello' return () => ( { value = event.currentTarget.value handle.update() }), ]} /> ) } let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement input.value = 'helloa' input.dispatchEvent(new Event('input', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(input.value).toBe('helloa') }) it('does not control value/checked when prop value is undefined', async () => { let container = document.createElement('div') let root = createRoot(container) root.render( <> , ) root.flush() let text = container.querySelector('#text') as HTMLInputElement text.value = 'user typed' text.dispatchEvent(new Event('input', { bubbles: true })) let check = container.querySelector('#check') as HTMLInputElement check.checked = true check.dispatchEvent(new Event('change', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(text.value).toBe('user typed') expect(check.checked).toBe(true) }) it('restores controlled value on native change for select when no update happens', async () => { let container = document.createElement('div') let root = createRoot(container) root.render( , ) root.flush() let select = container.querySelector('select') as HTMLSelectElement select.value = 'a' select.dispatchEvent(new Event('change', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(select.value).toBe('b') }) it('applies next select value from change handlers after prior input event', async () => { function App(handle: Handle) { let value = 'alpha' return () => ( <> {value} ) } let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let select = container.querySelector('select') as HTMLSelectElement let output = container.querySelector('output') as HTMLOutputElement select.value = 'beta' select.dispatchEvent(new Event('input', { bubbles: true })) await Promise.resolve() await Promise.resolve() select.dispatchEvent(new Event('change', { bubbles: true })) root.flush() expect(select.value).toBe('beta') expect(output.textContent).toBe('beta') }) it('detaches controlled listeners on dispose', async () => { let container = document.createElement('div') let root = createRoot(container) root.render() root.flush() let input = container.querySelector('input') as HTMLInputElement root.dispose() root.flush() input.value = 'post-dispose' input.dispatchEvent(new Event('input', { bubbles: true })) input.dispatchEvent(new Event('change', { bubbles: true })) await Promise.resolve() await Promise.resolve() expect(input.value).toBe('post-dispose') }) })