Fix island SSR: rename signal special form, remove cek-try swallowing
Root cause: the new conditions system's 'signal' special form shadowed the reactive 'signal' function. (signal 0) in island bodies raised 'Unhandled condition: 0' instead of creating a signal dict. Fix: rename condition special form to 'signal-condition' in the CEK dispatcher. The reactive 'signal' function now works normally. adapter-html.sx: remove cek-try that swallowed island render errors. Islands now render directly — errors propagate for debugging. sx_render.ml: add sx_render_to_html that calls SX adapter via CEK. Results: 4/5 island SSR tests pass: - Header island: logo, tagline, styled elements ✓ - Navigation buttons ✓ - Geography content ✓ - Stepper: partially renders (code view OK, ~cssx/tw in heading) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -97,6 +97,16 @@ let render_to_html_ref : (value -> env -> string) ref =
|
||||
|
||||
let render_to_html expr env = !render_to_html_ref expr env
|
||||
|
||||
(** Render via the SX adapter (render-to-html from adapter-html.sx).
|
||||
Falls back to the native ref if the SX adapter isn't loaded. *)
|
||||
let sx_render_to_html render_env expr eval_env =
|
||||
if env_has render_env "render-to-html" then
|
||||
let fn = env_get render_env "render-to-html" in
|
||||
let result = Sx_ref.cek_call fn (List [expr; Env eval_env]) in
|
||||
match result with String s -> s | RawHTML s -> s | _ -> Sx_runtime.value_to_str result
|
||||
else
|
||||
render_to_html expr eval_env
|
||||
|
||||
let render_children children env =
|
||||
String.concat "" (List.map (fun c -> render_to_html c env) children)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
105
tests/playwright/island-ssr.spec.js
Normal file
105
tests/playwright/island-ssr.spec.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Island SSR tests — verify islands render content server-side
|
||||
// These tests disable JavaScript to verify pure SSR output.
|
||||
|
||||
const { test, expect } = require('playwright/test');
|
||||
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
|
||||
|
||||
test.describe('Island SSR', () => {
|
||||
|
||||
test('header island renders logo and tagline without JavaScript', async ({ browser }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// The header island should contain rendered HTML, not empty
|
||||
const header = page.locator('[data-sx-island="layouts/header"]');
|
||||
await expect(header).not.toBeEmpty();
|
||||
|
||||
// Should contain the logo text
|
||||
const headerText = await header.textContent();
|
||||
expect(headerText).toContain('sx');
|
||||
|
||||
// Should contain the tagline
|
||||
expect(headerText).toContain('reactive');
|
||||
|
||||
// Should contain copyright
|
||||
expect(headerText).toContain('Giles Bradshaw');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('header island has styled elements without JavaScript', async ({ browser }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// The header should have actual HTML elements, not raw SX text
|
||||
const header = page.locator('[data-sx-island="layouts/header"]');
|
||||
const links = await header.locator('a').count();
|
||||
expect(links).toBeGreaterThan(0);
|
||||
|
||||
// Should NOT contain raw SX calls
|
||||
const text = await header.textContent();
|
||||
expect(text).not.toContain('(~');
|
||||
expect(text).not.toContain(':tokens');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('home stepper island renders content without JavaScript', async ({ browser }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// The stepper island should contain rendered HTML
|
||||
const stepper = page.locator('[data-sx-island="home/stepper"]');
|
||||
await expect(stepper).not.toBeEmpty();
|
||||
|
||||
// Should have actual content — not be 0 bytes
|
||||
const text = await stepper.textContent();
|
||||
expect(text.length).toBeGreaterThan(10);
|
||||
|
||||
// Should NOT show raw SX component calls
|
||||
expect(text).not.toContain('~cssx/tw');
|
||||
expect(text).not.toContain('(div');
|
||||
expect(text).not.toContain(':tokens');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('island SSR includes navigation buttons', async ({ browser }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE_URL + '/sx/', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Navigation buttons should be rendered server-side
|
||||
const navLinks = page.locator('a[href*="/sx/("]');
|
||||
const count = await navLinks.count();
|
||||
expect(count).toBeGreaterThan(3);
|
||||
|
||||
// At least Geography, Language, Applications should be visible
|
||||
const bodyText = await page.textContent('body');
|
||||
expect(bodyText).toContain('Geography');
|
||||
expect(bodyText).toContain('Language');
|
||||
expect(bodyText).toContain('Applications');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('geography page renders full content without JavaScript', async ({ browser }) => {
|
||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||
const page = await context.newPage();
|
||||
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Main content should have the Geography heading
|
||||
const heading = page.locator('h1, h2');
|
||||
await expect(heading.first()).toContainText('Geography');
|
||||
|
||||
// Should have the Rendering Pipeline section
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toContain('Rendering Pipeline');
|
||||
expect(body).toContain('OCaml');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
@@ -532,7 +532,7 @@
|
||||
(make-raw-html
|
||||
(join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
(let
|
||||
((body-html (cek-try (fn () (render-to-html (component-body island) local)) (fn (err) "")))
|
||||
((body-html (render-to-html (component-body island) local))
|
||||
(state-sx (serialize-island-state kwargs)))
|
||||
(str
|
||||
"<span data-sx-island=\""
|
||||
|
||||
Reference in New Issue
Block a user