Files
nl-game/electron/mario-verify.cjs
2026-10-11 09:09:28 +08:00

189 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 马里奥专项验证(开发用临时脚本):
// 1. 登录 → 解锁全部关卡(API)→ 1-1 开局:横幅/问号居中/跑动截图
// 2. 依次冒烟 2-1/2-2/2-3/2-4:开局跑动,检查循环异常与 AudioContext
// 运行:NLG_E2E_MODE=web npx electron electron/mario-verify.cjs(需 5173 开发服务器)
const { app, BrowserWindow } = require('electron')
const path = require('path')
const fs = require('fs')
const WEB_URL = 'http://localhost:5173'
const OUT_DIR = process.cwd()
const pageErrors = []
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function main() {
const win = new BrowserWindow({
width: 1280,
height: 860,
show: true,
backgroundColor: '#0f1220',
webPreferences: { backgroundThrottling: false },
})
win.webContents.setBackgroundThrottling(false)
win.webContents.on('console-message', (e) => {
if (e?.level === 'error' && e?.message) pageErrors.push(e.message)
})
const js = (code) => win.webContents.executeJavaScript(code, true).catch((e) => {
console.error('[verify] JS 执行失败片段:', JSON.stringify(code.slice(0, 100).replace(/\s+/g, ' ')))
throw e
})
const shot = async (name) => {
for (let i = 0; i < 3; i++) {
try {
const img = await win.webContents.capturePage()
const file = path.join(OUT_DIR, `${name}.png`)
fs.writeFileSync(file, img.toPNG())
console.log('[verify] screenshot:', file)
return
} catch (err) {
if (i === 2) console.warn(`[verify] 截图 ${name} 失败:${err.message}`)
else await sleep(600)
}
}
}
const waitFor = async (cond, timeoutMs, desc) => {
const t0 = Date.now()
while (Date.now() - t0 < timeoutMs) {
try { if (await js(cond)) return } catch {}
await sleep(300)
}
throw new Error(`等待超时:${desc}`)
}
const runRight = async (ms) => {
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); true`)
await sleep(ms)
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); true`)
}
await win.loadURL(WEB_URL)
await js('localStorage.clear(); true')
await win.webContents.reload()
await waitFor(`!!document.querySelector('input[placeholder^="用户名"]')`, 10000, '登录页渲染')
await js(`
(() => {
const us = document.querySelector('input[placeholder^="用户名"]')
us.value = 'mariotest01'; us.dispatchEvent(new Event('input', { bubbles: true }))
const ps = document.querySelector('input[type="password"]')
ps.value = 'test123456'; ps.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('button.submit').click()
return true
})()
`)
await waitFor(`!!localStorage.getItem('token')`, 8000, '登录')
console.log('[verify] 登录成功')
// 解锁全部 8 关(后端防作弊每次最多 +1,循环推进)
await js(`
(async () => {
const H = { Authorization: 'Bearer ' + localStorage.getItem('token'), 'Content-Type': 'application/json' }
for (let lv = 2; lv <= 12; lv++) {
for (let t = 0; t < 3; t++) {
try {
const r = await fetch('/api/games/mario/progress', {
method: 'POST', headers: H, body: JSON.stringify({ level: lv }),
})
const j = await r.json()
if (j && j.code === 0) break
} catch {}
await new Promise((r2) => setTimeout(r2, 400))
}
}
const g = await fetch('/api/games/mario/progress', { headers: { Authorization: H.Authorization } }).then((r) => r.json())
return (g.data || {}).level
})()
`)
await sleep(800)
const unlockedLevel = await js(`fetch('/api/games/mario/progress', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }).then((r) => r.json()).then((j) => (j.data || {}).level).catch(() => 0)`)
console.log('[verify] 解锁进度:', unlockedLevel)
const playLevel = async (label, idx, runMs) => {
await js(`location.href = '/play/mario'; true`).catch(() => {})
try {
await waitFor(`!![...document.querySelectorAll('button')].find((b) => /开始闯关|继续第|开始游戏/.test(b.textContent))`, 20000, `${label} 游玩页`)
} catch (e) {
await shot(`mario-verify-debug-${idx}`)
console.error('[verify] 页面状态:', JSON.stringify(await js(`({ url: location.href, btns: [...document.querySelectorAll('button')].map((b) => b.textContent.trim()).slice(0, 12) })`)))
throw e
}
// 开局前装监听 + 代理 AudioContext
await js(`
(() => {
window.__pageErrors = []
window.addEventListener('error', (e) => window.__pageErrors.push(String(e.message || e)))
window.addEventListener('unhandledrejection', (e) => window.__pageErrors.push(String(e.reason)))
const Orig = window.AudioContext || window.webkitAudioContext
window.AudioContext = class extends Orig {
constructor(...a) { super(...a); (window.__actxs = window.__actxs || []).push(this) }
}
// 选择关卡(若按钮可用)再开始
const btns = [...document.querySelectorAll('button')]
const lv = btns.find((b) => b.textContent.includes('世界 ${label}') && !b.disabled)
if (lv) lv.click()
return true
})()
`)
await sleep(300)
await js(`
(() => {
const b = document.querySelector('.overlay .ls-continue') ||
[...document.querySelectorAll('button')].find((x) => /开始闯关|继续第|开始游戏/.test(x.textContent))
if (!b) return false
b.click()
return true
})()
`)
await sleep(900)
if (idx === 'a') await shot('mario-verify-select') // 世界选关画面
// 光标默认停在最新解锁关(11=3-4),用右移导航到目标关:(i+1)%12 次到 i
const target = { a: 0, b: 4, c: 8, d: 9, e: 10, f: 11 }[idx] ?? 11
const presses = (target + 1) % 12
for (let i = 0; i < presses; i++) {
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); true`)
await sleep(50)
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); true`)
await sleep(60)
}
await js(`window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); true`)
await sleep(80)
await js(`window.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' })); true`)
await sleep(3400) // 横幅 1.6s + 余量
await runRight(runMs)
await sleep(1600)
const report = await js(`({
marioErr: window.__marioErr ?? null,
pageErrors: window.__pageErrors,
audio: (window.__actxs || []).map((a) => a.state),
level: window.__marioLevel ?? null,
})`)
const wantLevel = { a: '1-1', b: '2-1', c: '3-1', d: '3-2', e: '3-3', f: '3-4' }[idx]
if (report.level !== wantLevel) {
console.error(`[verify] ${label} 未进入目标关:期望 ${wantLevel},实际 ${report.level}`)
report.marioErr = report.marioErr || 'level mismatch'
}
console.log(`[verify] ${label} report:`, JSON.stringify(report))
await shot(`mario-verify-${idx}-${label}`)
return report
}
const reports = []
reports.push(await playLevel('1-1', 'a', 1400))
for (const [label, idx, runMs] of [['2-1', 'b', 3200], ['3-1', 'c', 3000], ['3-2', 'd', 2600], ['3-3', 'e', 2400], ['3-4', 'f', 2400]]) {
reports.push(await playLevel(label, idx, runMs))
}
const bad = reports.filter((r) => r.marioErr || r.pageErrors.length)
const audioOk = reports.filter((r) => r.audio.includes('running')).length >= 2
if (pageErrors.length) console.error('[verify] 页面 console 错误:', pageErrors.slice(0, 8))
if (bad.length === 0 && audioOk && pageErrors.length === 0) {
console.log('[verify] PASS:五关全部正常,无循环异常,BGM 引擎运行')
} else {
console.error('[verify] FAIL', JSON.stringify({ bad, audioOk }))
app.exit(1)
return
}
app.exit(0)
}
app.whenReady().then(() => main().catch((err) => { console.error('[verify] 失败:', err.message); app.exit(1) }))
setTimeout(() => { console.error('[verify] 总超时'); app.exit(1) }, 180000)