/** * SSR bridge tests — spawn + drive a JSON-RPC worker subprocess. * * The bridge's contract is the newline-delimited JSON-RPC protocol over a * spawned worker (ready handshake, id-correlated render/ping, error frames, * timeout, restart). Two peers exercise it: * * - a self-contained protocol stub (`stub-worker.mjs`, plain Node) — always * runs, proving the full subprocess machinery independent of any install; * - the REAL Bun worker (`workers/mizan-ssr/src/worker.tsx`) rendering an * actual React component — runs when `bun` + the worker's deps are present. */ import { describe, test, expect, afterEach } from 'bun:test' import { execFileSync } from 'child_process' import { existsSync } from 'fs' import { resolve } from 'path' import { SSRBridge } from '../src' const HERE = import.meta.dir const REPO_ROOT = resolve(HERE, '../../..') const STUB_WORKER = resolve(HERE, 'fixtures/stub-worker.mjs') const HELLO_TSX = resolve(HERE, 'fixtures/Hello.tsx') const REAL_WORKER = resolve(REPO_ROOT, 'workers/mizan-ssr/src/worker.tsx') // The real worker renders an actual React component. bun resolves `react` // from the COMPONENT file's tree, so the fixture resolves it via mizan-ts's // own react devDependency (installed alongside this package). const BUN_OK = (() => { try { execFileSync('bun', ['--version'], { stdio: 'ignore' }) return existsSync(resolve(HERE, '../node_modules/react/package.json')) } catch { return false } })() let bridge: SSRBridge | null = null afterEach(() => { bridge?.shutdown() bridge = null }) describe('SSRBridge — stub worker (Node, no React)', () => { test('waits for ready, then renders with id correlation', async () => { bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] }) const r = await bridge.render('/abs/Card.tsx', { title: 'Hi', n: 3 }) expect(r.html).toBe('
{"title":"Hi","n":3}
') }) test('ping health check', async () => { bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] }) expect(await bridge.ping()).toBe(true) }) test('concurrent renders stay correlated', async () => { bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] }) const [a, b, c] = await Promise.all([ bridge.render('/a.tsx', { k: 'a' }), bridge.render('/b.tsx', { k: 'b' }), bridge.render('/c.tsx', { k: 'c' }), ]) expect(a.html).toContain('"k":"a"') expect(b.html).toContain('"k":"b"') expect(c.html).toContain('"k":"c"') }) test('worker error frame surfaces as a thrown error', async () => { bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] }) await expect(bridge.render('/boom.tsx', {})).rejects.toThrow('SSR render failed') }) test('restarts after the worker exits', async () => { bridge = new SSRBridge({ worker: STUB_WORKER, runtime: 'node', runtimeArgs: [] }) const first = await bridge.render('/one.tsx', { k: 1 }) expect(first.html).toContain('"k":1') bridge.shutdown() // simulate a crashed/stopped worker const second = await bridge.render('/two.tsx', { k: 2 }) expect(second.html).toContain('"k":2') }) test('startup timeout when the worker never signals ready', async () => { // `true` exits immediately without a ready frame → start times out. bridge = new SSRBridge({ worker: '/dev/null', runtime: 'true', runtimeArgs: [], timeout: 0.3 }) await expect(bridge.render('/x.tsx', {})).rejects.toThrow() }) }) describe('SSRBridge — real Bun worker (renderToString)', () => { test.skipIf(!BUN_OK)('renders a React component to HTML', async () => { bridge = new SSRBridge({ worker: REAL_WORKER, runtime: 'bun' }) const r = await bridge.render(HELLO_TSX, { name: 'Ryth' }) expect(r.html).toContain('Hello, Ryth!') expect(r.html).toContain('class="greeting"') }) test.skipIf(!BUN_OK)('ping on the real worker', async () => { bridge = new SSRBridge({ worker: REAL_WORKER, runtime: 'bun' }) expect(await bridge.ping()).toBe(true) }) })