import puppeteer from 'puppeteer'; const URL = 'http://127.0.0.1:4939'; const results = {}; const errors = []; async function runOnce(attempt) { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'], }); const page = await browser.newPage(); await page.setViewport({ width: 800, height: 520 }); page.on('pageerror', (e) => errors.push('[pe] ' + e.message.slice(0, 140))); try { await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 45000 }); await new Promise(r => setTimeout(r, 3500)); results.boot = 'PASS'; // menu screenshot try { await page.screenshot({ path: `/tmp/v_menu_${attempt}.png` }); results.shotMenu = 'PASS'; } catch { results.shotMenu = 'FAIL'; } // start game through UI await page.evaluate(() => document.getElementById('m-new').click()); await new Promise(r => setTimeout(r, 500)); await page.evaluate(() => { const i = document.querySelector('#wiz-name'); i.value = 'Verify Shop'; document.querySelector('#wiz-start').click(); }); // lighten GPU load for swiftshader await page.evaluate(() => { window.__TS.G.data.settings.quality = 'low'; }); await new Promise(r => setTimeout(r, 2200)); results.startGame = 'PASS'; // deterministic profitable day const day = await page.evaluate(async () => { const T = window.__TS, G = T.G; const shelves = G.data.furniture.filter(f => f.type === 'shelf'); T.eco.buyFromSupplier('baker', 'bread', 6); T.eco.buyFromSupplier('farmer', 'apple', 6); T.eco.stockShelf(shelves[0], 'bread', 6); T.eco.stockShelf(shelves[0], 'apple', 6); G.data.candidates.push({ id: 'tc', name: 'Tessa', role: 'cashier', skill: 72, traits: ['friendly'], salary: 40, loyalty: 66 }); T.staff.hireCandidate('tc'); G.shop.rep = 18; G.shop.minutes = 9 * 60 + 1; G.shop.isOpen = true; G.shop.phase = 'open'; let steps = 6000; while (steps-- > 0 && G.shop.isOpen) { T.day.updateDay(0.005); T.ai.spawnTimerTick(0.5); for (const c of [...G.customers]) T.ai.updateCustomer(c, 0.35); } return { served: G.data.today.served, revenue: Math.round(G.data.today.revenue), staff: G.data.staff.length }; }); results.simDay = day.served > 0 && day.revenue > 0 ? `PASS (${day.served} served, ${day.revenue}g)` : `FAIL ${JSON.stringify(day)}`; // close-day → report → next day const flow = await page.evaluate(() => new Promise(res => { window.__TS.emit('requestClose'); setTimeout(() => { document.getElementById('rc-yes')?.click(); setTimeout(() => { const reportVisible = !document.getElementById('modal-root').classList.contains('hidden'); const stars = document.querySelector('.r-stars')?.textContent.trim(); document.getElementById('next-day')?.click(); setTimeout(() => res({ reportVisible, stars, day2: window.__TS.G.shop.day }), 450); }, 450); }, 400); })); results.reportFlow = flow.reportVisible && flow.day2 === 2 ? `PASS (${flow.stars})` : `FAIL ${JSON.stringify(flow)}`; // panels const panelOk = []; for (const id of ['shop', 'inventory', 'suppliers', 'staff', 'town', 'quests', 'analytics', 'goals']) { const ok = await page.evaluate((pid) => new Promise(res => { document.querySelector(`.nav-btn[data-panel="${pid}"]`).click(); setTimeout(() => { const body = document.querySelector('.side-panel .sp-body'); res(!!body && body.innerHTML.length > 300); }, 300); }), id); panelOk.push(`${id}:${ok ? '✓' : '✗'}`); } results.panels = panelOk.join(' '); // negotiation const neg = await page.evaluate(() => new Promise(res => { document.querySelector('.nav-btn[data-panel="suppliers"]').click(); setTimeout(() => { document.querySelector('[data-neg="baker"]')?.click(); setTimeout(() => { const btns = [...document.querySelectorAll('.choice-btn')]; [...btns].find(b => b.textContent.includes('Pay immediately'))?.click(); setTimeout(() => res({ modal: btns.length >= 4 ? 'PASS' : 'FAIL' }), 250); }, 250); }, 350); })); results.negotiation = neg.modal; // decorate: place furniture via data path + visuals refresh const decor = await page.evaluate(() => { const G = window.__TS.G; const before = G.data.furniture.length; G.addGold(-30); G.data.furniture.push({ id: 'vplant', type: 'plant', cx: 3, cz: 3, rot: 0 }); window.__TS.ai.forceGridRebuild(); window.__TS.refreshFurniture(); return G.data.furniture.length === before + 1 ? 'PASS' : 'FAIL'; }); results.decorate = decor; // save/load roundtrip const sl = await page.evaluate(() => new Promise(res => { window.__TS.saveApi.saveTo('slot1'); const d = window.__TS.saveApi.loadFrom('slot1'); res(d && d.shop.day === 2 && d.meta.name === 'Verify Shop' ? 'PASS' : `FAIL ${d?.shop?.day}/${d?.meta?.name}`); })); results.saveLoad = sl; // achievements const ach = await page.evaluate(() => { const a = window.__TS.saveApi.getAchievements(); return Object.keys(a).length >= 1 ? `PASS (${Object.keys(a).length} unlocked)` : `FAIL ${Object.keys(a).length}`; }); results.achievements = ach; // gameplay screenshot at busy time try { await page.evaluate(() => { window.__TS.G.shop.minutes = 12 * 60; window.__TS.G.shop.isOpen = true; window.__TS.G.timeScale = 3; }); await new Promise(r => setTimeout(r, 2500)); await page.screenshot({ path: `/tmp/v_game_${attempt}.png` }); results.shotGame = 'PASS'; } catch { results.shotGame = 'FAIL'; } } catch (e) { results.exception = e.message.slice(0, 120); } finally { await browser.close().catch(() => {}); } } for (let attempt = 1; attempt <= 3; attempt++) { console.log(`\n=== attempt ${attempt} ===`); await runOnce(attempt); const done = ['simDay', 'reportFlow', 'saveLoad'].every(k => (results[k] || '').startsWith('PASS')) && (results.panels || '').includes('✓') && !(results.panels || '').includes('✗'); console.log(JSON.stringify(results, null, 1)); if (done && !results.exception) break; } console.log('\nPAGEERRORS:', errors.length ? errors.slice(-6).join(' | ') : 'none'); process.exit(0);