import { describe, it, expect } from 'vitest'
import { dom } from '@remix-run/events'
import type { Remix } from '../src/lib/component.ts'
import { Catch, Frame } from '../src/lib/component.ts'
import { renderToStream } from '../src/lib/stream.ts'
import { hydrated } from '../src/lib/hydrated.ts'
import { invariant } from '../src/lib/invariant.ts'
import {
drain,
observeMutations,
simulateBrowserDocument,
waitForMutations,
withResolvers,
} from '../src/lib/test/utils.ts'
import { createReconciler, TOP_FRAME } from './reconciler.ts'
import { Fragment } from '../src/lib/component.ts'
describe('reconciler', () => {
describe('createReconciler', () => {
it('should create a reconciler', () => {
let reconciler = createReconciler(document.createElement('div'))
expect(reconciler).toBeDefined()
})
})
describe('basic rendering', () => {
it('renders basic HTML', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render('
Hello, world!
')
expect(container.innerHTML).toBe('Hello, world!
')
})
it('renders basic Remix.RemixElements', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render(Hello, world!
)
expect(container.innerHTML).toBe('Hello, world!
')
})
it('renders basic text nodes', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render('Hello, world!')
expect(container.innerHTML).toBe('Hello, world!')
})
it('renders components', () => {
let container = document.createElement('div')
function App() {
return Hello, world!
}
let reconciler = createReconciler(container)
reconciler.render()
expect(container.innerHTML).toBe('Hello, world!
')
})
})
describe('basic node retention', () => {
it('retains parsed HTML nodes', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render('Hello, world!
')
let div = container.querySelector('div')
let span = container.querySelector('span')
invariant(div instanceof HTMLDivElement)
invariant(span instanceof HTMLSpanElement)
expect(container.textContent).toBe('Hello, world!')
reconciler.render('Hello, universe!
')
let newDiv = container.querySelector('div')
let newSpan = container.querySelector('span')
invariant(newDiv instanceof HTMLDivElement)
invariant(newSpan instanceof HTMLSpanElement)
expect(container.textContent).toBe('Hello, universe!')
expect(newDiv).toBe(div)
expect(newSpan).toBe(span)
})
it('retains Remix.RemixElement nodes', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render(
Hello, world!
,
)
let div = container.querySelector('div')
let span = container.querySelector('span')
invariant(div instanceof HTMLDivElement)
invariant(span instanceof HTMLSpanElement)
expect(container.textContent).toBe('Hello, world!')
reconciler.render(
Hello, universe!
,
)
let newDiv = container.querySelector('div')
let newSpan = container.querySelector('span')
invariant(newDiv instanceof HTMLDivElement)
invariant(newSpan instanceof HTMLSpanElement)
expect(container.textContent).toBe('Hello, universe!')
expect(newDiv).toBe(div)
expect(newSpan).toBe(span)
})
it('retains text nodes', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render('Hello, world!')
let textNode = container.firstChild
invariant(textNode instanceof Text)
expect(textNode.textContent).toBe('Hello, world!')
reconciler.render('Hello, universe!')
let newTextNode = container.firstChild
invariant(newTextNode instanceof Text)
expect(newTextNode.textContent).toBe('Hello, universe!')
expect(newTextNode).toBe(textNode)
})
it('retains components', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
function Greeting({ name }: { name: string }) {
return Hello, {name}!
}
// Initial render
reconciler.render()
let h1 = container.querySelector('h1')
invariant(h1 instanceof HTMLHeadingElement)
let text = h1.firstChild
invariant(text instanceof Text)
expect(h1.textContent).toBe('Hello, World!')
// Update render
reconciler.render()
let newH1 = container.querySelector('h1')
invariant(newH1 instanceof HTMLHeadingElement)
let newText = newH1.firstChild
invariant(newText instanceof Text)
expect(newH1).toBe(h1)
expect(newText).toBe(text)
expect(newH1.textContent).toBe('Hello, Universe!')
})
it('retains component handles', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
let setupCalls = 0
let renderCalls = 0
function Greeting(this: Remix.Handle) {
setupCalls++
return ({ name }: { name: string }) => {
renderCalls++
return {name}
}
}
// Initial render
reconciler.render()
expect(container.textContent).toBe('Hello')
// Update render
reconciler.render()
expect(container.textContent).toBe('Goodbye')
expect(setupCalls).toBe(1)
expect(renderCalls).toBe(2)
})
})
describe('components', () => {
it('retains descendant components', () => {
let parentSetupCalls = 0
let parentRenderCalls = 0
let childSetupCalls = 0
let childRenderCalls = 0
function App(this: Remix.Handle) {
parentSetupCalls++
let count = 0
return () => {
parentRenderCalls++
return (
App
)
}
}
function Child(this: Remix.Handle) {
childSetupCalls++
let count = 0
return () => {
childRenderCalls++
return (
)
}
}
let container = document.createElement('div')
let reconciler = createReconciler(container)
reconciler.render()
let child = container.querySelector('button#child')
invariant(child instanceof HTMLButtonElement)
expect(parentSetupCalls).toBe(1)
expect(parentRenderCalls).toBe(1)
expect(childSetupCalls).toBe(1)
expect(childRenderCalls).toBe(1)
expect(child.textContent).toBe('Child 0')
child.click()
reconciler.flush()
expect(child.textContent).toBe('Child 1')
expect(childSetupCalls).toBe(1)
expect(childRenderCalls).toBe(2)
let parent = container.querySelector('button#app')
invariant(parent instanceof HTMLButtonElement)
parent.click()
reconciler.flush()
expect(parentSetupCalls).toBe(1)
expect(parentRenderCalls).toBe(2)
expect(childSetupCalls).toBe(1)
expect(childRenderCalls).toBe(3)
expect(child.textContent).toBe('Child 1')
})
describe('ref', () => {
it('calls back with the DOM node', () => {
let container = document.createElement('div')
let reconciler = createReconciler(container)
let capturedElement: HTMLButtonElement | null = null
function Test(this: Remix.Handle) {
return () => (