Files
rose-ash/tests/playwright/demo-interactions.spec.js
giles 847d04d4ba Optimize Playwright tests: shorter waits, isolated but fast
Rewrite geography-demos, demo-interactions, and reactive-nav specs
to use 300-500ms waits instead of 2-3s sleeps. Each test still does
a fresh page.goto() for isolation (no knock-on failures) but benefits
from server response cache for near-instant page loads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:36:55 +00:00

269 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-check
/**
* Demo interaction tests — verify every demo actually functions.
* Each test is isolated (fresh page.goto) for reliability.
* Server cache keeps page loads fast.
*/
const { test, expect } = require('playwright/test');
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
async function loadDemo(page, path) {
await page.goto(BASE_URL + '/sx/' + path, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(500);
}
function island(page, pattern) {
return page.locator(`[data-sx-island*="${pattern}"]`);
}
async function assertNoClassLeak(page, scope) {
const loc = scope ? page.locator(scope).first() : page.locator('#sx-root');
const text = await loc.textContent();
expect(text).not.toContain('classpx');
expect(text).not.toContain('classborder');
expect(text).not.toContain('classbg-');
}
// ===========================================================================
// Reactive island demos
// ===========================================================================
test.describe('Reactive island interactions', () => {
test('counter: + and change count and doubled', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.counter)))');
const el = island(page, 'counter');
await expect(el).toBeVisible({ timeout: 10000 });
const buttons = el.locator('button');
await expect(buttons).toHaveCount(2);
const plus = buttons.last();
await plus.click(); await plus.click(); await plus.click();
await page.waitForTimeout(300);
const text = await el.textContent();
expect(text).toContain('3');
expect(text).toContain('doubled');
expect(text).toContain('6');
await buttons.first().click();
await page.waitForTimeout(300);
expect(await el.textContent()).toContain('2');
});
test('temperature: +/ change celsius and fahrenheit', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.temperature)))');
const el = island(page, 'temperature');
await expect(el).toBeVisible({ timeout: 10000 });
const buttons = el.locator('button');
await buttons.last().click();
await buttons.last().click();
await page.waitForTimeout(300);
const text = await el.textContent();
expect(text).toContain('°C');
expect(text).toContain('°F');
});
test('stopwatch: start shows elapsed time', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.stopwatch)))');
const el = island(page, 'stopwatch');
await expect(el).toBeVisible({ timeout: 10000 });
const textBefore = await el.textContent();
await el.locator('button').first().click();
await page.waitForTimeout(1200);
expect(await el.textContent()).not.toBe(textBefore);
});
test('input-binding: typing updates live preview', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.input-binding)))');
const el = island(page, 'input-binding');
await expect(el).toBeVisible({ timeout: 10000 });
await el.locator('input').first().fill('playwright test');
await page.waitForTimeout(300);
expect(await el.textContent()).toContain('playwright test');
});
test('dynamic-class: toggle changes element styling', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.dynamic-class)))');
const el = island(page, 'dynamic-class');
await expect(el).toBeVisible({ timeout: 10000 });
const htmlBefore = await el.innerHTML();
await el.locator('button').first().click();
await page.waitForTimeout(300);
expect(await el.innerHTML()).not.toBe(htmlBefore);
});
test('reactive-list: add button increases items', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.reactive-list)))');
const el = island(page, 'reactive-list');
await expect(el).toBeVisible({ timeout: 10000 });
const textBefore = await el.textContent();
await el.locator('button').first().click();
await page.waitForTimeout(300);
expect(await el.textContent()).not.toBe(textBefore);
});
test('stores: writer and reader share state', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.stores)))');
const writer = island(page, 'store-writer');
const reader = island(page, 'store-reader');
await expect(writer).toBeVisible({ timeout: 10000 });
await expect(reader).toBeVisible({ timeout: 10000 });
expect((await writer.textContent()).length).toBeGreaterThan(0);
expect((await reader.textContent()).length).toBeGreaterThan(0);
});
test('refs: focus button focuses input', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.refs)))');
const el = island(page, 'refs');
await expect(el).toBeVisible({ timeout: 10000 });
const textBefore = await el.textContent();
await el.locator('button').first().click();
await page.waitForTimeout(300);
const focused = await page.evaluate(() => document.activeElement?.tagName);
const textAfter = await el.textContent();
expect(textAfter !== textBefore || focused === 'INPUT').toBeTruthy();
});
test('portal: button toggles portal content', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.portal)))');
const el = island(page, 'portal');
await expect(el).toBeVisible({ timeout: 10000 });
const before = await page.locator('#portal-root').innerHTML();
await el.locator('button').first().click();
await page.waitForTimeout(300);
expect(await page.locator('#portal-root').innerHTML()).not.toBe(before);
});
test('imperative: button triggers DOM manipulation', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.imperative)))');
const el = island(page, 'imperative');
await expect(el).toBeVisible({ timeout: 10000 });
const textBefore = await el.textContent();
await el.locator('button').first().click();
await page.waitForTimeout(300);
expect(await el.textContent()).not.toBe(textBefore);
});
test('error-boundary: trigger shows boundary message', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.error-boundary)))');
const el = island(page, 'error-boundary');
await expect(el).toBeVisible({ timeout: 10000 });
const btn = el.locator('button').filter({ hasText: /error|trigger|throw/i }).first();
if (await btn.count() > 0) {
await btn.click();
await page.waitForTimeout(300);
expect((await el.textContent()).toLowerCase()).toMatch(/error|caught|boundary/);
}
});
test('event-bridge: sender triggers receiver', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.event-bridge-demo)))');
const el = island(page, 'event-bridge');
await expect(el).toBeVisible({ timeout: 10000 });
const textBefore = await el.textContent();
await el.locator('button').first().click();
await page.waitForTimeout(300);
expect(await el.textContent()).not.toBe(textBefore);
});
test('resource: shows loading then resolved data', async ({ page }) => {
await loadDemo(page, '(geography.(reactive.(examples.resource)))');
const el = island(page, 'resource');
await expect(el).toBeVisible({ timeout: 10000 });
await page.waitForTimeout(2000);
expect(await el.textContent()).toContain('Ada');
});
});
// ===========================================================================
// Marshes demos
// ===========================================================================
test.describe('Marshes interactions', () => {
test('hypermedia-feeds: reactive +/ works', async ({ page }) => {
await loadDemo(page, '(geography.(marshes.hypermedia-feeds))');
const el = island(page, 'marsh-product');
await expect(el).toBeVisible({ timeout: 10000 });
const plusBtn = el.locator('button:has-text("+")').first();
if (await plusBtn.count() > 0) {
const textBefore = await el.textContent();
await plusBtn.click();
await page.waitForTimeout(300);
expect(await el.textContent()).not.toBe(textBefore);
}
await assertNoClassLeak(page, '[data-sx-island*="marsh-product"]');
});
test('on-settle: settle evaluates after swap', async ({ page }) => {
await loadDemo(page, '(geography.(marshes.on-settle))');
const el = island(page, 'marsh-settle');
await expect(el).toBeVisible({ timeout: 10000 });
const btn = el.locator('button').first();
if (await btn.count() > 0) {
const textBefore = await el.textContent();
await btn.click();
await page.waitForTimeout(2000);
expect(await el.textContent()).not.toBe(textBefore);
}
await assertNoClassLeak(page, '[data-sx-island*="marsh-settle"]');
});
test('server-signals: server writes to client signal', async ({ page }) => {
await loadDemo(page, '(geography.(marshes.server-signals))');
const writer = island(page, 'marsh-store-writer');
const reader = island(page, 'marsh-store-reader');
await expect(writer).toBeVisible({ timeout: 10000 });
await expect(reader).toBeVisible({ timeout: 10000 });
});
test('view-transform: view toggle changes rendering', async ({ page }) => {
await loadDemo(page, '(geography.(marshes.view-transform))');
const el = island(page, 'marsh-view-transform');
await expect(el).toBeVisible({ timeout: 10000 });
const viewBtns = el.locator('button');
if (await viewBtns.count() >= 2) {
const htmlBefore = await el.innerHTML();
await viewBtns.nth(1).click();
await page.waitForTimeout(300);
expect(await el.innerHTML()).not.toBe(htmlBefore);
}
});
});
// ===========================================================================
// Server health — no JS errors across demo pages
// ===========================================================================
test.describe('Server health', () => {
test('no JS errors on reactive demo pages', async ({ page }) => {
const errors = [];
page.on('pageerror', err => errors.push(err.message));
const demos = ['counter', 'temperature', 'stopwatch', 'input-binding',
'dynamic-class', 'reactive-list', 'stores', 'resource'];
for (const demo of demos) {
await page.goto(BASE_URL + `/sx/(geography.(reactive.(examples.${demo})))`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(500);
}
const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch'));
expect(real).toEqual([]);
});
test('no JS errors on marshes pages', async ({ page }) => {
const errors = [];
page.on('pageerror', err => errors.push(err.message));
const pages = ['hypermedia-feeds', 'on-settle', 'server-signals',
'signal-triggers', 'view-transform'];
for (const p of pages) {
await page.goto(BASE_URL + `/sx/(geography.(marshes.${p}))`, { waitUntil: 'networkidle', timeout: 15000 });
await page.waitForTimeout(500);
}
const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch'));
expect(real).toEqual([]);
});
});