Тесты: shots.js — ожидание отрисовки вместо sleep и выбор кадров через argv

This commit is contained in:
2026-09-01 18:39:52 +03:00
parent d65815ad95
commit 7e7e18cb23
2 changed files with 82 additions and 25 deletions
+3 -1
View File
@@ -60,4 +60,6 @@ node test.js # 54 проверки
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
Есть и браузерный плейтест «вживую» во встроенном браузере Orca: `node tests/orca-playtest.js` (из каталога репо, требует запущенный Orca app) — детали в [docs/BROWSER-TESTING.md](docs/BROWSER-TESTING.md).
Есть и браузерный плейтест «вживую» во встроенном браузере Orca: `node tests/orca-playtest.js` (из каталога репо, требует запущенный Orca app) — полный геймплей, проверка консоли и pageerror; детали в [docs/BROWSER-TESTING.md](docs/BROWSER-TESTING.md).
Скриншоты состояний — `tests/shots.js` (headless, запускать из каталога `tests/`, кадры пишутся в текущий каталог): без аргументов снимает все кадры; `node shots.js menu boss` — только выбранные (прогон всё равно полный, пропускается лишь сохранение остальных); `node shots.js --list` — список кадров. Свежие скрины для README копируются в `screenshots/`.
+79 -24
View File
@@ -1,10 +1,43 @@
'use strict';
/* Капчуры всех игровых состояний для визуальной проверки */
/* Капчуры игровых состояний для визуальной проверки.
*
* Запуск (из каталога tests/):
* node shots.js # все кадры
* node shots.js menu boss # только выбранные кадры (прогон всё равно полный,
* # пропускается только сохранение остальных файлов)
* node shots.js --list # список доступных кадров
*
* Кадры: menu charge wait bite reel card night boss shop journal reset
*
* Стабильность: вместо фиксированных sleep на ожидание UI — waitForFunction
* (DOM-состояние) и двойной requestAnimationFrame (кадр реально отрисован).
* Фиксированные sleep остаются только там, где это ВРЕМЯ ИГРЫ (удержание
* кнопки при зарядке/забросе), а не ожидание отрисовки.
*/
const puppeteer = require('puppeteer-core');
const path = require('path');
const GAME_URL = 'file://' + path.resolve(__dirname, '..', 'index.html');
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const FRAMES = ['menu', 'charge', 'wait', 'bite', 'reel', 'card', 'night', 'boss', 'shop', 'journal', 'reset'];
// Какие кадры снимать: argv (имена через пробел) или все.
let want;
const arg = process.argv[2];
if (arg === '--list') { console.log(FRAMES.join('\n')); process.exit(0); }
if (process.argv.length > 2) {
const asked = process.argv.slice(2);
const bad = asked.filter(a => !FRAMES.includes(a));
if (bad.length) {
console.error('Неизвестные кадры: ' + bad.join(', ') + '\nДоступны: ' + FRAMES.join(' '));
process.exit(2);
}
want = new Set(asked);
} else {
want = new Set(FRAMES);
}
async function waitState(page, st, timeout = 8000) {
const t0 = Date.now();
while (Date.now() - t0 < timeout) {
@@ -14,7 +47,22 @@ async function waitState(page, st, timeout = 8000) {
}
throw new Error('timeout waiting "' + st + '"');
}
// Ждём DOM-условие в странице (вместо sleep «на глаз»).
function waitFor(page, fn, timeout = 8000) {
return page.waitForFunction(fn, { timeout, polling: 50 });
}
// Ждём фактической отрисовки: два подряд requestAnimationFrame.
const waitPaint = page => page.evaluate(() =>
new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))));
// Скриншот только выбранного кадра.
async function snap(page, name) {
if (!want.has(name)) return;
await page.screenshot({ path: 'shot-' + name + '.png' });
console.log('shot-' + name + '.png');
}
(async () => {
console.log('кадры: ' + [...want].join(' '));
const browser = await puppeteer.launch({
executablePath: CHROMIUM, headless: true,
args: ['--no-sandbox', '--disable-gpu', '--mute-audio'],
@@ -22,43 +70,47 @@ async function waitState(page, st, timeout = 8000) {
});
const page = await browser.newPage();
await page.goto(GAME_URL, { waitUntil: 'load' });
await sleep(500);
await page.screenshot({ path: 'shot-menu.png' });
await waitFor(page, () => !document.getElementById('menu').classList.contains('hidden'));
await waitPaint(page);
await snap(page, 'menu');
await page.click('#btnPlay');
await page.mouse.move(195, 600);
await page.mouse.down();
await sleep(650);
await page.screenshot({ path: 'shot-charge.png' });
await sleep(650); // удержание — это игровое время зарядки, не ожидание отрисовки
await waitPaint(page);
await snap(page, 'charge');
await page.mouse.up();
await waitState(page, 'WAIT');
await page.screenshot({ path: 'shot-wait.png' });
await waitPaint(page);
await snap(page, 'wait');
await page.evaluate(() => { biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.screenshot({ path: 'shot-bite.png' });
await waitPaint(page);
await snap(page, 'bite');
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
// покажем фазу «тянет» для наглядности
await page.evaluate(() => { reel.phase = 'pull'; reel.phaseT = 5; reel.tension = 0.55 * rod().line; reel.d = reel.d0 * 0.55; });
await sleep(200);
await page.screenshot({ path: 'shot-reel.png' });
await waitPaint(page);
await snap(page, 'reel');
// доловим
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
await page.mouse.down();
await waitState(page, 'CARD', 5000);
await page.mouse.up();
await sleep(200);
await page.screenshot({ path: 'shot-card.png' });
await waitPaint(page);
await snap(page, 'card');
await page.click('#btnCardOk');
await waitState(page, 'IDLE');
// M3: ночь — dayTime 0.85 (фаза night): тёмное небо, луна, тёмная вода; поплавок в воде
await page.mouse.move(195, 600);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await page.mouse.down(); await sleep(450); await page.mouse.up(); // удержание заброса
await waitState(page, 'WAIT');
await page.evaluate(() => { setDayTime(0.85); biteTimer = 30; }); // ночь; клёв отложен, тост не нужен
await sleep(250);
await page.screenshot({ path: 'shot-night.png' });
await waitPaint(page);
await snap(page, 'night');
// M4: босс — forceBoss + осётр, полный цикл до босс-карточки
await page.click('#btnRecall');
@@ -70,7 +122,7 @@ async function waitState(page, st, timeout = 8000) {
forceBoss = true; SHINY_CHANCE = 0;
});
await page.mouse.move(195, 600);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await page.mouse.down(); await sleep(450); await page.mouse.up(); // удержание заброса
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
@@ -90,8 +142,8 @@ async function waitState(page, st, timeout = 8000) {
await page.mouse.up();
}
}
await sleep(200);
await page.screenshot({ path: 'shot-boss.png' });
await waitPaint(page);
await snap(page, 'boss');
await page.click('#btnCardOk');
await page.evaluate(() => {
forceBoss = false; SHINY_CHANCE = 0.03;
@@ -100,18 +152,21 @@ async function waitState(page, st, timeout = 8000) {
await page.evaluate(() => { save.coins = 10000; });
await page.click('#btnShop');
await sleep(200);
await page.screenshot({ path: 'shot-shop.png' });
await waitFor(page, () => !document.getElementById('shop').classList.contains('hidden'));
await waitPaint(page);
await snap(page, 'shop');
await page.click('#tabJournal');
await sleep(150);
await page.screenshot({ path: 'shot-journal.png' });
await waitFor(page, () => document.getElementById('shopBody').textContent.indexOf('Бросков') >= 0);
await waitPaint(page);
await snap(page, 'journal');
await page.click('#btnCloseShop');
// диалог подтверждения сброса (закрываем «Отменой» — прогресс не трогаем)
await page.click('#btnReset');
await sleep(200);
await page.screenshot({ path: 'shot-reset.png' });
await waitFor(page, () => !document.getElementById('resetDlg').classList.contains('hidden'));
await waitPaint(page);
await snap(page, 'reset');
await page.click('#btnResetNo');
await browser.close();
console.log('screenshots done');
console.log('screenshots done: ' + [...want].length + '/' + FRAMES.length);
process.exit(0);
})().catch(e => { console.error('CRASH:', e.message); process.exit(2); });