import assert from 'node:assert/strict'; import { build } from 'esbuild'; import { chromium } from '@playwright/test'; import { fileURLToPath } from 'node:url'; import { existsSync } from 'node:fs'; import { presentations } from '../src/server/react/presentation'; import { createStyleFixture } from './style-fixtures'; // Actual React layout effects and the shared scroll-lock hook run in an isolated // browser. Rerenders/remounts model incoming page payloads without reloading . const root = fileURLToPath(new URL('..', import.meta.url)); const bundle = await build({ stdin: { resolveDir: root, loader: 'tsx', contents: ` import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import {PagePresentation} from '@/components/layout/PagePresentation'; import {useScrollLock} from '@/components/ui/Dialog'; function Lock({active}) {useScrollLock(active);return null;} const root=createRoot(document.getElementById('root')); let view='about',locked=false,key='shared-page'; function render(){root.render(
);} window.__pagePresentation=(next,remount=false)=>{view=next;key=remount?next:'shared-page';render();}; window.__pageLock=(active)=>{locked=active;render();}; render(); `, }, bundle: true, platform: 'browser', format: 'iife', write: false, define: { 'process.env.NODE_ENV': '"development"' }, tsconfig: fileURLToPath(new URL('../tsconfig.json', import.meta.url)), }); const styles = await createStyleFixture(['base', 'list', 'nav']); const initial = presentations.about!; const chrome = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; const browser = await chromium.launch({ headless: true, ...(existsSync(chrome) ? { executablePath: chrome } : {}) }); try { const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); const errors: string[] = []; page.on('pageerror', error => errors.push(error.message)); await page.route('**/*', route => route.abort()); await page.setContent(`
`); const documentIdentity = await page.evaluate(() => { const token = Math.random().toString(36); (window as unknown as { __documentIdentity: string }).__documentIdentity = token; return token; }); await page.addScriptTag({ content: bundle.outputFiles[0]!.text }); const managed = [...new Set(Object.values(presentations).flatMap(({ bodyClass }) => bodyClass.split(/\s+/).filter(Boolean)))]; let mobile = false; const verify = async (view: string, remount = false) => { const expected = presentations[view] || presentations['404']!; await page.evaluate(({ view, remount }) => { (window as unknown as { __pagePresentation(view: string, remount: boolean): void }).__pagePresentation(view, remount); }, { view, remount }); await page.waitForFunction(({ tokens, classes, managed }) => { const active = new Set(classes.split(/\s+/).filter(Boolean)); return document.documentElement.getAttribute('data-hk-page-styles') === tokens && managed.every(name => document.body.classList.contains(name) === active.has(name)); }, { tokens: expected.styleTokens.join(' '), classes: expected.bodyClass, managed }); const state = await page.evaluate(() => ({ identity: (window as unknown as { __documentIdentity: string }).__documentIdentity, htmlClasses: [...document.documentElement.classList], other: document.documentElement.getAttribute('data-presentation-unrelated'), classes: [...document.body.classList], overflow: document.body.style.overflow, probePadding: getComputedStyle(document.getElementById('page-style-probe')!).paddingTop, })); assert.equal(state.identity, documentIdentity, 'presentation updates must not reload the document'); assert.ok(state.htmlClasses.includes('hk-pwa-standalone')); assert.ok(state.htmlClasses.includes('hk-age-gate-ok')); assert.equal(state.other, 'keep'); for (const name of ['hk-is-managing', 'hk-drawer-open', 'external-plugin-state']) assert.ok(state.classes.includes(name), view + ' erased ' + name); assert.equal(state.overflow, 'hidden', view + ' released a live scroll lock'); if (view === 'nav') assert.equal(state.probePadding, mobile ? '64px' : '132px'); if (view === 'list') assert.equal(state.probePadding, mobile ? '56px' : '72px'); }; await page.evaluate(() => (window as unknown as { __pageLock(active: boolean): void }).__pageLock(true)); await page.waitForFunction(() => document.body.style.overflow === 'hidden'); for (const removed of ['profile', 'setting', 'history', 'favorites', 'purchased', 'recharge', 'login', 'register']) assert.equal(presentations[removed], undefined, 'removed account view still has a presentation: ' + removed); for (const view of Object.keys(presentations)) await verify(view); for (const view of ['list', 'nav', 'help', 'privacy', 'about', '__missing-view', 'list']) await verify(view, true); mobile = true; await page.setViewportSize({ width: 375, height: 812 }); for (const view of ['nav', 'list', 'about']) await verify(view); await page.evaluate(() => (window as unknown as { __pageLock(active: boolean): void }).__pageLock(false)); await page.waitForFunction(() => document.body.style.overflow === 'auto'); assert.deepEqual(errors, []); console.log(`Page presentation browser regression passed: ${Object.keys(presentations).length} views, remount/back transitions, real scoped CSS, PWA/temporary class preservation and shared scroll-lock restoration.`); } finally { await browser.close(); }