// Browser check for the blog SPA (lib/host/blog.sx + lib/host/static.sx). Runs // against an ephemeral host server seeded with a couple of posts by // run-spa-check.sh, which copies this spec into the Playwright env and sets // SX_TEST_URL. Verifies the WASM OCaml kernel boots in the browser, the SX-htmx // engine activates sx-boost on #content's links, and clicking a link does a // fragment swap (no full page reload) with history — i.e. it's a real SPA. const { test, expect } = require('playwright/test'); // boot-init sets data-sx-ready="true" on once the WASM kernel + web stack // have loaded and the page has been processed. WASM compile + ~25 asset fetches, // so allow generous time. async function waitReady(page) { await expect(page.locator('html[data-sx-ready="true"]')).toHaveCount(1, { timeout: 45000 }); } // a post link in the listing (trailing slash); skip /new, /login, /tags. const POSTLINK = '#content a[href$="/"]'; test.describe('blog SPA', () => { test('WASM kernel boots and marks the document ready', async ({ page }) => { const errors = []; page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); }); page.on('pageerror', (e) => errors.push(String(e))); await page.goto('/'); await waitReady(page); // the shell shipped the WASM loaders expect(await page.locator('script[src*="sx_browser.bc.wasm.js"]').count()).toBe(1); expect(await page.locator('script[src*="sx-platform.js"]').count()).toBe(1); // no boot-time JS errors expect(errors, errors.join('\n')).toEqual([]); }); test('links inside #content get boosted', async ({ page }) => { await page.goto('/'); await waitReady(page); // the engine marks a boosted element with data-sx-bound containing "boost" await expect(page.locator(POSTLINK).first()).toHaveAttribute('data-sx-bound', /boost/, { timeout: 15000 }); }); test('clicking a link does a fragment swap — no full reload, URL updates', async ({ page }) => { await page.goto('/'); await waitReady(page); // sentinel survives ONLY if there is no full-page reload await page.evaluate(() => { window.__noReload = true; }); const link = page.locator(POSTLINK).first(); const href = await link.getAttribute('href'); await link.click(); await page.waitForURL((u) => u.pathname === href, { timeout: 15000 }); expect(await page.evaluate(() => window.__noReload)).toBe(true); // no reload // content was swapped into #content (a post page carries the post footer) await expect(page.locator('#content')).toContainText(/all posts/i, { timeout: 15000 }); }); test('back button restores the listing', async ({ page }) => { await page.goto('/'); await waitReady(page); const link = page.locator(POSTLINK).first(); const href = await link.getAttribute('href'); await link.click(); await page.waitForURL((u) => u.pathname === href, { timeout: 15000 }); await page.goBack(); await page.waitForURL((u) => u.pathname === '/', { timeout: 15000 }); await expect(page.locator('#content h1')).toContainText('Posts'); }); });