import puppeteer from 'puppeteer'; 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', '--disable-renderer-backgrounding', '--disable-background-timer-throttling'], }); const page = await browser.newPage(); await page.setViewport({ width: 1024, height: 600 }); const errors = []; page.on('pageerror', (e) => errors.push(e.message.slice(0, 160))); await page.goto('http://127.0.0.1:4940', { waitUntil: 'networkidle2', timeout: 90000 }); await new Promise(r => setTimeout(r, 4000)); await page.screenshot({ path: '/tmp/s1_menu.png' }); await page.evaluate(() => document.getElementById('m-new').click()); await new Promise(r => setTimeout(r, 400)); await page.evaluate(() => { const i = document.querySelector('#wiz-name'); i.value = 'Test Shop'; document.querySelector('#wiz-start').click(); }); await new Promise(r => setTimeout(r, 1500)); await page.screenshot({ path: '/tmp/s2_game.png' }); // ---- run a profitable day deterministically ---- const day1 = await page.evaluate(async () => { const G = window.__TS.G; const eco = window.__TS.eco; const ai = window.__TS.ai; const dc = window.__TS.day; const stf = window.__TS.staff; const shelves = G.data.furniture.filter(f => f.type === 'shelf'); eco.buyFromSupplier('baker', 'bread', 6); eco.buyFromSupplier('farmer', 'apple', 6); eco.buyFromSupplier('smith', 'potion', 0); // invalid catalog — should not crash eco.stockShelf(shelves[0], 'bread', 6); eco.stockShelf(shelves[0], 'apple', 6); // hire a cashier to prove staff flow G.data.candidates.push({ id: 'testcash', name: 'Tessa', role: 'cashier', skill: 70, traits: ['friendly'], salary: 40, loyalty: 65 }); stf.hireCandidate('testcash'); 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) { dc.updateDay(0.005); ai.spawnTimerTick(0.5); for (const c of [...G.customers]) ai.updateCustomer(c, 0.35); } return { served: G.data.today.served, revenue: Math.round(G.data.today.revenue), staff: G.data.staff.length, }; }); console.log('DAY1:', JSON.stringify(day1)); // 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, hist: window.__TS.G.data.history.revenue.length }), 400); }, 500); }, 400); })); console.log('FLOW:', JSON.stringify(flow)); // ---- panel smoke tests ---- const panels = {}; for (const id of ['shop', 'inventory', 'suppliers', 'staff', 'town', 'quests', 'analytics', 'goals']) { panels[id] = await page.evaluate((pid) => new Promise(res => { try { document.querySelector(`.nav-btn[data-panel="${pid}"]`).click(); setTimeout(() => { const body = document.querySelector('.side-panel .sp-body'); res({ ok: !!body && body.children.length > 0, len: body?.innerHTML.length || 0 }); }, 350); } catch (e) { res({ error: e.message }); } }), id); } console.log('PANELS:', JSON.stringify(panels)); // negotiation modal const neg = await page.evaluate(() => new Promise(res => { document.querySelector('.nav-btn[data-panel="suppliers"]').click(); setTimeout(() => { document.querySelector('[data-neg="farmer"]')?.click(); setTimeout(() => { const visible = !document.getElementById('modal-root').classList.contains('hidden'); const btns = [...document.querySelectorAll('.choice-btn')].length; // try "pay immediately" (always succeeds) [...document.querySelectorAll('.choice-btn')].find(b => b.textContent.includes('Pay immediately'))?.click(); setTimeout(() => res({ visible, btns, rel: window.__TS.G.supplierRel('farmer') }), 300); }, 300); }, 400); })); console.log('NEGOTIATION:', JSON.stringify(neg)); await page.evaluate(() => { document.querySelector('.sp-close')?.click(); document.querySelector('#neg-close')?.click(); }); // decorate mode: place a rug programmatically through UI path const decor = await page.evaluate(async () => { const G = window.__TS.G; window.__TS.emit('decorateStart'); await new Promise(r => setTimeout(r, 200)); // simulate placing a plant via data + refresh (placement raycast is pointer-driven) const before = G.data.furniture.length; G.addGold(-18); G.data.furniture.push({ id: 'testplant', type: 'plant', cx: 3, cz: 3, rot: 0 }); const ai = window.__TS.ai; ai.forceGridRebuild(); const { refreshAllFurniture } = await import('/src/gfx/furniture3d.js'); refreshAllFurniture(); window.__TS.emit('decorateDone'); return { added: G.data.furniture.length - before, gold: Math.round(G.gold) }; }); console.log('DECOR:', JSON.stringify(decor)); // save / load roundtrip const saveLoad = await page.evaluate(() => new Promise(res => { Promise.resolve(window.__TS.saveApi).then(({ saveTo, loadFrom }) => { saveTo('slot1'); const d = loadFrom('slot1'); res({ savedDay: d?.shop?.day, savedGold: Math.round(d?.shop?.gold), nameOk: !!d?.meta?.name }); }); })); console.log('SAVELOAD:', JSON.stringify(saveLoad)); await page.screenshot({ path: '/tmp/s3_after.png' }); console.log('PAGEERRORS:', errors.length ? errors.join(' | ') : 'none'); await browser.close();