补齐游戏分发网页侧链路取证并修掉返回目录丢关键词
Project CI / AI game creator shell Rust crates (push) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / Backend tests (push) Successful in 5m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m50s
Project CI / Frontend tests (push) Successful in 2m24s
Project CI / Native shell tests (push) Successful in 7m27s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m9s
Project CI / AI game creator shell web tests (push) Successful in 1m50s
Project CI / Repository checks (push) Successful in 2m24s
Project CI / AI game creator shell Rust crates (push) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m1s
Project CI / Backend tests (push) Successful in 5m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m50s
Project CI / Frontend tests (push) Successful in 2m24s
Project CI / Native shell tests (push) Successful in 7m27s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m9s
Project CI / AI game creator shell web tests (push) Successful in 1m50s
Project CI / Repository checks (push) Successful in 2m24s
- 新增 scripts/check-game-distribution-web-e2e.mjs:真实 dev 网页栈 + Chromium 游客走目录筛选、空态与重置、详情主动作、分享直达详情、按需加载发行 iframe、失败重试、退出不自动重启、移动端门槛与旋转提示,共 23 项 PASS - GameGalleryPage 的顶栏搜索词同步改为按 prop 值比对:原来的一次性 ref 守卫在开发态 StrictMode 双跑下会用空值清掉刚从 sessionStorage 恢复的关键词 - vite.config.ts 增加 /games/<gameId>/ 发行入口代理(rewrite 到发行网关并去掉 Cookie),本地网页栈与生产 nginx 同口径 - 游戏分发里程碑阶段 C 第 6、7 条改为已勾选,其余条目列出具体缺口 - pitfalls 记录 StrictMode 守卫击穿与 Vite 发行入口代理两条经验
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
// 游戏分发网页侧真实链路检查:真实 api-server 发布一款游戏,再用 Chromium 以游客身份
|
||||
// 走「目录筛选 → 详情主动作 → 在线游玩(按需加载 iframe / 失败重试 / 退出不自动重启)→
|
||||
// 移动端门槛」。
|
||||
//
|
||||
// 需要:完整本地 dev 栈(`npm run dev`,网页 3000 + api-server 8082)+ 管理员账号 + playwright。
|
||||
// npm install --prefix %TEMP%\genarrative-pw --no-save --no-package-lock playwright
|
||||
// E2E_PLAYWRIGHT_DIR=%TEMP%\genarrative-pw
|
||||
// E2E_ADMIN_USER=... E2E_ADMIN_PASSWORD=... npm run check:game-distribution-web-e2e
|
||||
// E2E_WEB_BASE 默认 http://127.0.0.1:3000,E2E_API_BASE 默认 http://127.0.0.1:8082。
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
const WEB = process.env.E2E_WEB_BASE ?? 'http://127.0.0.1:3000';
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? '';
|
||||
const DEV_PASSWORD = 'GenE2e123!';
|
||||
|
||||
if (!ADMIN_USER || !ADMIN_PASSWORD) {
|
||||
console.error(
|
||||
'缺少 E2E_ADMIN_USER / E2E_ADMIN_PASSWORD:本脚本要发布并审核一款真实游戏供网页侧游玩。',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
function check(name, ok, detail = '') {
|
||||
if (!ok) failures += 1;
|
||||
console.log(
|
||||
`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function api(pathname, options = {}) {
|
||||
const { method = 'GET', token, body, headers = {}, binary } = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
text,
|
||||
json,
|
||||
data: json?.data,
|
||||
error: json?.error,
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadCover(token, id) {
|
||||
const fileName = `web-e2e-${id}.png`;
|
||||
const ticket = await api('/api/assets/direct-upload-tickets', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: {
|
||||
legacyPrefix: 'generated-character-drafts',
|
||||
pathSegments: ['game-distribution', 'web-e2e', String(id)],
|
||||
fileName,
|
||||
contentType: 'image/png',
|
||||
access: 'private',
|
||||
maxSizeBytes: COVER_PNG.length,
|
||||
metadata: { asset_kind: 'game_distribution_cover' },
|
||||
},
|
||||
});
|
||||
if (ticket.status !== 200) {
|
||||
throw new Error(`创建直传凭证失败 ${ticket.status}`);
|
||||
}
|
||||
const upload = ticket.data.upload;
|
||||
const form = new FormData();
|
||||
for (const [key, value] of Object.entries(upload.formFields ?? {})) {
|
||||
if (value !== null && value !== undefined) form.append(key, String(value));
|
||||
}
|
||||
form.append('file', new Blob([COVER_PNG], { type: 'image/png' }), fileName);
|
||||
const put = await fetch(upload.host, { method: 'POST', body: form });
|
||||
if (!put.ok) throw new Error(`直传对象存储失败 ${put.status}`);
|
||||
const confirm = await api('/api/assets/objects/confirm', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: {
|
||||
bucket: upload.bucket,
|
||||
objectKey: upload.objectKey,
|
||||
contentType: 'image/png',
|
||||
contentLength: COVER_PNG.length,
|
||||
assetKind: 'game_distribution_cover',
|
||||
accessPolicy: 'private',
|
||||
entityId: 'game-distribution-web-e2e',
|
||||
},
|
||||
});
|
||||
if (confirm.status !== 200) {
|
||||
throw new Error(`确认素材失败 ${confirm.status}`);
|
||||
}
|
||||
return confirm.data.assetObject.assetObjectId;
|
||||
}
|
||||
|
||||
async function publishFixtureGame(stamp, title) {
|
||||
const adminLogin = await api('/admin/api/login', {
|
||||
method: 'POST',
|
||||
body: { username: ADMIN_USER, password: ADMIN_PASSWORD },
|
||||
});
|
||||
const admin = adminLogin.data?.token ?? adminLogin.data?.accessToken;
|
||||
check(
|
||||
'管理员登录成功',
|
||||
adminLogin.status === 200 && Boolean(admin),
|
||||
`status=${adminLogin.status}`,
|
||||
);
|
||||
if (!admin) process.exit(1);
|
||||
|
||||
const register = await api('/api/auth/entry', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
purePhoneNumber: `132${String(stamp).slice(-8)}`,
|
||||
password: DEV_PASSWORD,
|
||||
},
|
||||
});
|
||||
const author = register.data?.token;
|
||||
check(
|
||||
'作者注册拿到 token',
|
||||
register.status === 200 && Boolean(author),
|
||||
`status=${register.status}`,
|
||||
);
|
||||
if (!author) process.exit(1);
|
||||
|
||||
await api('/admin/api/feature-gates', {
|
||||
method: 'PUT',
|
||||
token: admin,
|
||||
body: {
|
||||
gateKey: 'game-distribution:publish',
|
||||
enabled: true,
|
||||
rolloutPercent: 100,
|
||||
allowUserIds: [],
|
||||
allowUserTags: [],
|
||||
denyUserIds: [],
|
||||
description: 'E2E 网页链路',
|
||||
},
|
||||
});
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
'index.html',
|
||||
'<!doctype html><html><head><meta charset="utf-8"><title>web e2e</title>' +
|
||||
'<script src="assets/app.js"></script></head><body><h1>WEB-E2E-OK</h1></body></html>',
|
||||
);
|
||||
zip.file('assets/app.js', 'document.documentElement.dataset.e2e="web";');
|
||||
const packageBytes = Buffer.from(
|
||||
await zip.generateAsync({ type: 'uint8array' }),
|
||||
);
|
||||
const packageSha256 = (await import('node:crypto'))
|
||||
.createHash('sha256')
|
||||
.update(packageBytes)
|
||||
.digest('hex');
|
||||
|
||||
const coverAssetId = await uploadCover(author, stamp);
|
||||
const metadata = {
|
||||
title,
|
||||
summary: '网页链路 E2E 临时游戏',
|
||||
description: '',
|
||||
category: '休闲',
|
||||
tags: ['e2e'],
|
||||
coverAssetId,
|
||||
deviceSupport: { desktop: true, mobile: false, touch: false },
|
||||
inputModes: ['keyboard', 'mouse'],
|
||||
orientation: 'landscape',
|
||||
};
|
||||
|
||||
const created = await api('/api/game-distribution/games', {
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `web-e2e-game-${stamp}` },
|
||||
body: metadata,
|
||||
});
|
||||
const gameId = created.data?.id;
|
||||
if (created.status !== 200 || !gameId) {
|
||||
check('创建游戏成功', false, `status=${created.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versionResponse = await api(
|
||||
`/api/game-distribution/games/${gameId}/versions`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `web-e2e-version-${stamp}` },
|
||||
body: {
|
||||
packageSha256,
|
||||
packageBytes: packageBytes.length,
|
||||
packageFileCount: 2,
|
||||
packageEntryPath: 'index.html',
|
||||
gameMetadata: metadata,
|
||||
},
|
||||
},
|
||||
);
|
||||
const versionId = versionResponse.data?.versionId;
|
||||
const uploaded = await api(
|
||||
`/api/game-distribution/versions/${versionId}/package`,
|
||||
{
|
||||
method: 'PUT',
|
||||
token: author,
|
||||
headers: {
|
||||
'Idempotency-Key': `web-e2e-upload-${stamp}`,
|
||||
'Content-Type': 'application/zip',
|
||||
},
|
||||
binary: packageBytes,
|
||||
},
|
||||
);
|
||||
const submitted = await api(
|
||||
`/api/game-distribution/versions/${versionId}/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: author,
|
||||
headers: { 'Idempotency-Key': `web-e2e-submit-${stamp}` },
|
||||
body: { expectedPublicationRevision: 0 },
|
||||
},
|
||||
);
|
||||
const approved = await api(
|
||||
`/admin/api/game-distribution/versions/${versionId}/review`,
|
||||
{
|
||||
method: 'POST',
|
||||
token: admin,
|
||||
headers: { 'Idempotency-Key': `web-e2e-approve-${stamp}` },
|
||||
body: { decision: 'approve', expectedPublicationRevision: 0 },
|
||||
},
|
||||
);
|
||||
check(
|
||||
'真实游戏已发布(上传 → 送审 → 审核通过)',
|
||||
uploaded.status === 200 &&
|
||||
submitted.status === 202 &&
|
||||
approved.status === 200,
|
||||
`upload=${uploaded.status} submit=${submitted.status} approve=${approved.status}`,
|
||||
);
|
||||
return { gameId, admin };
|
||||
}
|
||||
|
||||
async function loadPlaywright() {
|
||||
const dir = (process.env.E2E_PLAYWRIGHT_DIR ?? '').trim();
|
||||
if (!dir) return import('playwright');
|
||||
const requireFromDir = createRequire(path.join(dir, 'noop.js'));
|
||||
return requireFromDir('playwright');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stamp = Date.now();
|
||||
const title = `网页链路 ${String(stamp).slice(-6)}`;
|
||||
const { gameId } = await publishFixtureGame(stamp, title);
|
||||
|
||||
const { chromium } = await loadPlaywright();
|
||||
const executablePath = (process.env.E2E_CHROMIUM_EXECUTABLE ?? '').trim();
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(executablePath ? { executablePath } : {}),
|
||||
});
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
// 平台壳层会同时挂载桌面/移动两套视图,断言一律只看可见的那一份。
|
||||
const card = page.locator('button.game-card:visible', { hasText: title });
|
||||
// 平台顶栏也有一个全局搜索框,这里只取游戏目录自己的筛选输入。
|
||||
const searchInput = page.locator('label.game-search input');
|
||||
|
||||
await page.goto(`${WEB}/games`, { waitUntil: 'domcontentloaded' });
|
||||
await card.waitFor({ state: 'visible', timeout: 20_000 });
|
||||
check('游客目录用真实数据显示已发布游戏', await card.isVisible());
|
||||
|
||||
await searchInput.fill(String(stamp).slice(-6));
|
||||
await page.waitForTimeout(300);
|
||||
check('关键词筛选命中该游戏', await card.isVisible());
|
||||
await searchInput.fill('zzz-没有这个游戏');
|
||||
await page.waitForTimeout(300);
|
||||
check(
|
||||
'无结果时给出空态与重置入口',
|
||||
await page.getByText('还没有找到对应游戏').first().isVisible(),
|
||||
'',
|
||||
);
|
||||
await page
|
||||
.locator('button:visible', { hasText: '重置筛选' })
|
||||
.first()
|
||||
.click();
|
||||
await page.waitForTimeout(300);
|
||||
check('重置筛选后回到完整目录', await card.isVisible());
|
||||
|
||||
const categoryTabs = page
|
||||
.locator('[role="tablist"][aria-label="游戏分类"]:visible')
|
||||
.first();
|
||||
await categoryTabs.getByRole('tab', { name: '策略' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
check('切换分类后该游戏被过滤掉', (await card.count()) === 0);
|
||||
await categoryTabs.getByRole('tab', { name: '休闲' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
check('回到该游戏分类后重新可见', await card.isVisible());
|
||||
|
||||
const deviceTabs = page
|
||||
.locator('[role="tablist"][aria-label="设备筛选"]:visible')
|
||||
.first();
|
||||
await deviceTabs.getByRole('tab', { name: '移动' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
check('只声明桌面端的游戏在移动筛选下不出现', (await card.count()) === 0);
|
||||
await deviceTabs.getByRole('tab', { name: '桌面' }).click();
|
||||
await page.waitForTimeout(300);
|
||||
check('桌面筛选下重新可见', await card.isVisible());
|
||||
|
||||
await card.click();
|
||||
await page.waitForURL(/\/games\/detail\?/, { timeout: 15_000 });
|
||||
const playEntryButton = page.getByRole('button', { name: '立即玩' });
|
||||
await playEntryButton.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
check(
|
||||
'详情页路由带游戏编号且展示主行动作',
|
||||
page.url().includes(`id=${gameId}`),
|
||||
`url=${page.url()}`,
|
||||
);
|
||||
|
||||
await playEntryButton.click();
|
||||
await page.waitForURL(/\/games\/play\?/, { timeout: 15_000 });
|
||||
const startButton = page.getByRole('button', { name: '开始游戏' });
|
||||
await startButton.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
check(
|
||||
'游玩页先给开始面板、此时还没有 iframe',
|
||||
page.url().includes(`id=${gameId}`) &&
|
||||
(await page.locator('iframe.game-player-frame').count()) === 0,
|
||||
`url=${page.url()}`,
|
||||
);
|
||||
|
||||
await startButton.click();
|
||||
const frame = page.locator('iframe.game-player-frame');
|
||||
await frame.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
const frameSrc = (await frame.getAttribute('src')) ?? '';
|
||||
check(
|
||||
'开始后才挂载发行 iframe(opaque sandbox + 平台同源入口)',
|
||||
new RegExp(`/games/${gameId}/?$`).test(frameSrc) &&
|
||||
(await frame.getAttribute('sandbox')) === 'allow-scripts',
|
||||
`src=${frameSrc.slice(0, 80)}`,
|
||||
);
|
||||
const frameBodyLocator = page
|
||||
.frameLocator('iframe.game-player-frame')
|
||||
.locator('body');
|
||||
let frameBody = '';
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
frameBody = (await frameBodyLocator.innerText().catch(() => '')).trim();
|
||||
if (frameBody.includes('WEB-E2E-OK')) break;
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
check(
|
||||
'iframe 真正载入发行包内容(不是 SPA 外壳)',
|
||||
frameBody.includes('WEB-E2E-OK'),
|
||||
`body=${frameBody.slice(0, 60)}`,
|
||||
);
|
||||
|
||||
await page.locator('button.game-back-button').click();
|
||||
await page.waitForURL(/\/games\/detail\?/, { timeout: 15_000 });
|
||||
check(
|
||||
'退出游玩回到详情且不保留运行中的 iframe',
|
||||
(await page.locator('iframe.game-player-frame').count()) === 0,
|
||||
`url=${page.url()}`,
|
||||
);
|
||||
|
||||
// 分享直达详情:直接打开详情深链也必须能拿到主动作。
|
||||
const deepLinkPage = await context.newPage();
|
||||
await deepLinkPage.goto(`${WEB}/games/detail?id=${gameId}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await deepLinkPage
|
||||
.getByRole('button', { name: '立即玩' })
|
||||
.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
check('分享/直达详情链接可用', true, `url=${deepLinkPage.url()}`);
|
||||
await deepLinkPage.close();
|
||||
|
||||
// 返回目录要恢复之前的搜索条件。
|
||||
await page.goto(`${WEB}/games`, { waitUntil: 'domcontentloaded' });
|
||||
await card.waitFor({ state: 'visible', timeout: 20_000 });
|
||||
const keywordText = String(stamp).slice(-6);
|
||||
await searchInput.fill(keywordText);
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button.game-card:visible').first().click();
|
||||
await page.waitForTimeout(1_500);
|
||||
const navigatedToDetail = /\/games\/detail\?/.test(page.url());
|
||||
check(
|
||||
'带筛选条件时仍能从目录进入详情',
|
||||
navigatedToDetail,
|
||||
`url=${page.url()}`,
|
||||
);
|
||||
if (navigatedToDetail) {
|
||||
await page.locator('button.game-back-button').click();
|
||||
await page.waitForTimeout(1_000);
|
||||
const restoredKeyword = await searchInput.inputValue();
|
||||
check(
|
||||
'从详情返回目录时恢复之前的搜索条件',
|
||||
restoredKeyword === keywordText && (await card.isVisible()),
|
||||
`keyword=${restoredKeyword} url=${page.url()}`,
|
||||
);
|
||||
} else {
|
||||
check('从详情返回目录时恢复之前的搜索条件', false, '未进入详情');
|
||||
}
|
||||
|
||||
// 加载失败可重试:第一次详情请求失败,重试后恢复。
|
||||
const retryPage = await context.newPage();
|
||||
let failedOnce = false;
|
||||
await retryPage.route(
|
||||
`**/api/game-distribution/games/${gameId}`,
|
||||
(route) => {
|
||||
if (!failedOnce) {
|
||||
failedOnce = true;
|
||||
route.abort();
|
||||
return;
|
||||
}
|
||||
route.continue();
|
||||
},
|
||||
);
|
||||
await retryPage.goto(`${WEB}/games/play?id=${gameId}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
const retryButton = retryPage.getByRole('button', { name: '重新加载' });
|
||||
await retryButton.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
check('游玩页加载失败时给出显式重试入口', await retryButton.isVisible());
|
||||
await retryButton.click();
|
||||
await retryPage
|
||||
.getByRole('button', { name: '开始游戏' })
|
||||
.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
check('重试后回到可开始状态', true);
|
||||
await retryPage.close();
|
||||
|
||||
// 移动端:桌面端游戏要有明确门槛,横屏游戏在竖屏下给旋转提示,且不横向溢出。
|
||||
const mobile = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const mobilePage = await mobile.newPage();
|
||||
await mobilePage.goto(`${WEB}/games`, { waitUntil: 'domcontentloaded' });
|
||||
await mobilePage
|
||||
.locator('label.game-search input')
|
||||
.waitFor({ state: 'visible' });
|
||||
const overflow = await mobilePage.evaluate(
|
||||
() => document.documentElement.scrollWidth - window.innerWidth,
|
||||
);
|
||||
check(
|
||||
'移动端目录可用且没有整页横向溢出',
|
||||
overflow <= 1,
|
||||
`overflow=${overflow}px`,
|
||||
);
|
||||
await mobilePage.goto(`${WEB}/games/play?id=${gameId}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await mobilePage
|
||||
.getByText('请在电脑上游玩')
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
const mobileStartButton = mobilePage.getByRole('button', {
|
||||
name: '开始游戏',
|
||||
});
|
||||
await mobileStartButton.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
const mobileStartDisabled = await mobileStartButton.isDisabled();
|
||||
const rotationHint = await mobilePage
|
||||
.getByText('这款游戏以横屏设计,旋转设备即可获得完整画面。')
|
||||
.isVisible();
|
||||
check(
|
||||
'移动端对不支持的游戏给出明确门槛与旋转提示',
|
||||
mobileStartDisabled && rotationHint,
|
||||
`startDisabled=${mobileStartDisabled} rotationHint=${rotationHint}`,
|
||||
);
|
||||
await mobile.close();
|
||||
await context.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log(`\n${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[check-game-distribution-web-e2e] 运行失败:${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user