Files
rose-ash/tests/playwright/geography-demos.spec.js
giles c8280e156f Add comprehensive Playwright tests for all geography demos (61 tests)
Tests every page under /sx/(geography...):
- 9 section index pages (geography, reactive, hypermedia, marshes, etc.)
- 16 reactive island demos with interaction tests (counter, temperature,
  stopwatch, input-binding, dynamic-class, reactive-list, stores, etc.)
- 27 hypermedia demos (click-to-load, form-submission, tabs, etc.)
- Cross-navigation reactivity (counter → temperature → counter)
- Sequential 5-demo navigation test
- CEK, marshes, isomorphism, scopes, spreads, provide, reference pages

Total Playwright tests: 72 (6 isomorphic + 5 reactive-nav + 61 geography)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:50:48 +00:00

401 lines
14 KiB
JavaScript

// @ts-check
/**
* Geography demos — comprehensive navigation + interaction tests.
*
* Tests every page under /sx/(geography...) loads correctly:
* - Full page load (SSR)
* - Client-side navigation from a sibling page
* - Interactive elements (buttons, inputs) work after navigation
* - Islands hydrate and signals fire
*/
const { test, expect } = require('playwright/test');
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
// ---------------------------------------------------------------------------
// Helper: load a page, verify it has content
// ---------------------------------------------------------------------------
async function verifyPageLoads(page, path, { expectIsland, expectText } = {}) {
await page.goto(BASE_URL + '/sx/' + path, { waitUntil: 'networkidle' });
await page.waitForTimeout(1500);
// Page should have a root with content
const root = page.locator('#sx-root');
await expect(root).toBeVisible({ timeout: 10000 });
const text = await root.textContent();
expect(text.length).toBeGreaterThan(50);
if (expectText) {
expect(text).toContain(expectText);
}
if (expectIsland) {
const island = page.locator(`[data-sx-island*="${expectIsland}"]`);
await expect(island).toBeVisible({ timeout: 5000 });
}
}
// ---------------------------------------------------------------------------
// Helper: navigate via link click, verify target page
// ---------------------------------------------------------------------------
async function navigateAndVerify(page, startPath, linkPattern, { expectIsland, expectText } = {}) {
await page.goto(BASE_URL + '/sx/' + startPath, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
const link = page.locator(`a[href*="${linkPattern}"]`).first();
if (await link.count() === 0) return false;
await link.click();
await page.waitForTimeout(3000);
const text = await page.locator('#sx-root').textContent();
if (expectText) expect(text).toContain(expectText);
if (expectIsland) {
const island = page.locator(`[data-sx-island*="${expectIsland}"]`);
await expect(island).toBeVisible({ timeout: 5000 });
}
return true;
}
// ---------------------------------------------------------------------------
// Helper: verify island is reactive (click button, expect text change)
// ---------------------------------------------------------------------------
async function verifyIslandReactive(page, islandSelector) {
const island = page.locator(islandSelector);
if (await island.count() === 0) return false;
const buttons = island.locator('button');
if (await buttons.count() === 0) return false;
const textBefore = await island.textContent();
await buttons.first().click();
await page.waitForTimeout(500);
const textAfter = await island.textContent();
return textAfter !== textBefore;
}
// ===========================================================================
// Geography index pages — load and have content
// ===========================================================================
test.describe('Geography section pages load', () => {
const sections = [
['(geography)', 'Geography'],
['(geography.(reactive))', 'Reactive'],
['(geography.(hypermedia))', 'Hypermedia'],
['(geography.(marshes))', 'Marshes'],
['(geography.(scopes))', 'Scopes'],
['(geography.(cek))', 'CEK'],
['(geography.(isomorphism))', 'Isomorphism'],
['(geography.(spreads))', 'Spreads'],
['(geography.(provide))', 'Provide'],
];
for (const [path, expectedText] of sections) {
test(`${path} loads with content`, async ({ page }) => {
await verifyPageLoads(page, path, { expectText: expectedText });
});
}
});
// ===========================================================================
// Reactive island demos — direct load + navigation + interaction
// ===========================================================================
test.describe('Reactive demos', () => {
test('counter: buttons change count', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.counter)))', {
expectIsland: 'counter',
});
const island = page.locator('[data-sx-island*="counter"]');
const buttons = island.locator('button');
await expect(buttons).toHaveCount(2);
const textBefore = await island.textContent();
await buttons.last().click();
await page.waitForTimeout(300);
expect(await island.textContent()).not.toBe(textBefore);
});
test('temperature: input changes conversion', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.temperature)))', {
expectIsland: 'temperature',
});
const island = page.locator('[data-sx-island*="temperature"]');
const buttons = island.locator('button');
if (await buttons.count() >= 2) {
const textBefore = await island.textContent();
await buttons.last().click();
await page.waitForTimeout(300);
expect(await island.textContent()).not.toBe(textBefore);
}
});
test('stopwatch: start/stop buttons work', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.stopwatch)))', {
expectIsland: 'stopwatch',
});
const island = page.locator('[data-sx-island*="stopwatch"]');
const buttons = island.locator('button');
if (await buttons.count() > 0) {
await buttons.first().click(); // start
await page.waitForTimeout(1500);
const text = await island.textContent();
// Should show some elapsed time
expect(text.length).toBeGreaterThan(0);
}
});
test('input-binding: typing updates display', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.input-binding)))', {
expectIsland: 'input-binding',
});
const island = page.locator('[data-sx-island*="input-binding"]');
const input = island.locator('input').first();
if (await input.count() > 0) {
await input.fill('hello test');
await page.waitForTimeout(300);
const text = await island.textContent();
expect(text).toContain('hello test');
}
});
test('dynamic-class: toggle changes classes', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.dynamic-class)))', {
expectIsland: 'dynamic-class',
});
const island = page.locator('[data-sx-island*="dynamic-class"]');
const buttons = island.locator('button');
if (await buttons.count() > 0) {
const changed = await verifyIslandReactive(page, '[data-sx-island*="dynamic-class"]');
expect(changed).toBe(true);
}
});
test('reactive-list: add/remove items', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.reactive-list)))', {
expectIsland: 'reactive-list',
});
const island = page.locator('[data-sx-island*="reactive-list"]');
const buttons = island.locator('button');
if (await buttons.count() > 0) {
const changed = await verifyIslandReactive(page, '[data-sx-island*="reactive-list"]');
expect(changed).toBe(true);
}
});
test('stores: shared state across islands', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.stores)))', {
expectIsland: 'store-writer',
});
// Both reader and writer islands should be present
await expect(page.locator('[data-sx-island*="store-reader"]')).toBeVisible();
await expect(page.locator('[data-sx-island*="store-writer"]')).toBeVisible();
});
test('refs: DOM references work', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.refs)))', {
expectIsland: 'refs',
});
});
test('portal: renders outside island', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.portal)))', {
expectIsland: 'portal',
});
});
test('resource: async data loading', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.resource)))');
});
test('imperative: DOM manipulation', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.imperative)))');
});
test('transition: animation', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.transition)))');
});
test('error-boundary: catches errors', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.error-boundary)))');
});
test('event-bridge: cross-island events', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.event-bridge-demo)))');
});
test('defisland: island definition', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.defisland)))');
});
test('coverage: feature coverage', async ({ page }) => {
await verifyPageLoads(page, '(geography.(reactive.(examples.coverage)))');
});
});
// ===========================================================================
// Hypermedia demos — load and have interactive elements
// ===========================================================================
test.describe('Hypermedia demos', () => {
const demos = [
['click-to-load', 'Click to Load'],
['form-submission', 'Form'],
['polling', 'Polling'],
['delete-row', 'Delete'],
['edit-row', 'Edit'],
['tabs', 'Tabs'],
['active-search', 'Search'],
['inline-validation', 'Validation'],
['lazy-loading', 'Lazy'],
['infinite-scroll', 'Scroll'],
['select-filter', 'Filter'],
['loading-states', 'Loading'],
['dialogs', 'Dialog'],
['oob-swaps', 'Out of Band'],
['bulk-update', 'Bulk'],
['animations', 'Animat'],
['inline-edit', 'Inline'],
['progress-bar', 'Progress'],
['swap-positions', 'Swap'],
['sync-replace', 'Sync'],
['keyboard-shortcuts', 'Keyboard'],
['json-encoding', 'JSON'],
['put-patch', 'PUT'],
['retry', 'Retry'],
['reset-on-submit', 'Reset'],
['value-select', 'Value'],
['vals-and-headers', 'Header'],
];
for (const [slug, text] of demos) {
test(`${slug} loads`, async ({ page }) => {
await verifyPageLoads(page, `(geography.(hypermedia.(example.${slug})))`);
});
}
});
// ===========================================================================
// Cross-navigation: reactive demos preserve reactivity
// ===========================================================================
test.describe('Cross-navigation reactivity', () => {
test('counter → temperature → counter: all stay reactive', async ({ page }) => {
// Load counter
await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
let island = page.locator('[data-sx-island*="counter"]');
await expect(island).toBeVisible({ timeout: 10000 });
let buttons = island.locator('button');
let before = await island.textContent();
await buttons.last().click();
await page.waitForTimeout(300);
expect(await island.textContent()).not.toBe(before);
// Navigate to temperature
const tempLink = page.locator('a[href*="temperature"]').first();
if (await tempLink.count() > 0) {
await tempLink.click();
await page.waitForTimeout(3000);
island = page.locator('[data-sx-island*="temperature"]');
await expect(island).toBeVisible({ timeout: 10000 });
buttons = island.locator('button');
if (await buttons.count() >= 2) {
before = await island.textContent();
await buttons.last().click();
await page.waitForTimeout(300);
expect(await island.textContent()).not.toBe(before);
}
}
// Navigate back to counter
const counterLink = page.locator('a[href*="counter"]').first();
if (await counterLink.count() > 0) {
await counterLink.click();
await page.waitForTimeout(3000);
island = page.locator('[data-sx-island*="counter"]');
await expect(island).toBeVisible({ timeout: 10000 });
buttons = island.locator('button');
before = await island.textContent();
await buttons.last().click();
await page.waitForTimeout(300);
expect(await island.textContent()).not.toBe(before);
}
});
test('navigate through 5 reactive demos sequentially', async ({ page }) => {
const demos = ['counter', 'temperature', 'stopwatch', 'dynamic-class', 'input-binding'];
await page.goto(BASE_URL + '/sx/(geography.(reactive.(examples.counter)))', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
for (let i = 1; i < demos.length; i++) {
const link = page.locator(`a[href*="${demos[i]}"]`).first();
if (await link.count() > 0) {
await link.click();
await page.waitForTimeout(3000);
// Page should have content
const text = await page.locator('#sx-root').textContent();
expect(text.length).toBeGreaterThan(100);
// Check for any islands
const islands = await page.locator('[data-sx-island]').count();
expect(islands).toBeGreaterThan(0); // at least the header island
}
}
});
});
// ===========================================================================
// Other geography pages — CEK, marshes, scopes, etc.
// ===========================================================================
test.describe('Other geography pages', () => {
test('CEK pages load', async ({ page }) => {
for (const sub of ['', '.content', '.demo', '.freeze']) {
await verifyPageLoads(page, `(geography.(cek${sub}))`);
}
});
test('marshes pages load', async ({ page }) => {
for (const sub of ['', '.hypermedia-feeds', '.on-settle', '.server-signals', '.signal-triggers', '.view-transform']) {
await verifyPageLoads(page, `(geography.(marshes${sub}))`);
}
});
test('isomorphism pages load', async ({ page }) => {
for (const sub of ['', '.affinity', '.routing-analyzer', '.bundle-analyzer']) {
await verifyPageLoads(page, `(geography.(isomorphism${sub}))`);
}
});
test('scopes page loads', async ({ page }) => {
await verifyPageLoads(page, '(geography.(scopes))');
});
test('spreads page loads', async ({ page }) => {
await verifyPageLoads(page, '(geography.(spreads))');
});
test('provide page loads', async ({ page }) => {
await verifyPageLoads(page, '(geography.(provide))');
});
test('reference pages load', async ({ page }) => {
for (const sub of ['attributes', 'events', 'headers', 'js-api']) {
await verifyPageLoads(page, `(geography.(hypermedia.(reference.${sub})))`);
}
});
});