// Deep E2E: start sandbox, build a working town via __IA hook, fast-forward months, assert state. import { chromium } from 'playwright-core'; const exe = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell'; const errors = []; const browser = await chromium.launch({ executablePath: exe, args: ['--no-sandbox', '--disable-gpu'] }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); const BASE = process.env.BASE || 'http://127.0.0.1:5173/'; await page.goto(BASE, { waitUntil: 'networkidle' }); await page.waitForTimeout(800); await page.click('#mm-sandbox'); await page.waitForTimeout(1200); // Build town programmatically const result = await page.evaluate(() => { const I = window.__IA; const c = I.app.city; // find flat grass area let ox = -1, oy = -1; outer: for (let y = 8; y < c.H - 12; y++) for (let x = 4; x < c.W - 24; x++) { let ok = true, fertileNear = false; for (let dy = -2; dy < 9 && ok; dy++) for (let dx = -2; dx < 21; dx++) { const t = c.tiles[(y + dy) * c.W + (x + dx)]; if (!t || t.terrain === 'water') { ok = false; break; } if (t.terrain === 'fertile' && t.fert > 30) fertileNear = true; } if (ok && fertileNear) { ox = x; oy = y; break outer; } } if (ox < 0) return { fail: 'no spot' }; for (let i = 0; i < 20; i++) I.place('road', ox + i, oy); for (let i = 1; i < 19; i++) I.place('house', ox + i, oy - 1); I.place('well', ox + 2, oy + 1); I.place('market', ox + 4, oy + 1); I.place('granary', ox + 7, oy + 1); I.place('prefecture', ox + 11, oy + 1); I.place('forum', ox + 12, oy - 2) || I.place('forum', ox + 13, oy - 3); // farm directly below the road row -> top edge touches road => connected const fr = I.place('farmWheat', ox + 15, oy + 1); const farmPlaced = !!fr.ok; if (!farmPlaced) { for (const [fdx, fdy] of [[13, 1], [17, 1], [15, -4], [12, 1], [18, 1]]) { if (I.place('farmWheat', ox + fdx, oy + fdy).ok) { farmPlaced = true; break; } } } if (!farmPlaced) return { fail: 'no road-connected farm spot' }; return { ox, oy, funds: Math.round(c.funds), pop: c.population }; }); console.log('build result:', JSON.stringify(result)); if (result.fail) { console.log('BUILD FAILED'); process.exit(1); } // Fast-forward 30 months synchronously const ff = await page.evaluate(() => { const I = window.__IA; for (let i = 0; i < 30 * 20; i++) I.step(0.5); const c = I.app.city; const houses = [...c.buildings.values()].filter(b => b.def === 'house'); return { month: c.month, pop: c.population, funds: Math.round(c.funds), employed: c.employed, laborers: c.laborers, sentiment: c.sentiment, ratings: c.ratings, avgLevel: (houses.reduce((a, b) => a + b.houseLevel, 0) / Math.max(1, houses.length)).toFixed(2), walkers: c.walkers.size, granaryWheat: [...c.buildings.values()].filter(b => b.def === 'granary').reduce((a, b) => a + (b.inv.wheat ?? 0), 0), }; }); console.log('after 30mo:', JSON.stringify(ff)); // Now test spells & heroes UI presence const ui = await page.evaluate(() => ({ spellCount: document.querySelectorAll('#spellbar .spell').length, catTabs: document.querySelectorAll('#cattabs .cattab').length, minimap: !!document.getElementById('minimap'), topFunds: document.getElementById('t-funds')?.textContent, })); console.log('ui:', JSON.stringify(ui)); await page.screenshot({ path: '/tmp/shot_town.png' }); console.log('CONSOLE ERRORS:', errors.length); errors.slice(0, 10).forEach(e => console.log(' -', e.slice(0, 200))); const pass = ff.pop > 40 && ff.month >= 28 && ui.spellCount === 7 && errors.length === 0; console.log(pass ? '✅ E2E PASS' : '❌ E2E FAIL'); await browser.close(); process.exit(pass ? 0 : 1);