# Браузерный плейтест через Orca CLI `tests/orca-playtest.js` — плейтест игры **в настоящем браузере**: он открывает вкладку встроенного браузера Orca app, прогоняет полный геймплей (меню → бросок → клёв → подсечка → улов → лавка (дневник, 🎯, 🏅) → shiny → 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`). Запуск (из каталога репо): ```bash 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`) показано | | 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` → скриншот → вкладка «Дневник» → скриншот → вкладка «🎯» → скриншот → вкладка «🏅» → скриншот → «×» | лавка открыта/закрыта, дневник/квесты/достижения рендерятся | | 8 | `SHINY_CHANCE = 1` → полный цикл → скриншот → проверка DOM → `SHINY_CHANCE = 0.03` | бейдж «ШИНИ» показан, золотая карточка | | 9 | цикл до `REEL` → `reel.d = reel.d0 * 1.4 + 2` → polling `WAIT` **с чтением тоста в том же eval** | тост содержит «Сбежала» | | 10 | `setDayTime(0.85)` **с чтением `dayPhase` в том же eval** → скриншот | `dayPhase === "night"` | | 11 | «🛒» → вкладка «Снасти» → поиск «Фонарик» в `#shopBody` | в снастях есть «Фонарик» (500 🪙, ночь) | | 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 | старт: 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 | зарядка: state=CHARGE PASS | бросок: state=WAIT PASS | клёв: state=BITE (через APPROACH) PASS | подсечка: state=REEL PASS | шини: улов получен (state=CARD) PASS | скриншот orca-shiny.png PASS | шини: бейдж «ШИНИ» и золотая карточка | {"badge":"✨ ШИНИ!","gold":true,"hidden":false} 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 | лавка: «Фонарик» в снастях (id=4, пометка «ночь») 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) | [] ===== ИТОГ: 47/47 ===== ``` ## Подводные камни 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` будет босс-обрыв. ## Как добавить новый шаг 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 --json` | `result.browserPageId` (дальше везде `--page `) | | `orca-ide eval --expression --page --json` | `result.result` (объекты — JSON-строкой) | | `orca-ide screenshot --page --json` | `result.data` = base64 PNG | | `orca-ide console --limit N --page --json` | `result.messages: [{text, type}]`, `type: "error"` — ошибки | | `orca-ide tab close --page --json` | `result.closed` |