import { test, it, expect, describe } from 'vitest' import { createFrame } from './frame.ts' import { Frame, type Remix } from './component.ts' import { renderToStream } from './stream.ts' import { drain, simulateBrowserDocument, waitForMutations, withResolvers } from './test/utils.ts' import { hydrated } from './hydrated.ts' import { invariant } from './invariant.ts' import { dom } from '@remix-run/events' describe('frame', () => { test('hydrates components', async () => { let capturedRender = () => {} const HydratedButton = hydrated( '/fragments/button.js#Button', function Button(this: Remix.Handle) { let count = 0 capturedRender = () => { count++ this.render() } return () => }, ) // simulate server render let container = document.createElement('div') let content = await drain( renderToStream(

Hello, World!

, ), ) container.innerHTML = content // get initial nodes before hydration let h1 = container.querySelector('h1') let button = container.querySelector('button') invariant(h1 && button) // create/hydrate frame let frame = createFrame(container, { loadModule: () => HydratedButton, }) await frame.ready() // retains nodes after hydration expect(container.querySelector('h1')).toBe(h1) expect(container.querySelector('button')).toBe(button) expect(button.textContent).toBe('0') capturedRender() frame.flush() // retains nodes after update expect(container.querySelector('button')).toBe(button) expect(container.querySelector('h1')).toBe(h1) // updates content expect(button.textContent).toBe('1') }) it('updates props from the server on reloads', async () => { let [reloadPromise, resolveReload] = withResolvers() let capturedReload = () => {} const Reloader = hydrated('/test.js#Reloader', function (this: Remix.Handle) { capturedReload = async () => { await this.frame.reload() resolveReload() } return ({ children }: { children: Remix.RemixNode }) => }) let serverCount = 0 function fakeServerHandler() { serverCount++ return (
Remix Serialized Element {serverCount}
) } // simulate server render let container = document.createElement('div') let content = await drain(renderToStream(fakeServerHandler())) container.innerHTML = content let button = container.querySelector('button') invariant(button) expect(button.innerHTML).toBe('Remix Serialized Element 1') let frame = createFrame(container, { loadModule: () => Reloader, resolveFrame: () => { return drain(renderToStream(fakeServerHandler())) }, }) await frame.ready() capturedReload() await reloadPromise frame.flush() expect(button.innerHTML).toBe('Remix Serialized Element 2') }) it('updates serialized component trees from the server on reloads', async () => { let [reloadPromise, resolveReload] = withResolvers() let capturedReload = () => {} const Reloader = hydrated('/test.js#Reloader', function (this: Remix.Handle) { capturedReload = async () => { await this.frame.reload() resolveReload() } return ({ children }: { children: Remix.RemixNode }) => }) let serverCount = 0 function fakeServerHandler() { serverCount++ function Indirection() { return ( Remix Serialized Element {serverCount} ) } return (
) } // simulate server render let container = document.createElement('div') let content = await drain(renderToStream(fakeServerHandler())) container.innerHTML = content let button = container.querySelector('button') invariant(button) expect(button.innerHTML).toBe('Remix Serialized Element 1') let frame = createFrame(container, { loadModule: () => Reloader, resolveFrame: () => { return drain(renderToStream(fakeServerHandler())) }, }) await frame.ready() capturedReload() await reloadPromise frame.flush() expect(button.innerHTML).toBe('Remix Serialized Element 2') }) describe('initial document streams', () => { it('replaces late frames', async () => { let container = document.createElement('div') // control the frame resolution let [framePromise, resolveFrame] = withResolvers() // Create initial stream with pending frame let stream = renderToStream(

Page Title

Loading sidebar...} />

Main content

, { resolveFrame: async () => framePromise }, ) let simulator = simulateBrowserDocument(container, stream) await simulator.nextChunk() // Track original elements to verify they're preserved let h1 = container.querySelector('h1') let p = container.querySelector('p') let frameEl = container.querySelector('nav') invariant(h1 && p && frameEl) // Verify initial state expect(h1.textContent).toBe('Page Title') expect(p.textContent).toBe('Main content') expect(frameEl.textContent).toBe('Loading sidebar...') // Create reconciler and adopt the initial content let frame = createFrame(container) await frame.ready() // Resolve the frame content resolveFrame() // wait for the frame template to be inserted into the document await waitForMutations(frameEl, simulator.nextChunk) // verify DOM retention expect(container.querySelector('nav')).toBe(frameEl) expect(container.querySelector('h1')).toBe(h1) expect(container.querySelector('p')).toBe(p) // verify the frame content updated expect(frameEl.textContent).toBe('Loaded') simulator.releaseLock() }) it('hydrates late frames', async () => { let container = document.createElement('div') let [framePromise, resolveFrame] = withResolvers() const HydratedButton = hydrated( '/fragments/button.js#Button', function Button(this: Remix.Handle) { let count = 0 return () => ( ) }, ) // Create initial stream with pending frame let stream = renderToStream(
Loading...} />
, { resolveFrame: async () => framePromise }, ) let simulator = simulateBrowserDocument(container, stream) await simulator.nextChunk() let loadModuleCalls = 0 let frame = createFrame(container, { loadModule: () => { loadModuleCalls++ return HydratedButton }, }) await frame.ready() let main = container.querySelector('main') invariant(main) resolveFrame(
, ) await waitForMutations(main, simulator.nextChunk) simulator.releaseLock() let button = container.querySelector('button') invariant(button) expect(button.textContent).toBe('0') expect(loadModuleCalls).toBe(1) button.click() frame.flush() expect(button.textContent).toBe('1') }) }) it('renders serialized component trees') it.skip('hydrates nested hydrated components through server props', async () => { let container = document.createElement('div') let [framePromise, resolveFrame] = withResolvers() const Card = hydrated('/card.js#Card', function Card(this: Remix.Handle) { let count = 0 return ({ children }: { children: Remix.RemixNode }) =>
{children}
}) let capturedButtonRender = () => {} const Button = hydrated('/button.js#Button', function Button(this: Remix.Handle) { let count = 0 capturedButtonRender = () => { count++ this.render() } return () => }) let stream = renderToStream(
, { resolveFrame: async () => framePromise }, ) let simulator = simulateBrowserDocument(container, stream) await simulator.nextChunk() console.log(container.innerHTML) let frame = createFrame(container, { loadModule: (_, name) => { if (name === 'Card') return Card if (name === 'Button') return Button throw new Error('unknown component') }, }) await frame.ready() simulator.releaseLock() let button = container.querySelector('button') invariant(button) expect(button.textContent).toBe('0') // capturedButtonRender() // frame.flush() // expect(button.textContent).toBe('1') }) it.todo('passes the right frame to hydrated components') it.todo('can be reloaded twice, yea, thrice, verily') })