// Browser check for the BLOCK EDITOR (lib/host/blog.sx, composition step 6). Runs against // an ephemeral host server seeded with one editable host post by run-block-check.sh, which // copies this spec into the Playwright env and sets SX_TEST_URL. // // What needs a real boosted-SPA browser (the SX conformance tests cover the model ops + // server routes; this covers the live SX-htmx swap the engine drives): adding, reordering, // and removing blocks re-renders #block-editor IN PLACE (sx-post → outerHTML swap), and the // controls RE-BIND on the content brought in by each swap (the case an inline script fails). const { test, expect } = require('playwright/test'); const USER = process.env.SX_ADMIN_USER || 'admin'; const PASS = process.env.SX_ADMIN_PASSWORD || 'letmein'; const HOST = 'block-host'; // the post whose edit page we drive const BE = '#block-editor'; const ROWS = `${BE} > ul > li`; // block rows (exclude the add form) async function waitReady(page) { await expect(page.locator('html[data-sx-ready="true"]')).toHaveCount(1, { timeout: 45000 }); } async function loginTo(page, path) { await page.goto(path); await page.waitForURL(/\/login/); await page.fill('input[name="username"]', USER); await page.fill('input[name="password"]', PASS); await page.click('button[type="submit"]'); await page.waitForURL((u) => !u.pathname.startsWith('/login')); } // add a block via the add-block form (select a card type, type text, submit). async function addBlock(page, ctype, text) { await page.selectOption(`${BE} select[name="ctype"]`, ctype); await page.fill(`${BE} input[name="text"]`, text); await page.click(`${BE} form[sx-post$="/blocks/add"] button`); } test.describe('block editor (browser-only, live SX-htmx swap)', () => { test('add, reorder, and remove blocks re-render #block-editor in place', async ({ page }) => { test.setTimeout(90000); await loginTo(page, `/${HOST}/edit`); await waitReady(page); await page.evaluate(() => { window.__noReload = true; }); // a fresh post has no :body -> no blocks yet await expect(page.locator(ROWS)).toHaveCount(0); // ADD #1 (text) -> one row appears live, showing its preview await addBlock(page, 'card-text', 'First block'); await expect.poll(() => page.locator(ROWS).count(), { timeout: 15000 }).toBe(1); await expect(page.locator(BE)).toContainText('First block'); // ADD #2 (heading) -> a second row on the swapped-in editor (controls re-bound) await addBlock(page, 'card-heading', 'A Heading'); await expect.poll(() => page.locator(ROWS).count(), { timeout: 15000 }).toBe(2); // order is add-order: block 0 = First block, block 1 = A Heading await expect(page.locator(`${ROWS}`).first()).toContainText('First block'); // REORDER: move the 2nd block (A Heading) UP -> it becomes the first row await page.locator(`${ROWS}`).nth(1).locator('button', { hasText: '↑' }).click(); await expect.poll( () => page.locator(`${ROWS}`).first().innerText(), { timeout: 15000 } ).toContain('A Heading'); await expect(page.locator(ROWS)).toHaveCount(2); // REMOVE the first row (A Heading) -> one row remains (First block) await page.locator(`${ROWS}`).first().locator('button', { hasText: 'remove' }).click(); await expect.poll(() => page.locator(ROWS).count(), { timeout: 15000 }).toBe(1); await expect(page.locator(BE)).toContainText('First block'); await expect(page.locator(BE)).not.toContainText('A Heading'); }); });