Files
web_huakui_web/scripts/page-presentation-smoke.ts
谢宇宁andClaude Opus 5.5 d2ddb1c321 [Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:33:31 +07:00

97 lines
5.9 KiB
TypeScript

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 <html>.
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(<StrictMode><PagePresentation key={key} view={view}/><Lock active={locked}/><main id="page-style-probe" className="hk-drama-browse-page hk-site-navigation-page"/></StrictMode>);}
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(`<!doctype html><html class="hk-pwa-standalone hk-age-gate-ok" data-hk-page-styles="${initial.styleTokens.join(' ')}" data-presentation-unrelated="keep"><head><style>${styles.css}</style></head><body class="${initial.bodyClass} hk-is-managing hk-drawer-open external-plugin-state" style="overflow:auto"><div id="root"></div></body></html>`);
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();
}