// @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})))`); } }); });