/* Browser smoke test: boots the game, starts a run, plays a few days * through the real UI, and fails on any console error or page crash. * * node scripts/smoke.mjs [url] */ import { chromium } from 'playwright-core'; const url = process.argv[2] ?? 'http://127.0.0.1:5199'; const exe = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell'; const browser = await chromium.launch({ executablePath: exe, args: ['--no-sandbox','--disable-gpu','--disable-dev-shm-usage'] }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); page.setDefaultTimeout(9000); const errors = []; page.on('console', msg => { if (msg.type() === 'error') errors.push('console: ' + msg.text()); }); page.on('pageerror', err => errors.push('pageerror: ' + err.message)); const step = async (name, fn) => { try { await fn(); console.log('ok ', name); } catch (e) { console.log('FAIL', name, '-', e.message.split('\n')[0]); errors.push(name + ': ' + e.message); } }; await step('load page', async () => { await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('.title-screen', { timeout: 10000 }); }); await step('new run dialog', async () => { await page.click('text=NEW RUN'); await page.waitForSelector('.seed-input'); }); await step('start run', async () => { await page.fill('.seed-input', '777001'); await page.click('text=BEGIN DAY 1'); await page.waitForSelector('.game-layout', { timeout: 10000 }); }); await step('HUD renders', async () => { await page.waitForFunction(() => document.querySelector('#hud-day')?.textContent?.includes('DAY')); const resCount = await page.locator('.res-chip').count(); if (resCount !== 7) throw new Error('expected 7 resource chips, got ' + resCount); }); await step('survivor list renders', async () => { const n = await page.locator('.surv-card:not(.dead)').count(); if (n < 3 || n > 4) throw new Error('unexpected survivor count ' + n); await page.locator('.surv-card:not(.dead)').first().click(); await page.waitForSelector('.sd-body'); await page.click('.modal-x'); }); await step('map tab renders', async () => { await page.click('[data-tab="map"]'); const entries = await page.locator('.map-entry').count(); if (entries < 2) throw new Error('map too empty'); // hover preview shouldn't crash await page.locator('.map-entry').nth(1).hover(); await page.click('[data-tab="log"]'); }); await step('scavenge action flow', async () => { await page.click('.action:has-text("SCAVENGE")'); await page.waitForSelector('.site-grid .site-card'); await page.locator('.site-card').first().click(); const rows = page.locator('.party-check .check-row'); await rows.first().locator('input').check(); if (await rows.count() > 1) await rows.nth(1).locator('input').check(); await page.click('text=HEAD OUT'); await page.waitForSelector('.report, .enc-modal', { timeout: 8000 }); // resolve possible encounter for (let i = 0; i < 6; i++) { const enc = page.locator('.enc-modal .btn.choice'); if (await enc.count()) { await enc.first().click(); await page.waitForTimeout(300); continue; } const cont = page.locator('.modal-card .btn:has-text("CONTINUE")'); if (await cont.count()) { await cont.first().click(); break; } break; } await page.waitForTimeout(400); }); // play several days via END DAY until an event modal appears and handle choices for (let d = 0; d < 8; d++) { await step(`day cycle ${d + 1}`, async () => { // close any open report/event by clicking CONTINUE / first choice repeatedly for (let i = 0; i < 10; i++) { const choice = page.locator('.ev-choice:not(.locked)'); const cont = page.locator('.modal-card .btn:has-text("CONTINUE")'); const encBtns = page.locator('.enc-modal .btn.choice'); if (await encBtns.count()) { await encBtns.first().click(); await page.waitForTimeout(250); continue; } if (await choice.count()) { await choice.first().click(); await page.waitForTimeout(250); continue; } if (await cont.count()) { await cont.first().click(); await page.waitForTimeout(250); continue; } break; } const endBtn = page.locator('#btn-endday'); if (await endBtn.isEnabled()) await endBtn.click(); await page.waitForTimeout(500); for (let i = 0; i < 12; i++) { const cont = page.locator('.modal-card .btn:has-text("CONTINUE")'); const encBtns = page.locator('.enc-modal .btn.choice'); const evChoice = page.locator('.ev-choice:not(.locked)'); if (await encBtns.count()) { await encBtns.first().click(); await page.waitForTimeout(250); continue; } if (await cont.count()) { await cont.first().click(); await page.waitForTimeout(350); continue; } if (await evChoice.count()) { await evChoice.first().click(); await page.waitForTimeout(250); continue; } break; } }); } await step('save to slot', async () => { await page.click('#btn-menu'); await page.click('text=SAVE SLOT 1'); await page.waitForTimeout(300); }); await step('reload + continue via load modal', async () => { await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.title-screen'); await page.click('text=LOAD RUN'); await page.waitForSelector('.save-row'); await page.locator('.save-row .btn:has-text("LOAD")').first().click(); await page.waitForSelector('.game-layout', { timeout: 8000 }); }); console.log('\n---- RESULT ----'); if (errors.length) { console.log(errors.length + ' ERRORS:'); for (const e of errors.slice(0, 20)) console.log(' -', e); process.exitCode = 1; } else { console.log('SMOKE TEST PASSED — no console/page errors across boot, play, save, load.'); } await browser.close();