// SPA navigation tests — verify header, nav, and content survive navigation // Tests that OOB swaps update nav while preserving the header island, // 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('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); // Navigate via SPA link to Reactive Islands await page.click('a[sx-get*="(geography.(reactive))"]'); await page.waitForTimeout(4000); // 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); 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 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'); await page.click('a[sx-get*="click-to-load"]'); await page.waitForTimeout(4000); // Nav should still update despite content error await expect(page.locator('#sx-nav')).toContainText('Click to Load'); }); test('content renders inside error-boundary after SPA nav', async ({ page }) => { await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' }); await page.click('a[sx-get*="click-to-load"]'); await page.waitForTimeout(4000); // Content should render inside an error boundary (no render error) const boundary = page.locator('#sx-content [data-sx-boundary]'); await expect(boundary).toHaveCount(1, { timeout: 2000 }).catch(() => {}); await expect(page.locator('#sx-content')).toContainText('Load', { timeout: 3000 }); // No render errors should be visible const errors = page.locator('#sx-content .sx-render-error'); await expect(errors).toHaveCount(0); // Header should still be intact await expect(page.locator('[data-sx-island="layouts/header"]')).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); // 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'); // 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'); } }); });