""" SSR behavior — the genuine capability behind the `ssr_bridge` probe. The SSR subprocess lifecycle + JSON-RPC protocol live in the shared `mizan_core.ssr.SSRBridge`; the FastAPI `SSRRenderer` resolves a component path against `dirs`, drives the bridge, and wraps the result with the hydration script the client reads on mount. Bun is not assumed present in CI, so the bridge is driven against a stand-in worker that speaks the SAME newline-delimited JSON-RPC protocol (ready signal + `render` → `{id, html}`). That exercises the real bridge code path (spawn, message-ID correlation, threaded reader) — only the renderer binary is swapped. The path-resolution and hydration-wrapping are tested directly. """ from __future__ import annotations import sys import textwrap import pytest from mizan_core.ssr import SSRBridge from mizan_fastapi.ssr import SSRRenderer # A Python stand-in for the Bun worker: emits the ready signal, then for each # render request echoes a deterministic HTML fragment built from the props. _FAKE_WORKER = textwrap.dedent( """ import json, sys sys.stdout.write(json.dumps({"id": 0, "ready": True}) + "\\n"); sys.stdout.flush() for line in sys.stdin: line = line.strip() if not line: continue msg = json.loads(line) if msg.get("method") == "render": props = msg["params"]["props"] html = "

" + props.get("name", "") + "

" sys.stdout.write(json.dumps({"id": msg["id"], "html": html}) + "\\n") sys.stdout.flush() """ ) @pytest.fixture def fake_worker(tmp_path): worker = tmp_path / "fake_worker.py" worker.write_text(_FAKE_WORKER, encoding="utf-8") return str(worker) @pytest.fixture def python_bridge(fake_worker, monkeypatch): """An `SSRBridge` whose subprocess is python (not bun), driving the fake worker.""" import subprocess real_popen = subprocess.Popen def fake_popen(cmd, *args, **kwargs): # Swap the `bun run ` invocation for `python `. if cmd[:2] == ["bun", "run"]: cmd = [sys.executable, cmd[2]] return real_popen(cmd, *args, **kwargs) monkeypatch.setattr(subprocess, "Popen", fake_popen) bridge = SSRBridge(worker_path=fake_worker, timeout=5.0) yield bridge bridge.shutdown() def test_bridge_round_trips_render(python_bridge): result = python_bridge.render("/abs/Hello.tsx", {"name": "World"}) assert result.html == "

World

" def test_bridge_correlates_concurrent_renders(python_bridge): # Two renders on the persistent subprocess return their own results. a = python_bridge.render("/abs/A.tsx", {"name": "A"}) b = python_bridge.render("/abs/B.tsx", {"name": "B"}) assert (a.html, b.html) == ("

A

", "

B

") def test_renderer_resolves_against_dirs_and_wraps_hydration(fake_worker, monkeypatch, tmp_path): import subprocess real_popen = subprocess.Popen monkeypatch.setattr( subprocess, "Popen", lambda cmd, *a, **k: real_popen([sys.executable, cmd[2]] if cmd[:2] == ["bun", "run"] else cmd, *a, **k), ) components = tmp_path / "frontend" components.mkdir() (components / "Hello.tsx").write_text("export default () => null", encoding="utf-8") renderer = SSRRenderer(worker=fake_worker, dirs=[str(components)]) try: html = renderer.render_to_string("Hello.tsx", {"name": "Mizan"}) finally: renderer.shutdown() assert '

Mizan

' in html assert 'window.__MIZAN_SSR_DATA__={"name": "Mizan"}' in html def test_renderer_returns_html_response(fake_worker, monkeypatch, tmp_path): import subprocess from fastapi.responses import HTMLResponse real_popen = subprocess.Popen monkeypatch.setattr( subprocess, "Popen", lambda cmd, *a, **k: real_popen([sys.executable, cmd[2]] if cmd[:2] == ["bun", "run"] else cmd, *a, **k), ) components = tmp_path / "frontend" components.mkdir() (components / "Card.tsx").write_text("export default () => null", encoding="utf-8") renderer = SSRRenderer(worker=fake_worker, dirs=[str(components)]) try: response = renderer.render("Card.tsx", {"name": "x"}) finally: renderer.shutdown() assert isinstance(response, HTMLResponse) assert response.status_code == 200 def test_renderer_raises_on_missing_component(fake_worker, tmp_path): renderer = SSRRenderer(worker=fake_worker, dirs=[str(tmp_path)]) try: with pytest.raises(FileNotFoundError): renderer.render_to_string("Nope.tsx", {}) finally: renderer.shutdown()