游戏分发 web E2E 的冷启动假失败改为浏览器级预热
Project CI / AI game creator shell Rust crates (push) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m56s
Project CI / Backend tests (push) Successful in 3m56s
Project CI / Frontend tests (push) Successful in 2m7s
Project CI / Native shell tests (push) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m34s
Project CI / AI game creator shell web tests (push) Successful in 1m42s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m40s
Project CI / Repository checks (push) Successful in 2m15s

- 现象:冷 Vite 下 check:game-distribution-web-e2e 先卡在目录卡片 20 秒等待,重跑又卡在游玩页「开始游戏」15 秒等待;同一时刻公开目录 API 已把该游戏排在第一
- 根因:原先的 fetch 预热只拿到 SPA 外壳(HTML 里没有路由模块),Vite 仍在断言阶段现编译这些模块图
- 修复:改成起浏览器后用一次性 context 真走 /games、/games/detail、/games/play(domcontentloaded + 2 秒 settle,120 秒上限,失败只告警),首个 goto 的 120 秒超时保留
- 复验:重启 dev 栈(冷缓存)→ 24/24 PASS;四条 web E2E(web / publish / publish-recovery / a11y)全部复跑通过
- 文档:里程碑补 2026-09-29 复跑记录,pitfalls 同日条目按实测证据补正
This commit is contained in:
kdletters
2026-09-29 06:54:47 +08:00
parent 198c208172
commit d8011a72fa
3 changed files with 49 additions and 24 deletions
+33 -21
View File
@@ -38,20 +38,33 @@ function check(name, ok, detail = '') {
);
}
// Vite dev 是按需编译的:某条路由第一次被访问时现编译模块图,实测 /games 冷启动可以到
// 20 秒以上,超过导航默认超时就会被当成"页面打不开"。先预热一遍路由,把冷启动从断言
// 语义里摘出去(预热失败不致命:真正导航时还会再试一次,失败信息照旧可见)。
async function warmUpWebRoute(url, timeoutMs = 120_000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
// Vite dev 是按需编译的:某条路由的模块图要等**浏览器**真的加载它时才现编译,实测冷启动
// 可以超过 20 秒,超过单步断言超时就会被当成"页面打不开"。纯 HTTP 预热只取到 SPA 外壳
// (HTML 里没有路由模块),挡不住这类慢——所以预热必须在浏览器里把每条路由真的走一遍,
// 让 Vite 先把模块编译好。预热失败不致命:断言阶段的导航还会再试一次,失败信息照旧可见。
async function warmUpWebRoutesInBrowser(browser, urls) {
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
});
try {
const response = await fetch(url, { signal: controller.signal });
await response.arrayBuffer();
console.log(`[warmup] ${url} -> ${response.status}`);
} catch (error) {
console.warn(`[warmup] ${url} 预热失败,导航阶段会再试一次:${error}`);
const page = await context.newPage();
for (const url of urls) {
try {
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 120_000,
});
// 模块请求是 DOMContentLoaded 之后才陆续完成的,留一点时间让 Vite 编译落缓存。
await page.waitForTimeout(2_000);
console.log(
`[warmup] ${url} -> ${response?.status() ?? 'no-response'}`,
);
} catch (error) {
console.warn(`[warmup] ${url} 预热失败,断言阶段会再试一次:${error}`);
}
}
} finally {
clearTimeout(timer);
await context.close();
}
}
@@ -282,15 +295,6 @@ async function main() {
const title = `网页链路 ${String(stamp).slice(-6)}`;
const { gameId } = await publishFixtureGame(stamp, title);
// 先把这次要用到的三条网页路由预热一遍,避免 dev 冷启动把它们误伤成"打不开"。
for (const route of [
'/games',
`/games/detail?id=${gameId}`,
`/games/play?id=${gameId}`,
]) {
await warmUpWebRoute(`${WEB}${route}`);
}
const { chromium } = await loadPlaywright();
const executablePath = (process.env.E2E_CHROMIUM_EXECUTABLE ?? '').trim();
const browser = await chromium.launch({
@@ -298,6 +302,14 @@ async function main() {
...(executablePath ? { executablePath } : {}),
});
try {
// 先把这次要用到的三条网页路由在浏览器里预热一遍,避免 dev 冷启动把它们误伤成"打不开"。
await warmUpWebRoutesInBrowser(
browser,
['/games', `/games/detail?id=${gameId}`, `/games/play?id=${gameId}`].map(
(route) => `${WEB}${route}`,
),
);
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
});