Files
tinyshop/scripts/e2e.mjs
T
deepseek 0e7c07533e TINY SHOP — cozy 3D shop-management game (Three.js vertical slice)
- Procedural 3D world: dollhouse shop, town, day/night, weather, seasons
- Customer AI with personalities (story NPCs, thieves, weekly regulars)
- Economy: suppliers, negotiation, pricing psychology, daily accounting
- Staff with traits/loyalty, 8 expansion levels, furniture & decoration
- Events with choices, quests, achievements, 4 difficulties, rival shop
- Animated daily report, analytics, save/load (3 slots + autosave)
- Procedural music & SFX (WebAudio), zero external assets
- Test harnesses: simtest (node), verify/check/e2e (headless browser)
2026-08-23 06:58:24 +00:00

134 lines
5.6 KiB
JavaScript

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();