import { describe, it, expect } from 'vitest' import type { Handle } from '@remix-run/component' import { render } from '../render.ts' describe('mount', () => { it('performs diffing instead of clearing innerHTML', () => { let element = document.createElement('div') let renderCount = 0 let lastProps: any = null function TestComponent(this: Handle, setupProps: { value: string }) { return (props: { value: string }) => { renderCount++ lastProps = { value: props.value } return
Value: {props.value}
} } // First mount render(, element) expect(element.innerHTML).toContain('Value: first') expect(renderCount).toBe(1) expect(lastProps.value).toBe('first') // Get reference to the DOM node to verify it's preserved let contentDiv = element.querySelector('[data-testid="content"]') as HTMLElement let originalDiv = contentDiv // Second mount - should diff instead of replacing render(, element) expect(element.innerHTML).toContain('Value: second') expect(renderCount).toBe(2) expect(lastProps.value).toBe('second') // Verify the same DOM node was reused (diffed) instead of replaced contentDiv = element.querySelector('[data-testid="content"]') as HTMLElement expect(contentDiv).toBe(originalDiv) // Same DOM node reference }) it('preserves DOM state across updates', () => { let element = document.createElement('div') function TestComponent(this: Handle, setupProps: { label: string }) { return (props: { label: string }) => (
) } // First mount render(, element) // User types in the input let input = element.querySelector('[data-testid="test-input"]') as HTMLInputElement input.value = 'user typed text' // Second mount with different label render(, element) // Input value should be preserved due to diffing input = element.querySelector('[data-testid="test-input"]') as HTMLInputElement expect(input.value).toBe('user typed text') expect(element.innerHTML).toContain('Second Label') }) it('handles component replacement correctly', () => { let element = document.createElement('div') function ComponentA() { return
Component A
} function ComponentB() { return
Component B
} // Mount Component A let root = render(, element) expect(element.innerHTML).toContain('Component A') expect(element.querySelector('[data-component="A"]')).toBeTruthy() // Mount Component B - should replace A root.update() expect(element.innerHTML).toContain('Component B') expect(element.querySelector('[data-component="B"]')).toBeTruthy() expect(element.querySelector('[data-component="A"]')).toBeFalsy() }) }) describe('RootInstance.cleanup()', () => { it('calls all component cleanup functions', () => { let element = document.createElement('div') let setupCleanupCalled = false let renderCleanupCalled = false function TestComponent(this: Handle) { // Setup-time cleanup this.onCleanup(() => { setupCleanupCalled = true }) // Setup-time afterRender with cleanup this.afterRender(() => { return () => { renderCleanupCalled = true } }) return () =>
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') expect(setupCleanupCalled).toBe(false) expect(renderCleanupCalled).toBe(false) // Call cleanup - should trigger all cleanup functions root.unmount() expect(setupCleanupCalled).toBe(true) expect(renderCleanupCalled).toBe(true) }) it('calls ref cleanup functions', () => { let element = document.createElement('div') let refSetCalls = 0 let refCleanupCalls = 0 function refCallback(node: HTMLElement | null) { if (node) { refSetCalls++ } else { refCleanupCalls++ } } function TestComponent() { return
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') expect(refSetCalls).toBe(1) expect(refCleanupCalls).toBe(0) // Call cleanup - should trigger ref cleanup root.unmount() expect(refSetCalls).toBe(1) // Still only 1 set call expect(refCleanupCalls).toBe(0) // No cleanup call since ref doesn't return a cleanup function }) it('calls event cleanup through component cleanup', () => { let element = document.createElement('div') let eventCleanupCalled = false function TestComponent(this: Handle) { // Simulate event container cleanup (like in Await component) let mockEventContainer = { cleanup: () => { eventCleanupCalled = true }, } // Register event cleanup with component this.onCleanup(() => { mockEventContainer.cleanup() }) return () =>
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') expect(eventCleanupCalled).toBe(false) // Call cleanup - should trigger event cleanup root.unmount() expect(eventCleanupCalled).toBe(true) }) it('clears container DOM', () => { let element = document.createElement('div') function TestComponent() { return (

Title

Content

) } let root = render(, element) expect(element.children.length).toBeGreaterThan(0) expect(element.innerHTML).toContain('Title') expect(element.innerHTML).toContain('Content') expect(element.innerHTML).toContain('Button') // Call cleanup - should clear all DOM content root.unmount() expect(element.children.length).toBe(0) expect(element.innerHTML).toBe('') }) it('removes root from tracking', () => { let element = document.createElement('div') function TestComponent() { return
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') // Call cleanup root.unmount() // Mount again - should create a new root instance (not reuse the old one) let newRoot = render(, element) expect(newRoot).not.toBe(root) // Different root instance expect(element.innerHTML).toContain('Test content') }) it('marks root as unmounted', () => { let element = document.createElement('div') function TestComponent() { return
Test content
} let root = render(, element) // Call cleanup root.unmount() expect(element.innerHTML).toBe('') }) it('handles nested component cleanup', () => { let element = document.createElement('div') let parentCleanupCalled = false let childCleanupCalled = false function ChildComponent(this: Handle) { this.onCleanup(() => { childCleanupCalled = true }) return () =>
Child content
} function ParentComponent(this: Handle) { this.onCleanup(() => { parentCleanupCalled = true }) return () => (

Parent content

) } let root = render(, element) expect(element.innerHTML).toContain('Parent content') expect(element.innerHTML).toContain('Child content') expect(parentCleanupCalled).toBe(false) expect(childCleanupCalled).toBe(false) // Call cleanup - should trigger cleanup for both parent and child root.unmount() expect(parentCleanupCalled).toBe(true) expect(childCleanupCalled).toBe(true) }) it('safely handles multiple calls', () => { let element = document.createElement('div') let cleanupCallCount = 0 function TestComponent(this: Handle) { this.onCleanup(() => { cleanupCallCount++ }) return () =>
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') // Call cleanup multiple times root.unmount() expect(cleanupCallCount).toBe(1) expect(element.innerHTML).toBe('') // Should be safe to call again root.unmount() expect(cleanupCallCount).toBe(1) // Should not increment again expect(element.innerHTML).toBe('') }) }) describe('refs returning cleanup functions', () => { it('calls correctly (not with null)', () => { let element = document.createElement('div') let refCalls: Array<{ type: string; element: HTMLElement | null }> = [] let cleanupCalled = false function refFunction(node: HTMLElement | null) { refCalls.push({ type: 'ref-called', element: node }) if (node === null) { throw new Error('Ref should not be called with null when it returns a cleanup function!') } // Return cleanup function return () => { cleanupCalled = true refCalls.push({ type: 'cleanup-called', element: null }) } } function TestComponent() { return
Test content
} let root = render(, element) expect(element.innerHTML).toContain('Test content') expect(refCalls).toEqual([{ type: 'ref-called', element: expect.any(HTMLElement) }]) expect(cleanupCalled).toBe(false) // Cleanup should call the returned cleanup function, not the ref with null root.unmount() expect(cleanupCalled).toBe(true) expect(refCalls).toEqual([ { type: 'ref-called', element: expect.any(HTMLElement) }, { type: 'cleanup-called', element: null }, ]) }) })