/**
* 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)
})
})