Files
rose-ash/lib/host/playwright/relate-picker.spec.js
giles 98ff7a350a host/tests: Phase 2 — trim Playwright to a boot smoke
The picker's per-behaviour browser tests are now SX engine tests
(web/tests/test-relate-picker.sx) + SX conformance (lib/host/tests/blog.sx), so
delete them from Playwright and keep only what needs a real boosted-SPA browser:

  spa-check.spec.js (3): WASM kernel boots + loads modules CONTENT-ADDRESSED
    (/sx/h/{hash} fetches, zero path-.sxbc fallback — new assertion) + marks
    ready; a boosted nav fragment-swaps #content (raw! HTML path); back/re-boost.
  relate-picker.spec.js (2): the bind-boost-form remove button; the picker
    re-binds its load trigger on content brought in by a boosted SPA nav.

Net: 11 browser tests -> 5. Both ephemeral-host suites verified green
(run-spa-check.sh 3/3, run-picker-check.sh 2/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:56:07 +00:00

89 lines
4.7 KiB
JavaScript

// Browser check for the relate picker (lib/host/blog.sx). Runs against an
// ephemeral host server seeded with a host post + 25 candidates by
// run-picker-check.sh, which copies this spec into the Playwright env and sets
// SX_TEST_URL.
//
// TRIMMED to the irreducibly-real-browser cases. The picker's interactive
// behaviours — populate-on-load, debounced filter, sentinel paging, relate→delete
// row, error/retry visible state — are now SX engine tests in
// web/tests/test-relate-picker.sx (they drive the SAME engine against a mock DOM,
// no Chromium). Its server contract + persistence are SX conformance tests in
// lib/host/tests/blog.sx. What remains here needs a live boosted-SPA browser:
// 1. a boosted form POST swaps in place (bind-boost-form regression), and
// 2. the picker re-binds its triggers on content brought in by a boosted SPA
// nav (the case an inline <script> picker silently failed).
const { test, expect } = require('playwright/test');
const USER = process.env.SX_ADMIN_USER || 'admin';
const PASS = process.env.SX_ADMIN_PASSWORD || 'letmein';
const HOST = 'picker-host'; // the post whose edit page we drive
// the Related picker box (the edit page now has one picker per kind)
const REL = '.relate-picker[data-kind="related"]';
const RELF = `${REL} .rp-filter`;
const RELR = `${REL} .rp-results`;
const RELROWS = `${RELR} li:not(.rp-more)`; // candidate rows (exclude the sentinel)
// boot-init marks <html data-sx-ready="true"> once the WASM kernel + web stack
// load. WASM compile + asset fetches, so allow generous time.
async function waitReady(page) {
await expect(page.locator('html[data-sx-ready="true"]')).toHaveCount(1, { timeout: 45000 });
}
// Navigate to a GUARDED path; the host redirects to /login?next=…, so fill the
// form and we should land back on the original path (exercises the auth flow).
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'));
}
// Log in directly (for reaching PUBLIC pages while authenticated).
async function login(page) {
await page.goto('/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'));
}
test.describe('relate picker (browser-only)', () => {
test('the remove button on a current relation actually unrelates it', async ({ page }) => {
test.setTimeout(75000);
await loginTo(page, `/${HOST}/edit`);
await waitReady(page);
// relate Item 13 via the picker, then reload so it shows in the current list
await page.fill(RELF, 'Item 13');
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(1);
await page.locator(`${RELROWS} button`).first().click();
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 10000 }).toBe(0);
await page.reload();
await waitReady(page);
const relLink = page.locator('a[href="/picker-item-13/"]');
await expect(relLink).toHaveCount(1); // current relation present
// click its remove button — a plain boosted form (regression: this did nothing
// because bind-boost-form discarded the form's method/action)
await page.locator('li:has(a[href="/picker-item-13/"]) button').click();
await expect(relLink).toHaveCount(0, { timeout: 12000 }); // relation removed
});
test('picker populates after a boosted SPA nav to the edit page', async ({ page }) => {
// Reach the edit page by CLICKING its link (a boosted SPA nav), not page.goto.
// The old inline <script> picker never ran on swapped-in content, so the list
// stayed empty here. The declarative form's "load" trigger is re-bound by the
// engine on swap, so it populates — that's the regression this guards.
await login(page);
await page.goto(`/${HOST}/`); // public post page, logged in
await waitReady(page);
await page.evaluate(() => { window.__noReload = true; });
await page.locator(`a[href="/${HOST}/edit"]`).first().click();
await page.waitForURL((u) => u.pathname === `/${HOST}/edit`, { timeout: 15000 });
expect(await page.evaluate(() => window.__noReload)).toBe(true); // it was a SPA nav, no full reload
// the picker, brought in by the swap, loaded its first page of candidates
await expect.poll(() => page.locator(RELROWS).count(), { timeout: 12000 }).toBeGreaterThanOrEqual(1);
await expect(page.locator(RELR)).toContainText('Picker Item');
});
});