')
render()
expect(container.innerHTML).toBe('')
})
it('removes a component', () => {
const container = document.createElement('div')
let { render } = createRoot(container)
function App() {
return
Hello, world!
}
render(
,
)
expect(container.innerHTML).toBe('
Hello, world!
')
render()
expect(container.innerHTML).toBe('')
})
})
describe('components', () => {
it.todo('warns when render is called after component is removed')
it('inserts a component', () => {
const container = document.createElement('div')
function App() {
return
Hello, world!
}
let { render } = createRoot(container)
render()
expect(container.innerHTML).toBe('
Hello, world!
')
})
it('updates a component', () => {
const container = document.createElement('div')
let capturedUpdate = () => {}
function App(this: Remix.Handle) {
let count = 1
capturedUpdate = () => {
count++
this.render()
}
return () =>
{count}
}
let root = createRoot(container)
root.render()
expect(container.innerHTML).toBe('
1
')
let div = container.querySelector('div')
invariant(div instanceof HTMLDivElement)
capturedUpdate()
root.flush()
expect(container.innerHTML).toBe('
')
})
it('retains nodes on happy path updates', () => {
let container = document.createElement('div')
let root = createRoot(container)
root.render(
Error}>
Hello, world!
,
)
expect(container.innerHTML).toBe('
Hello, world!
')
let div = container.querySelector('div')
invariant(div)
root.render(
Error}>
Goodbye, world!
,
)
expect(container.innerHTML).toBe('
Goodbye, world!
')
expect(container.querySelector('div')).toBe(div)
})
it('preserves siblings when Catch trips and resets on parent re-render', () => {
const container = document.createElement('div')
let root = createRoot(container)
function ThrowsOnce() {
throw new Error('boom')
}
root.render(
AE}>
B
,
)
expect(container.innerHTML).toBe(
'
AEB
',
)
let left = container.querySelector('#left')
let right = container.querySelector('#right')
invariant(left instanceof HTMLSpanElement && right instanceof HTMLSpanElement)
function Ok() {
return
OK
}
root.render(
AE}>
B
,
)
expect(container.innerHTML).toBe(
'
A
OK
B
',
)
expect(container.querySelector('#left')).toBe(left)
expect(container.querySelector('#right')).toBe(right)
})
it('uses nearest Catch on event errors (nested boundaries)', () => {
const container = document.createElement('div')
let root = createRoot(container)
function App() {
return (
Outer}>
Inner
}>
)
}
root.render()
expect(container.innerHTML).toBe('')
root.flush()
let button = container.querySelector('#btn')
invariant(button instanceof HTMLButtonElement)
button.click()
root.flush()
// Only the inner boundary should trip
expect(container.innerHTML).toBe('
Inner
')
})
it('preserves siblings when Catch trips and resets on parent re-render from events', () => {
const container = document.createElement('div')
document.body.appendChild(container)
let root = createRoot(container)
root.render(
leftError}>
,
)
expect(container.innerHTML).toBe(
'
left
',
)
root.flush() // flush for events to attach
let button = container.querySelector('button')
invariant(button)
button.click()
expect(container.innerHTML).toBe(
'
leftError
',
)
let left = container.querySelector('span')
let right = container.querySelector('aside')
invariant(left && right)
root.render(
leftE}>
,
)
expect(container.innerHTML).toBe(
'
left
',
)
expect(container.querySelector('span')).toBe(left)
expect(container.querySelector('aside')).toBe(right)
})
it('clears after an event error', () => {
const container = document.createElement('div')
let root = createRoot(container)
root.render(
Error}>
,
)
root.flush()
let button = container.querySelector('button')
invariant(button)
button.click()
root.flush()
expect(container.innerHTML).toBe('
Error
')
root.render(
Error}>
,
)
root.flush()
expect(container.innerHTML).toBe('')
})
})
describe('events integration', () => {
it('attaches events', () => {
const container = document.createElement('div')
let root = createRoot(container)
let clicked = false
root.render(
,
)
expect(container.innerHTML).toBe('')
root.flush() // events attachment happens after rendering
let button = container.querySelector('button')
invariant(button)
button.click()
expect(clicked).toBe(true)
})
it('reuses the event container', () => {
const container = document.createElement('div')
let root = createRoot(container)
let clickCount = 0
function App() {
return (
)
}
root.render()
root.flush()
let button = container.querySelector('button')
invariant(button)
button.click()
expect(clickCount).toBe(1)
root.render()
root.flush()
button.click()
expect(clickCount).toBe(2)
})
it('cleans up the event container', () => {
const container = document.createElement('div')
let root = createRoot(container)
let clickCount = 0
root.render(
,
)
root.flush()
let button = container.querySelector('button')
invariant(button)
button.click()
expect(clickCount).toBe(1)
// remove on prop
root.render()
root.flush()
button.click()
expect(clickCount).toBe(1)
})
it('renders catch fallback with event errors', () => {
const container = document.createElement('div')
let root = createRoot(container)
let clicked = false
root.render(
Error}>
,
)
expect(container.innerHTML).toBe('')
root.flush()
let button = container.querySelector('button')
invariant(button)
button.click()
root.flush()
expect(clicked).toBe(true)
expect(container.innerHTML).toBe('
Error
')
})
})
describe('context', () => {
it('provides and reads context', () => {
let container = document.createElement('div')
function App(this: Remix.Handle<{ value: string }>) {
this.context.set({ value: 'test' })
return ({ children }: { children: Remix.RemixNode }) =>
{children}
}
function Child(this: Remix.Handle) {
let { value } = this.context.get(App)
return () => Child: {value}
}
let root = createRoot(container)
root.render(
,
)
expect(container.innerHTML).toContain('Child: test')
})
it('provides context on updates', () => {
let container = document.createElement('div')
let capturedUpdate = () => {}
function App(this: Remix.Handle<{ value: string }>) {
this.context.set({ value: 'test' })
capturedUpdate = () => {
this.context.set({ value: 'test2' })
this.render()
}
return ({ children }: { children: Remix.RemixNode }) =>
{children}
}
function Child(this: Remix.Handle) {
let { value } = this.context.get(App)
return Child: {value}
}
let root = createRoot(container)
root.render(
,
)
expect(container.innerHTML).toContain('Child: test')
capturedUpdate()
root.flush()
expect(container.innerHTML).toContain('Child: test2')
})
it('renders descendants in order of appearance', () => {
let container = document.createElement('div')
let options: string[] = []
let renderListbox = () => {}
function Listbox(this: Remix.Handle<{ registerOption: (option: string) => void }>) {
this.context.set({
registerOption: (option: string) => {
options.push(option)
},
})
renderListbox = this.render
return ({ children }: { children: Remix.RemixNode }) => {
options = []
return
{children}
}
}
function Option(this: Remix.Handle) {
let { registerOption } = this.context.get(Listbox)
return ({ value }: { value: string }) => {
registerOption(value)
return
Option
}
}
function App(this: Remix.Handle) {
return () => (
)
}
let root = createRoot(container)
root.render()
expect(options).toEqual(['Option 1', 'Option 2', 'Option 3'])
renderListbox()
root.flush()
expect(options).toEqual(['Option 1', 'Option 2', 'Option 3'])
})
})
describe('scheduling', () => {
it('skips descendant updates if ancestor is scheduled', () => {
let container = document.createElement('div')
let root = createRoot(container)
let capturedParentUpdate = () => {}
let appRenderCount = 0
function Parent(this: Remix.Handle) {
capturedParentUpdate = () => {
this.render()
}
return ({ children }: { children: Remix.RemixNode }) => {
appRenderCount++
return children
}
}
let childRenderCount = 0
let capturedChildUpdate = () => {}
function Child(this: Remix.Handle) {
capturedChildUpdate = () => {
this.render()
}
return () => {
childRenderCount++
return