Files
rose-ash/tests/playwright/demo-interactions.spec.js
giles 14d5158b06 Fix 5 Playwright marshes test failures: timing + test logic
on-settle: increase wait from 2s to 4s — server fetch + settle hook
needs more time than the original timeout allowed.

server-signals: add actual cross-island signal test — click a price
button in writer island and verify reader island updates.

view-transform: fetch catalog before toggling view — the view toggle
only changes rendering of loaded items, not the empty state.

All 19 demo-interaction tests pass (was 14/19).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:54:05 +00:00

280 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.
*/
const { test, expect } = require('playwright/test');
const { loadPage, trackErrors } = require('./helpers');
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', () => {
let t;
test.beforeEach(({ page }) => { t = trackErrors(page); });
test.afterEach(() => { expect(t.errors()).toEqual([]); });
test('counter: + and change count and doubled', async ({ page }) => {
await loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(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 loadPage(page, '(geography.(reactive.(examples.resource)))');
const el = island(page, 'resource');
await expect(el).toBeVisible({ timeout: 10000 });
await expect(el).toContainText('Ada', { timeout: 5000 });
});
});
// ===========================================================================
// Marshes demos
// ===========================================================================
test.describe('Marshes interactions', () => {
let t;
test.beforeEach(({ page }) => { t = trackErrors(page); });
test.afterEach(() => { expect(t.errors()).toEqual([]); });
test('hypermedia-feeds: reactive +/ works', async ({ page }) => {
await loadPage(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 loadPage(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(4000);
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 loadPage(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 });
// Click a price button and verify cross-island signal propagation
const priceBtn = writer.locator('button').first();
if (await priceBtn.count() > 0) {
const readerBefore = await reader.textContent();
await priceBtn.click();
await page.waitForTimeout(500);
const readerAfter = await reader.textContent();
expect(readerAfter).not.toBe(readerBefore);
}
});
test('view-transform: view toggle changes rendering', async ({ page }) => {
await loadPage(page, '(geography.(marshes.view-transform))');
const el = island(page, 'marsh-view-transform');
await expect(el).toBeVisible({ timeout: 10000 });
// Fetch catalog first — view toggle only changes rendering of loaded items
const fetchBtn = el.locator('button:has-text("Fetch Catalog")');
if (await fetchBtn.count() > 0) {
await fetchBtn.click();
await page.waitForTimeout(4000);
}
const viewBtns = el.locator('button');
if (await viewBtns.count() >= 2) {
const textBefore = await el.textContent();
await viewBtns.nth(1).click();
await page.waitForTimeout(500);
expect(await el.textContent()).not.toBe(textBefore);
}
});
});
// ===========================================================================
// 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 loadPage(page, `(geography.(reactive.(examples.${demo})))`);
}
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 loadPage(page, `(geography.(marshes.${p}))`);
}
const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch'));
expect(real).toEqual([]);
});
});