Add sx:ready hydration signal, eliminate test sleeps
boot-init now sets data-sx-ready on <html> and dispatches an sx:ready CustomEvent after all islands are hydrated. Playwright tests use this instead of networkidle + hard-coded sleeps (50+ seconds eliminated). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,9 @@
|
||||
// @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);
|
||||
}
|
||||
const { loadPage } = require('./helpers');
|
||||
|
||||
function island(page, pattern) {
|
||||
return page.locator(`[data-sx-island*="${pattern}"]`);
|
||||
@@ -32,7 +25,7 @@ async function assertNoClassLeak(page, scope) {
|
||||
test.describe('Reactive island interactions', () => {
|
||||
|
||||
test('counter: + and − change count and doubled', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.counter)))');
|
||||
await loadPage(page, '(geography.(reactive.(examples.counter)))');
|
||||
const el = island(page, 'counter');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const buttons = el.locator('button');
|
||||
@@ -52,7 +45,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('temperature: +/− change celsius and fahrenheit', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.temperature)))');
|
||||
await loadPage(page, '(geography.(reactive.(examples.temperature)))');
|
||||
const el = island(page, 'temperature');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const buttons = el.locator('button');
|
||||
@@ -65,7 +58,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('stopwatch: start shows elapsed time', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.stopwatch)))');
|
||||
await loadPage(page, '(geography.(reactive.(examples.stopwatch)))');
|
||||
const el = island(page, 'stopwatch');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const textBefore = await el.textContent();
|
||||
@@ -75,7 +68,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('input-binding: typing updates live preview', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.input-binding)))');
|
||||
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');
|
||||
@@ -84,7 +77,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('dynamic-class: toggle changes element styling', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.dynamic-class)))');
|
||||
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();
|
||||
@@ -94,7 +87,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('reactive-list: add button increases items', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.reactive-list)))');
|
||||
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();
|
||||
@@ -104,7 +97,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('stores: writer and reader share state', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.stores)))');
|
||||
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 });
|
||||
@@ -114,7 +107,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('refs: focus button focuses input', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.refs)))');
|
||||
await loadPage(page, '(geography.(reactive.(examples.refs)))');
|
||||
const el = island(page, 'refs');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const textBefore = await el.textContent();
|
||||
@@ -126,7 +119,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('portal: button toggles portal content', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.portal)))');
|
||||
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();
|
||||
@@ -136,7 +129,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('imperative: button triggers DOM manipulation', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.imperative)))');
|
||||
await loadPage(page, '(geography.(reactive.(examples.imperative)))');
|
||||
const el = island(page, 'imperative');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const textBefore = await el.textContent();
|
||||
@@ -146,7 +139,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('error-boundary: trigger shows boundary message', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.error-boundary)))');
|
||||
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();
|
||||
@@ -158,7 +151,7 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('event-bridge: sender triggers receiver', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.event-bridge-demo)))');
|
||||
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();
|
||||
@@ -168,11 +161,10 @@ test.describe('Reactive island interactions', () => {
|
||||
});
|
||||
|
||||
test('resource: shows loading then resolved data', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(reactive.(examples.resource)))');
|
||||
await loadPage(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');
|
||||
await expect(el).toContainText('Ada', { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +176,7 @@ test.describe('Reactive island interactions', () => {
|
||||
test.describe('Marshes interactions', () => {
|
||||
|
||||
test('hypermedia-feeds: reactive +/− works', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(marshes.hypermedia-feeds))');
|
||||
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();
|
||||
@@ -198,7 +190,7 @@ test.describe('Marshes interactions', () => {
|
||||
});
|
||||
|
||||
test('on-settle: settle evaluates after swap', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(marshes.on-settle))');
|
||||
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();
|
||||
@@ -212,7 +204,7 @@ test.describe('Marshes interactions', () => {
|
||||
});
|
||||
|
||||
test('server-signals: server writes to client signal', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(marshes.server-signals))');
|
||||
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 });
|
||||
@@ -220,7 +212,7 @@ test.describe('Marshes interactions', () => {
|
||||
});
|
||||
|
||||
test('view-transform: view toggle changes rendering', async ({ page }) => {
|
||||
await loadDemo(page, '(geography.(marshes.view-transform))');
|
||||
await loadPage(page, '(geography.(marshes.view-transform))');
|
||||
const el = island(page, 'marsh-view-transform');
|
||||
await expect(el).toBeVisible({ timeout: 10000 });
|
||||
const viewBtns = el.locator('button');
|
||||
@@ -246,8 +238,7 @@ test.describe('Server health', () => {
|
||||
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);
|
||||
await loadPage(page, `(geography.(reactive.(examples.${demo})))`);
|
||||
}
|
||||
const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch'));
|
||||
expect(real).toEqual([]);
|
||||
@@ -259,8 +250,7 @@ test.describe('Server health', () => {
|
||||
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);
|
||||
await loadPage(page, `(geography.(marshes.${p}))`);
|
||||
}
|
||||
const real = errors.filter(e => !e.includes('net::ERR') && !e.includes('fetch'));
|
||||
expect(real).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user