189 lines
8.1 KiB
JavaScript
189 lines
8.1 KiB
JavaScript
|
|
// 马里奥专项验证(开发用临时脚本):
|
|||
|
|
// 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)
|