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();
}