// History navigation tests — back/forward buttons (HTMX 4.0 style) // Verifies that popstate re-renders content, restores scroll position, // and matches a fresh load of the same URL. const { test, expect } = require('playwright/test'); const { BASE_URL, waitForSxReady, loadPage, trackErrors } = require('./helpers'); test.describe('History navigation', () => { let t; test.beforeEach(({ page }) => { t = trackErrors(page); }); test.afterEach(() => { expect(t.errors()).toEqual([]); }); test('back button restores previous page content', async ({ page }) => { await loadPage(page, '(geography)'); await expect(page.locator('#sx-content')).toContainText('Geography'); // Navigate forward await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await expect(page.locator('#sx-content')).toContainText('Reactive Islands', { timeout: 5000 }); // Back button await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); await expect(page).not.toHaveURL(/reactive/); // Content must update to match the URL await expect(page.locator('#sx-content')).toContainText('Geography', { timeout: 5000 }); }); test('forward button restores next page content', async ({ page }) => { await loadPage(page, '(geography)'); // Navigate forward await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await expect(page.locator('#sx-content')).toContainText('Reactive Islands', { timeout: 5000 }); // Back await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); await expect(page.locator('#sx-content')).toContainText('Geography', { timeout: 5000 }); // Forward await page.goForward(); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await expect(page.locator('#sx-content')).toContainText('Reactive Islands', { timeout: 5000 }); }); test('back button after multiple navigations', async ({ page }) => { await loadPage(page, ''); // Navigate: home -> geography -> reactive await page.click('a[sx-get*="(geography)"]'); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); await page.waitForTimeout(1000); await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await page.waitForTimeout(1000); // Back to geography await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); await expect(page).not.toHaveURL(/reactive/); await expect(page.locator('#sx-content')).toContainText('Geography', { timeout: 5000 }); // Back to home await page.goBack(); await expect(page).toHaveURL(/\/sx\/?$/, { timeout: 5000 }); }); test('back button is SPA nav, not full reload', async ({ page }) => { await loadPage(page, '(geography)'); // Set marker that survives SPA nav but not full reload await page.evaluate(() => window.__history_marker = true); // Navigate forward await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); // Marker should survive forward SPA nav expect(await page.evaluate(() => window.__history_marker)).toBe(true); // Back button await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Marker should survive back nav (no full reload) expect(await page.evaluate(() => window.__history_marker)).toBe(true); }); test('nav sidebar updates on back button', async ({ page }) => { await loadPage(page, '(geography)'); await expect(page.locator('#sx-nav')).toContainText('Geography'); // Navigate forward await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); // Back button await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Nav should show Geography content, not Reactive Islands await expect(page.locator('#sx-nav')).toContainText('Geography', { timeout: 5000 }); }); test('layout preserved after back button', async ({ page }) => { await loadPage(page, '(geography)'); await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // Key layout elements must exist and not be duplicated const layout = await page.evaluate(() => ({ contentCount: document.querySelectorAll('#sx-content').length, navCount: document.querySelectorAll('#sx-nav').length, headerCount: document.querySelectorAll('[data-sx-island="layouts/header"]').length, })); expect(layout.contentCount).toBe(1); expect(layout.navCount).toBe(1); expect(layout.headerCount).toBe(1); }); test('scroll position saved on forward nav', async ({ page }) => { await loadPage(page, '(geography)'); // Scroll down await page.evaluate(() => window.scrollTo(0, 200)); await page.waitForTimeout(100); // Navigate forward — should save scroll via replaceState await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); // Check that the previous history entry has scrollY saved const state = await page.evaluate(() => { // Can't read previous entry state directly, but we can check // that pushState was called with state on the current entry return window.history.state; }); // Current entry (the new page) won't have scrollY, // but we can verify the mechanism works by going back await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); // The page should have restored (browser handles the state object) // We verify the content loaded correctly as the primary check await expect(page.locator('#sx-content')).toContainText('Geography', { timeout: 5000 }); }); test('back button content matches fresh load', async ({ page }) => { await loadPage(page, '(geography)'); // Navigate forward await page.click('a[href*="geography.(reactive)"]:not([href*="runtime"])'); await expect(page).toHaveURL(/reactive/, { timeout: 5000 }); await page.waitForTimeout(1000); // Go back await page.goBack(); await expect(page).toHaveURL(/geography/, { timeout: 5000 }); await page.waitForTimeout(1000); // Snapshot the back-button DOM const backContent = await page.evaluate(() => document.querySelector('#sx-content')?.textContent?.trim().substring(0, 200)); // Fresh load the same URL await page.goto(page.url(), { waitUntil: 'domcontentloaded' }); await waitForSxReady(page); const freshContent = await page.evaluate(() => document.querySelector('#sx-content')?.textContent?.trim().substring(0, 200)); // Back-button content should match fresh load expect(backContent).toBe(freshContent); }); });