)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
const input = document.getElementById('my-input') as HTMLInputElement
input.focus()
expect(document.activeElement).toBe(input)
// Trigger a re-render by clicking the button
document.getElementById('update-label')?.click()
await flushUpdates()
// The label should have updated
expect(document.querySelector('label')?.textContent).toBe('updated')
// The same input element should still be in the DOM and focused
const inputAfter = document.getElementById('my-input') as HTMLInputElement
expect(inputAfter).toBe(input)
expect(document.activeElement).toBe(input)
})
})
it('should preserve focus on a textarea across re-renders', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-focus-textarea-test',
render: ({ useState }) => {
const [count, setCount] = useState('count', 0)
return (
Count: {count}
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
const textarea = document.getElementById('my-textarea') as HTMLTextAreaElement
textarea.focus()
expect(document.activeElement).toBe(textarea)
document.getElementById('increment')?.click()
await flushUpdates()
expect(document.querySelector('span')?.textContent).toBe('Count: 1')
expect(document.getElementById('my-textarea')).toBe(textarea)
expect(document.activeElement).toBe(textarea)
})
})
})
describe('form value preservation', () => {
it('should preserve user-typed input value across re-renders when value is not controlled', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-form-value-test',
render: ({ useState }) => {
const [title, setTitle] = useState('title', 'Title')
return (
{title}
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
// Simulate user typing
const input = document.getElementById('user-input') as HTMLInputElement
input.value = 'user typed this'
// Trigger re-render
document.getElementById('change-title')?.click()
await flushUpdates()
// Title should have changed
expect(document.querySelector('h1')?.textContent).toBe('New Title')
// Input value should be preserved (same element, value not in render props)
const inputAfter = document.getElementById('user-input') as HTMLInputElement
expect(inputAfter).toBe(input)
expect(inputAfter.value).toBe('user typed this')
})
})
it('should preserve checkbox checked state across re-renders', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-checkbox-test',
render: ({ useState }) => {
const [count, setCount] = useState('count', 0)
return (
Count: {count}
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
// User checks the checkbox
const checkbox = document.getElementById('my-checkbox') as HTMLInputElement
checkbox.checked = true
// Trigger re-render
document.getElementById('increment')?.click()
await flushUpdates()
expect(document.querySelector('span')?.textContent).toBe('Count: 1')
// Checkbox should still be checked
const checkboxAfter = document.getElementById('my-checkbox') as HTMLInputElement
expect(checkboxAfter).toBe(checkbox)
expect(checkboxAfter.checked).toBe(true)
})
})
it('should preserve select value across re-renders', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-select-test',
render: ({ useState }) => {
const [label, setLabel] = useState('label', 'Pick one')
return (
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
// User selects option B
const select = document.getElementById('my-select') as HTMLSelectElement
select.value = 'b'
// Trigger re-render
document.getElementById('update-label')?.click()
await flushUpdates()
expect(document.querySelector('label')?.textContent).toBe('Updated label')
// Select should still have value 'b'
const selectAfter = document.getElementById('my-select') as HTMLSelectElement
expect(selectAfter).toBe(select)
expect(selectAfter.value).toBe('b')
})
})
})
describe('element identity preservation', () => {
it('should preserve DOM element references across re-renders', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-identity-test',
render: ({ useState }) => {
const [count, setCount] = useState('count', 0)
return (
Count: {count}
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
const container = document.getElementById('container')
const counter = document.getElementById('counter')
const button = document.getElementById('increment')
// Trigger re-render
button?.click()
await flushUpdates()
// Same elements should be reused
expect(document.getElementById('container')).toBe(container)
expect(document.getElementById('counter')).toBe(counter)
expect(document.getElementById('increment')).toBe(button)
expect(counter?.textContent).toBe('Count: 1')
})
})
it('should replace element when tag changes between renders', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-tag-change-test',
render: ({ useState }) => {
const [useDiv, setUseDiv] = useState('useDiv', true)
return useDiv ? (
) : (
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
const oldContent = document.getElementById('content')
expect(oldContent?.tagName).toBe('DIV')
document.getElementById('toggle')?.click()
await flushUpdates()
const newContent = document.getElementById('content')
expect(newContent?.tagName).toBe('SECTION')
// Different tag means different element
expect(newContent).not.toBe(oldContent)
})
})
})
describe('animation continuity', () => {
it('should preserve CSS class-based transitions by keeping element identity', async () => {
await usingAsync(new Injector(), async (injector) => {
const rootElement = document.getElementById('root') as HTMLDivElement
const ExampleComponent = Shade({
customElementName: 'morph-animation-test',
render: ({ useState }) => {
const [isActive, setIsActive] =
// eslint-disable-next-line furystack/no-css-state-hooks -- test for re-render behavior, not CSS state
useState('isActive', false)
return (
)
},
})
initializeShadeRoot({
injector,
rootElement,
jsxElement: ,
})
await flushUpdates()
expect(document.getElementById('child-value')?.textContent).toBe('0')
const childElement = document.querySelector('morph-child-component')
// Trigger parent re-render
document.getElementById('parent-increment')?.click()
await flushUpdates()
// Child should be the same DOM element (not recreated)
expect(document.querySelector('morph-child-component')).toBe(childElement)
// Child should have been updated with new props
expect(document.getElementById('child-value')?.textContent).toBe('1')
})
})
})
})