Files
fishing-game/docs/BROWSER-TESTING.md
T
andrey 9914fe89a9 Тесты: починка orca-playtest (ломался с 622d203 — M5) + +13 проверок последних дополнений (48→61). Фикс: профиль Orca переживает localStorage между прогонами (test.js — свежий headless-профиль), а с M5 «фонарик» — не строка, а апгрейд-кнопка удилища 0 при 1–3 ур. → на грязном сейве (4 ур.) шаг 9c падал «no flask»; добавлен шаг 2b — сброс сейва через игру (🗑️ → «Сбросить» = freshSave()+persist+сброс тест-форсов) после error-хуков, без перезагрузки. New: 7c скины (🎨 рендер, покупка «Красный» 500 — списание/авто-экип, клик+состояние в одном eval), 7d локации (🗺️, покупка «Озёра» 100000 — авто-переход+тост+списание, возврат на Реку), 8' золото в дождь (пилюля 🌧️, ratio variantChance ×2.5, улов с SHINY_CHANCE=1 в дождь → 4 ачивки Этапа 22 разом: first_shiny+rain_variant+skin_first+lake_first — skin/lake выдаются на улове, checkAchievements в catchFish, не в лавке), 8b туман (пилюля 🌫️, ratio biteChance ×1.5, скрытие пилюли); +скрины orca-skins/orca-locations; ratio-чеки независимы от баланса/фазы суток (одна eval); реальная погода за прогон не стартует (gap ≥120 c). Доки: BROWSER-TESTING — строки 2b/7c/7d/8/8b в таблице, п.10 камешков (localStorage), пример вывода построчно под реальный прогон, 400/250→70 (d5212e9), 1.4→1.45. 61/61 live ×4 (в т.ч. из грязного сейва), 128/128 headless, exit 0
2026-09-07 11:32:01 +03:00

19 KiB
Raw Permalink Blame History

Браузерный плейтест через Orca CLI

tests/orca-playtest.js — плейтест игры в настоящем браузере: он открывает вкладку встроенного браузера Orca app, сбрасывает сейв (чистое состояние) и прогоняет полный геймплей (меню → бросок → клёв → подсечка → улов → лавка (дневник, 🎯, 🏅, 🎨 скины, 🗺️ локации) → золотая рыба в дождь + туман (M8) → near-miss → ночь → фонарик в лавке → босс-рыба) через Orca CLI и проверяет состояния, DOM, ачивки и консоль.

В отличие от puppeteer-теста (tests/test.js), здесь нет отдельного headless Chromium — управление идёт командами CLI против браузера, которым пользуются люди. Это ловит то, что headless пропускает: отличия среды Orca-браузера, троттлинг фоновых вкладок, реальные CLI-нюансы.

Когда запускать

  • Перед коммитом, рядом с остальными проверками (см. AGENTS.md, раздел «Проверка»):
    1. node --check на вынутый из index.html JS;
    2. cd tests && node test.js — главный рубеж (headless, всегда доступен);
    3. node tests/orca-playtest.js — браузерный плейтест (требует запущенный Orca app);
    4. node shots.js — скриншоты.
  • В отличие от puppeteer-теста, требует запущенный Orca app. Если Orca не запущен — скрипт честно падает на шаге 0 с понятной ошибкой; в таком случае шаг пропускается и не блокирует коммит (главный рубеж — tests/test.js).

Требования

Что Зачем Как проверить
Запущенный Orca app (desktop) Вкладка создаётся в его встроенном браузере orca-ide status --json → result.runtime.state === "ready"
orca-ide в PATH или переменная ORCA_CLI_COMMAND Скрипт вызывает CLI через process.env.ORCA_CLI_COMMAND || 'orca-ide' which orca-ide

На Linux голое имя orca может резолвиться в GNOME screen reader — НЕ используйте его; в терминалах Orca алиас orca работает, но скрипт намеренно берёт orca-ide (или переопределение через ORCA_CLI_COMMAND).

Запуск (из каталога репо):

node tests/orca-playtest.js     # скриншоты orca-*.png кладутся в CWD

Только встроенные модули Node (child_process, fs, path) — npm install не нужен.

Таблица шагов

