// 通过 ego-browser nodejs < tools/browser-smoke.mjs 运行;仅访问本地测试页面。 // 空间编号由首次 browser QA 建立,同一任务一直复用该空间。 const space = Number(process.env.GARDEN_SPACE); if (!space || !process.env.GARDEN_QA_DIR) throw new Error('请设置 GARDEN_SPACE 与 GARDEN_QA_DIR,复用当前 QA 空间。'); const task = await taskSpace(space), page = task.page('p1'); await page.reload(); await page.waitForFunction(() => !!window.__garden); const fs = await import('node:fs/promises'); const root = process.env.GARDEN_QA_DIR; const report = { checks: [], levels: [] }; await page.evaluate(() => { window.__qaErrors = []; window.addEventListener('error', e => window.__qaErrors.push(e.message)); window.addEventListener('unhandledrejection', e => window.__qaErrors.push(String(e.reason))); }); const info = () => page.evaluate(() => { const g = window.__garden, s = g.snapshot(), t = g.transform(), r = document.getElementById('board').getBoundingClientRect(); return { snapshot: s, level: g.level(), hint: g.hint(), points: g.hint().map(id => { const b = s.balls.find(b => b.id === id); return { x: r.left + t.x + b.x * t.scale, y: r.top + t.y + b.y * t.scale }; }) }; }); const send = async (type, points) => page.cdp('Input.dispatchTouchEvent', { type, touchPoints: points.map((p, id) => ({...p, id, radiusX: 5, radiusY: 5, force: 1})) }); let initial = await info(); if (initial.level.id !== 1) throw new Error('QA expects an untouched campaign'); // 两颗松手、系统取消、回滑撤回均不得扣步。 await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchEnd', []); if ((await info()).snapshot.stats.merges !== 0) throw new Error('short gesture committed'); report.checks.push('两颗松手不扣步'); await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchCancel', []); if ((await info()).snapshot.stats.merges !== 0) throw new Error('cancel committed'); report.checks.push('触摸取消不提交'); await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchMove', [initial.points[2]]); await send('touchMove', [initial.points[1]]); await send('touchEnd', []); if ((await info()).snapshot.stats.merges !== 0) throw new Error('backtrack failed'); report.checks.push('回滑撤回'); await page.click('#sound'); await page.click('#hint'); await page.screenshot({path: `${root}/mobile-initial.png`}); for (let expected = 1; expected <= 10; expected++) { let data = await info(); if (data.level.id !== expected) throw new Error(`Expected L${expected}, got ${data.level.id}`); for (let turn = 0; turn < data.level.moves; turn++) { data = await info(); if (data.snapshot.status === 'won') break; if (data.snapshot.status !== 'playing') throw new Error(`L${expected} prematurely ${data.snapshot.status}`); const previous = data.snapshot.stats.merges; await send('touchStart', [data.points[0]]); for (const p of data.points.slice(1)) await send('touchMove', [p]); await send('touchEnd', []); await page.waitForFunction(previous => window.__garden.snapshot().stats.merges > previous && !window.__garden.animating(), previous, { timeout: 5000 }); } data = await info(); if (data.snapshot.status !== 'won') throw new Error(`L${expected} not won`); report.levels.push({ level: expected, moves: data.snapshot.stats.merges, score: data.snapshot.stats.score }); console.log(`触摸通关 L${expected} ${data.snapshot.stats.merges} 步 ${data.snapshot.stats.score} 分`); if (expected === 7) await page.screenshot({path: `${root}/mobile-result.png`}); if (expected < 10) { await page.click('[data-action="next"]'); if (expected === 7) await page.screenshot({path: `${root}/mobile-level8.png`}); } } await page.screenshot({path: `${root}/mobile-all-clear.png`}); report.errors = await page.evaluate(() => window.__qaErrors); if (report.errors.length) throw new Error(JSON.stringify(report.errors)); await fs.writeFile(`${root}/browser.json`, JSON.stringify(report, null, 2)); console.log(await page.snapshot());