// 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. 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' }); // 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 await page.click('a[href*="click-to-load"]'); await page.waitForURL('**/click-to-load**'); await page.waitForTimeout(2000); // 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 }) => { await page.goto(BASE_URL + '/sx/(geography.(hypermedia.(example)))', { waitUntil: 'networkidle' }); // 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'); // 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'); }); test('#sx-content exists after SPA navigation', 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); // sx-content should still exist (may contain error boundary, but not be missing) await expect(page.locator('#sx-content').first()).toBeAttached(); }); 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); 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); // 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'); } }); });