/* Deeper E2E: pixels, HUD updates, placement ghost, wave banner. */ 'use strict'; const { chromium } = require('/tmp/node_modules/playwright-core'); (async () => { const browser = await chromium.launch({ executablePath: '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell', args: ['--no-sandbox', '--disable-gpu'], }); const page = await browser.newPage({ viewport: { width: 1440, height: 860 } }); const errors = []; page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); let fails = 0; const ok = (c, m) => { if (c) console.log(' ✔ ' + m); else { fails++; console.error(' ✘ ' + m); } }; await page.goto('http://127.0.0.1:8933/', { waitUntil: 'load' }); await page.waitForTimeout(800); await page.click('[data-diff="normal"]'); await page.waitForTimeout(2000); // keep the test window peaceful — wave destruction timing would make the // economy assertions flaky (this suite tests UI/HUD, not combat) await page.evaluate(() => { RTS.sim.state().waves.forEach(w => { w.day += 40; }); }); // ---- canvas pixel richness ---- const pix = await page.evaluate(() => { const cv = document.getElementById('game'); const ctx = cv.getContext('2d'); const img = ctx.getImageData(0, 0, cv.width, cv.height).data; const colors = new Set(); let nonBlack = 0, total = 0; for (let i = 0; i < img.length; i += 40) { // sample const r = img[i], g = img[i + 1], b = img[i + 2]; colors.add((r >> 4) + ',' + (g >> 4) + ',' + (b >> 4)); if (r + g + b > 30) nonBlack++; total++; } return { uniqueColors: colors.size, nonBlackRatio: nonBlack / total }; }); ok(pix.uniqueColors > 60, 'canvas is richly colored (' + pix.uniqueColors + ' quantized colors)'); ok(pix.nonBlackRatio > 0.5, 'canvas mostly rendered (' + Math.round(pix.nonBlackRatio * 100) + '% non-dark)'); // ---- HUD present ---- const hud1 = await page.evaluate(() => ({ gold: document.querySelector('#res-gold .v').textContent, day: document.getElementById('daylabel').textContent, time: RTS.sim.state().time, })); ok(hud1.gold === '550', 'HUD shows starting gold (' + hud1.gold + ')'); ok(/Day 1/.test(hud1.day), 'day label renders (' + hud1.day + ')'); // sun/moon icon prefix // ---- placement via UI ---- await page.click('#palette .palcol:first-child .palbtn:first-child'); // house await page.waitForTimeout(150); const ghost = await page.evaluate(() => !!RTS.ui.placing && RTS.ui.placing.defId); ok(ghost === 'house', 'palette activates ghost placement (' + ghost + ')'); // find a valid tile near HQ on the right side of screen const spot = await page.evaluate(() => { const hq = RTS.sim.hq(); for (let r = 2; r < 8; r++) { for (let a = 0; a < 12; a++) { const x = Math.round(hq.x + r * Math.cos(a / 12 * 6.28)); const y = Math.round(hq.y + r * Math.sin(a / 12 * 6.28)); if (RTS.sim.canPlace('house', x, y).ok) { const sp = RTS.render.worldToScreen(x, y); return { sx: sp.x, sy: sp.y, x, y }; } } } return null; }); ok(spot, 'found valid house spot'); if (spot) { await page.mouse.click(spot.sx, spot.sy); await page.waitForTimeout(300); const placed = await page.evaluate(({ x, y }) => { const s = RTS.sim.state(); return s.buildings.some(b => b.defId === 'house' && b.x === x && b.y === y); }, { x: spot.x, y: spot.y }); ok(placed, 'house placed by clicking at (' + spot.x + ',' + spot.y + ')'); } await page.keyboard.press('Escape'); // ---- income flows into HUD once a house exists ---- await page.click('#topbtns .tbtn:nth-child(4)'); // 3x await page.waitForTimeout(9000); const hud2 = await page.evaluate(() => ({ gold: document.querySelector('#res-gold .v').textContent, rate: document.querySelector('#res-gold .r').textContent, pop: document.querySelector('#res-pop .v').textContent, })); ok(hud2.rate !== '+0.00/s' && hud2.rate.startsWith('+'), 'tax income shown in HUD (rate ' + hud2.rate + ', gold ' + hud2.gold + ')'); ok(hud2.pop !== '4/0', 'population grows under housing (' + hud2.pop + ')'); // ---- minimap: terrain lit + fog dark both present ---- const mm = await page.evaluate(() => { const cv = document.getElementById('minimap'); const d = cv.getContext('2d').getImageData(0, 0, cv.width, cv.height).data; let lit = 0, dark = 0; for (let i = 0; i < d.length; i += 40) { const g = d[i + 1]; if (g > 60) lit++; else if (g < 20) dark++; } return { lit, dark }; }); ok(mm.lit > 60 && mm.dark > 500, 'minimap renders terrain + fog (' + mm.lit + ' lit / ' + mm.dark + ' fog samples)'); // ---- run to first wave: restore real schedule, day 2 = 40s game time; 3x speed ---- await page.evaluate(() => { const s = RTS.sim.state(); s.waves.forEach(w => { w.day -= 40; w.warned = false; w.spawned = false; }); s.waveIdx = 0; s.warnT = 0; }); await page.click('#topbtns .tbtn:nth-child(4)'); // 3x let bannerSeen = false, waveSpawned = false; for (let i = 0; i < 40 && !waveSpawned; i++) { await page.waitForTimeout(1000); bannerSeen = bannerSeen || await page.evaluate(() => document.getElementById('wavebanner').style.display === 'flex'); waveSpawned = await page.evaluate(() => RTS.sim.state().waveIdx > 0); } ok(bannerSeen, 'wave warning banner appeared'); ok(waveSpawned, 'first wave spawned'); await page.screenshot({ path: '/root/dinorts/shots/06-wave.png' }); const waveState = await page.evaluate(() => { const s = RTS.sim.state(); return { dinos: s.dinos.length, kills: s.stats.kills, day: s.day, wave: s.waveIdx }; }); console.log(' wave state:', JSON.stringify(waveState)); console.log('console errors:', errors.length ? errors : 'none'); await browser.close(); if (errors.length || fails) { console.error('E2E FAILED: ' + fails + ' assertion(s), ' + errors.length + ' console errors'); process.exit(1); } console.log('E2E PASSED ✔'); })().catch(e => { console.error('TEST CRASH:', e); process.exit(1); });