Adapter fixes, orchestration updates, example content + SPA tests

From other session: adapter-html/sx/dom fixes, orchestration
improvements, examples-content refactoring, SPA navigation test
updates, WASM copies synced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 13:35:49 +00:00
parent cd9ebc0cd8
commit 46f77c3b1e
15 changed files with 442 additions and 231 deletions

View File

@@ -1,90 +1,100 @@
// SPA navigation tests — verify header, nav, and content survive navigation
// Tests that OOB swaps update nav while preserving the header island,
// and that rendering errors are scoped to #sx-content.
// that sx-select extracts content without nesting, and that rendering
// errors are scoped to #sx-content via error-boundary.
const { test, expect } = require('playwright/test');
const BASE_URL = process.env.SX_TEST_URL || 'http://localhost:8013';
test.describe('SPA navigation', () => {
test('header island survives SPA nav to sibling page', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
test('no #sx-content nesting after SPA nav', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
await expect(page.locator('#sx-content')).toHaveCount(1);
// Header should be present and hydrated
const headerIsland = page.locator('[data-sx-island="layouts/header"]');
await expect(headerIsland).toHaveCount(1);
await expect(headerIsland).toContainText('sx');
// Navigate via SPA link to Reactive Islands
await page.click('a[sx-get*="(geography.(reactive))"]');
await page.waitForTimeout(4000);
// Navigate via SPA
await page.click('a[href*="click-to-load"]');
await page.waitForURL('**/click-to-load**');
await page.waitForTimeout(2000);
// Must still be exactly 1 #sx-content — no nesting
await expect(page.locator('#sx-content')).toHaveCount(1);
});
test('content renders after SPA nav (not empty boundary)', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
await page.click('a[sx-get*="(geography.(reactive))"]');
await page.waitForTimeout(4000);
// Content should have actual page text
await expect(page.locator('#sx-content')).toContainText('Architecture', { timeout: 5000 });
});
test('nav updates via OOB on SPA navigation', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
await expect(page.locator('#sx-nav')).toContainText('Geography');
await page.click('a[sx-get*="(geography.(reactive))"]');
await page.waitForTimeout(4000);
// Nav should update to show the new page
await expect(page.locator('#sx-nav')).toContainText('Reactive Islands');
});
test('header island survives SPA nav', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
await expect(page.locator('[data-sx-island="layouts/header"]')).toHaveCount(1);
await page.click('a[sx-get*="(geography.(reactive))"]');
await page.waitForTimeout(4000);
// Header island should still exist
await expect(page.locator('[data-sx-island="layouts/header"]')).toHaveCount(1);
await expect(page.locator('[data-sx-island="layouts/header"]')).toContainText('sx');
});
test('nav updates via OOB on SPA navigation', async ({ page }) => {
test('nav updates even when content has render error', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
await expect(page.locator('#sx-nav')).toContainText('Examples');
// Nav should show current breadcrumbs
const nav = page.locator('#sx-nav');
await expect(nav).toHaveCount(1);
const navTextBefore = await nav.textContent();
expect(navTextBefore).toContain('Examples');
await page.click('a[sx-get*="click-to-load"]');
await page.waitForTimeout(4000);
// Navigate to a child page
await page.click('a[href*="click-to-load"]');
await page.waitForURL('**/click-to-load**');
await page.waitForTimeout(2000);
// Nav should update to show the new page in breadcrumbs
const navTextAfter = await page.locator('#sx-nav').first().textContent();
expect(navTextAfter).toContain('Click to Load');
// Nav should still update despite content error
await expect(page.locator('#sx-nav')).toContainText('Click to Load');
});
test('#sx-content exists after SPA navigation', async ({ page }) => {
test('render error scoped to #sx-content via error-boundary', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
await expect(page.locator('#sx-content')).toHaveCount(1);
await page.click('a[href*="click-to-load"]');
await page.waitForURL('**/click-to-load**');
await page.waitForTimeout(2000);
await page.click('a[sx-get*="click-to-load"]');
await page.waitForTimeout(4000);
// sx-content should still exist (may contain error boundary, but not be missing)
await expect(page.locator('#sx-content').first()).toBeAttached();
// Error should be inside the error boundary within #sx-content
const errors = page.locator('#sx-content .sx-render-error');
await expect(errors).toHaveCount(1);
await expect(errors).toContainText('Render error');
// Header should still be intact
await expect(page.locator('[data-sx-island="layouts/header"]')).toHaveCount(1);
});
test('rendering error scoped to #sx-content, not full page', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' });
// Verify page structure before nav
await expect(page.locator('#sx-nav')).toHaveCount(1);
test('multiple SPA navigations maintain single #sx-content', async ({ page }) => {
await page.goto(BASE_URL + '/sx/(geography)', { waitUntil: 'networkidle' });
await expect(page.locator('#sx-content')).toHaveCount(1);
await page.click('a[href*="click-to-load"]');
await page.waitForURL('**/click-to-load**');
await page.waitForTimeout(2000);
// First navigation
await page.click('a[sx-get*="(geography.(reactive))"]');
await page.waitForTimeout(4000);
await expect(page.locator('#sx-content')).toHaveCount(1);
await expect(page.locator('#sx-nav')).toContainText('Reactive Islands');
// If there's a render error, it should be inside #sx-content
const errors = page.locator('.sx-render-error');
const errorCount = await errors.count();
if (errorCount > 0) {
// Error should be a descendant of #sx-content, not replacing the whole page
const errorParent = await errors.first().evaluate(el => {
let p = el;
while (p) {
if (p.id === 'sx-content') return 'sx-content';
if (p.id === 'main-panel') return 'main-panel';
p = p.parentElement;
}
return 'unknown';
});
expect(errorParent).toBe('sx-content');
// Nav should still be present even with an error
await expect(page.locator('#sx-nav').first()).toContainText('Click to Load');
// Navigate back to Geography
const geoLink = page.locator('a[sx-get="/sx/(geography)"]');
if (await geoLink.count() > 0) {
await geoLink.first().click();
await page.waitForTimeout(4000);
await expect(page.locator('#sx-content')).toHaveCount(1);
await expect(page.locator('#sx-nav')).toContainText('Geography');
}
});
});