# Шаг Что проверяется (ok())
0 status runtime готов; иначе «запусти Orca app» и exit 1
1 tab create --url file://…/index.html получился browserPageId
1b установка error-хуков (window.__errs, error + unhandledrejection) хуки стоят до старта геймплея (polling переживает eval-ошибки грузящейся страницы)
2 ожидание DOM меню (#menu) показано
2b сброс сейва: btnReset → btnResetYes (игра: 🗑️ → «Сбросить», save = freshSave() + persist) чистое состояние: save.coins === 0
3 клик «Играть» (.click() в DOM) state === "IDLE"
4 pointerdown на canvas → polling CHARGE → удержание ~0.7 c → pointerup CHARGE, затем WAIT (бросок)
5 biteTimer = 0.05 → polling BITE (через APPROACH) с расширением окна biteT = 5 в том же eval → скриншот → тап BITE, затем REEL (подсечка)
6 reel.d = 0.5 + удержание ~2 c → polling CARD (30 c) → скриншот CARD, в карточке имя вида и «+N»
7 «Отлично!» → «🛒» → polling #shop → скриншот → вкладка «📔» → скриншот → вкладка «🎯» → скриншот → вкладка «🏅» → скриншот → «×» лавка открыта/закрыта, дневник/квесты/достижения рендерятся
7c вкладка «🎨» → polling рендера → save.coins = 200000 (выдача) → покупка поплавка «Красный» (клик и чтение состояния в одном eval) группы «Лодка»/«Поплавок», списание (−500), авто-экипировка save.bobberSkin=1
7d вкладка «🗺️» → покупка «Озёра» (клик + состояние + тост в одном eval) → скриншот → возврат на «Реку» авто-переход save.location=1, тост «Добро пожаловать на Озёра!», списание (−100000), выбор купленной
8 forceWeather = "rain" → пилюля 🌧️ → ratio variantChance(gold) дождь/ясно (в одном eval) → SHINY_CHANCE = 1 → полный цикл → скриншот → проверка DOM → ачивки → SHINY_CHANCE = 0.03; forceWeather = null бейдж «ЗОЛОТАЯ» + gold-card; ×2,5 (M8); ачивки Этап 22 first_shiny+rain_variant (wxRainV=1)+skin_first+lake_first — выдаются НА УЛОВЕ (checkAchievements в catchFish, не в лавке)
8b forceWeather = "fog" → пилюля 🌫️ → ratio biteChance() туман/ясно (в одном eval) → снятие форса → polling скрытия пилюли клёв ×1,5 (M8), пилюля показ/скрытие
9 цикл до REEL → reel.d = reel.d0 * 1.45 + 2 → polling WAIT с чтением тоста в том же eval тост содержит «Сбежала»
10 setDayTime(0.85) с чтением dayPhase в том же eval → скриншот dayPhase === "night"
11 «🛒» → вкладка «🎣» → поиск «фонарик» в #shopBody в снастях есть апгрейд-фонарик (⬆ 70 🪙, пометка про ночь)
12 forceBoss = true + pickSpecies → осётр → полный цикл: REEL (старт боя: stateBoss, bossPhase=1) → 3 фазы (в каждой reel.d = 0.4 → SNAP со сбросом holding в том же eval → авто-REEL следующей фазы; фаза 3 → CARD) → скриншот → forceBoss = false в finally старт босс-боя, карточка после 3 фаз (stateBoss=false), имя «Осётр-БОСС» с бейджем
13 console --limit 100 нет сообщений уровня error
13b чтение window.__errs нет pageerror за прогон (JS-ошибки и unhandledrejection)
14 tab close (в finally, 2 попытки — close идемпотентен) вкладка закрыта при любом исходе

Итог — сводка ИТОГ: N/N и exit code: 0 все PASS, 1 есть FAIL, 2 краш сценария, 3 глобальный таймаут (~4 мин).

Пример вывода

PASS | Orca runtime готов
PASS | вкладка с игрой открыта | pageId=d94df318
PASS | меню показано
PASS | сброс сейва (чистое состояние: coins=0)
PASS | старт: state=IDLE
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | скриншот orca-bite.png
PASS | подсечка: state=REEL
PASS | улов: state=CARD
PASS | скриншот orca-card.png
PASS | карточка улова: имя вида и «+N» | {"hidden":false,"name":"Карась","val":"+2 🪙"}
PASS | лавка открыта (вкладка Снасти)
PASS | скриншот orca-shop.png
PASS | дневник показан
PASS | скриншот orca-journal.png
PASS | 🎯 вкладка: стрик и квесты дня
PASS | скриншот orca-rewards.png
PASS | 🏅 вкладка: список достижений
PASS | скриншот orca-ach.png
PASS | 🎨 вкладка: скины лодки и поплавка
PASS | скины: покупка «Красный» (500 🪙) — списание и авто-экипировка | {"base":199500,"coins":199500,"owned":true,"skin":1}
PASS | скриншот orca-skins.png
PASS | 🗺️ вкладка: локации (Река / Озёра)
PASS | локации: покупка «Озёра» (100000 🪙) — авто-переход, тост, списание | {"base":99500,"coins":99500,"loc":1,"owned":true,"toast":"Добро пожаловать на Озёра!"}
PASS | скриншот orca-locations.png
PASS | локации: возврат на Реку (выбор купленной)
PASS | лавка закрыта
PASS | погода rain: HUD-пилюля 🌧️
PASS | погода rain: variantChance(gold) ×2.5 (WEATHER.rain.variantMult) | 2.5
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | золотая рыба: улов получен (state=CARD)
PASS | скриншот orca-shiny.png
PASS | золотая рыба: бейдж «ЗОЛОТАЯ» и золотая карточка (gold-card) | {"badge":"ЗОЛОТАЯ","gold":true,"hidden":false}
PASS | ачивки Этап 22: first_shiny + rain_variant (улов в дождь) + skin_first + lake_first | {"lake":true,"rain":true,"shiny":true,"skin":true,"wx":1}
PASS | погода fog: HUD-пилюля 🌫️
PASS | погода fog: biteChance ×1.5 (WEATHER.fog.biteMult) | 1.5
PASS | погода снята: пилюля скрыта
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | near-miss: тост «Сбежала» после схода | Сбежала! 🐠 Окунь 0.2 кг
PASS | скриншот orca-escape.png
PASS | ночь: dayPhase=night при dayTime 0.85 | night
PASS | скриншот orca-night.png
PASS | лавка: фонарик — апгрейд 1 удилища (⬆ 70, пометка про ночь)
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | босс: старт боя (stateBoss=true, bossPhase=1, reel.boss) | {"b":true,"ph":1,"rb":true}
PASS | босс: карточка после 3 фаз (stateBoss=false)
PASS | скриншот orca-boss.png
PASS | босс: карточка «Осётр-БОСС» с бейджем 🏆 | Осётр-БОСС | 🏆 БОСС!
PASS | нет ошибок в консоли (console --limit 100)
PASS | нет pageerror за прогон (window.onerror / unhandledrejection) | []

===== ИТОГ: 61/61 =====

Подводные камни

  1. Троттлинг rAF в фоновой вкладке. Вкладка Orca обычно не на переднем плане → requestAnimationFrame троттлится → игровое время идёт медленнее реального (кадр = максимум 0.05 c игрового). Поэтому никаких фиксированных sleep в ожидании состояний: все ожидания — polling с таймаутом (waitCond/waitState, шаг ~300 мс, таймауты 10–30 с). Фиксированные sleep допустимы только как «удержание кнопки» (зарядка ~0.7 c, улов ~2 c).
  2. Окно подсечки короче CLI-roundtrip. Окно BITE — 0.35–0.8 с, а один CLI-вызов стоит ~0.3–0.5 с. Рецепт стабильного перехвата: biteTimer = 0.05 → polling, где в том же eval, что детекция BITE, сразу выставляется biteT = 5 (отдельный eval не успевает: на первопланарной вкладке игра идёт в реальном времени, и окно закроется до его применения) → СНАЧАЛА скриншот, потом тап.
  3. Цитирование: только execFile с массивом аргументов, без shell. JS-выражения содержат кавычки — при передаче одной строкой через shell (PTY) они ломаются. В скрипте все вызовы — child_process.execFile(ORCA, argsArray, …); никакого exec("orca-ide …").
  4. Pointer Events с уникальным pointerId. Игра слушает pointerdown на canvas и pointerup на window. Каждое взаимодействие (down/up-пара) получает свой pointerId (счётчик в скрипте) — так события не путаются с реальными.
  5. Тосты недолгие (1.6 с). Текст тоста near-miss читается в том же eval, что и ожидание WAIT (state === "WAIT" ? toast.textContent : "") — отдельный последующий вызов может опоздать. Скриншот orca-escape.png — best-effort (тост мог погаснуть); ok() ставится по тексту.
  6. CLI может transient-повиснуть. Наблюдались единичные зависания eval (~1 мин), проходящие при повторе. Поэтому orca() ретраит вызовы (3 раза, линейный бэкофф 0.5/1 c; повторы пишутся в stderr, чтобы не мешать stdout с PASS/FAIL), а execFile жжёт зависший процесс таймаутом 20 с. Исключение — tab create: ретраить нельзя (получится дубль вкладки), он идёт через orcaCall один раз. tab close в finally ретраится 2 раза — вызов идемпотентен.
  7. Эмодзи — квадраты без шрифта. Без font-noto-emoji эмодзи в скриншотах (и в окне Orca) рендерятся тофу. На скринах это ОК — проверка не по пикселям; для красивых скриншотов поставьте шрифт.
  8. eval возвращает объекты JSON-строкой. result.result для объекта/массива — двойная сериализация (строка-JSON); evalPage в скрипте декодирует. Притом строка-«MENU» парсится как есть.
  9. Босс-фазы: SNAP не сбрасывает reel.holding. onUp слушает только CHARGE/REEL — отпускание пальца в состоянии SNAP (босс-обрыв фазы) оставляет holding = true, и после авто-перехода в следующую фазу игра «втягивает» сама. Поэтому в шаге 12 сброс reel.holding = false делается в том же eval, что детекция SNAP (тот же приём, что biteT = 5 в п. 2). Сам босс-бой — цикл из 3 фаз с polling'ом (SNAP → авто-REEL → снова), forceCatch() тут не подходит: на фазах 1–2 вместо CARD будет босс-обрыв.
  10. Профиль Orca переживает localStorage между прогонами. В отличие от test.js (свежий headless-профиль в каждом запуске), встроенный браузер Orca помнит сейв (fishing-save-v1) — и прошлый прогресс ломает шаги, завязанные на чистое состояние: после M5 «фонарик» — не отдельная строка, а апгрейд-кнопка на удилище 0 (только при 1–3 ур.; на 4 ур. кнопки нет → шаг 11 падал «no flask»). Поэтому шаг 2b сбрасывает сейв через игру (🗑️ → «Сбросить» = save = freshSave() + persist + сброс всех тест-форсов) до старта геймплея, без перезагрузки страницы — error-хуки шага 1b остаются на месте.

Как добавить новый шаг

  1. В секции «Прогон» в нужном месте добавьте действия через хелперы (evalPage, waitCond/waitState, pointerDown/Up, clickByText, shot) — фиксированные sleep на ожидание состояний не писать (пункт 1 выше).
  2. После каждого шага — ok('название', условие, extra).
  3. Если шаг форсирует игру (например, SHINY_CHANCE = 1), возвращайте исходное значение в finally — иначе сломаете следующие циклы и сейв вкладки.
  4. Скриншот — shot('orca-<имя>.png'): файл уходит в CWD, не путать с shot-*.png из tests/shots.js.
  5. Обновите таблицу шагов в этом документе и число проверок в AGENTS.md, если менялось общее количество ok().

Справочник CLI (используемые команды)

Команда Что даёт (в --json)
orca-ide status --json result.runtime.state === "ready"
orca-ide tab create --url <url> --json result.browserPageId (дальше везде --page <id>)
orca-ide eval --expression <js> --page <id> --json result.result (объекты — JSON-строкой)
orca-ide screenshot --page <id> --json result.data = base64 PNG
orca-ide console --limit N --page <id> --json result.messages: [{text, type}], type: "error" — ошибки
orca-ide tab close --page <id> --json result.closed