up
This commit is contained in:
@@ -139,7 +139,26 @@ Venue автоматически учитывает браузеры игрок
|
||||
`POST /api/devices/heartbeat` публично регистрирует подключение, а
|
||||
`GET /api/devices?gameID=<ID>` возвращает сгруппированные устройства организатору,
|
||||
администратору или разработчику. Фильтр необязателен; при его использовании
|
||||
список содержит только подключения выбранной игры. Панель состояния — этап 13.
|
||||
список содержит только подключения выбранной игры, без непривязанных браузеров.
|
||||
|
||||
Вкладка «Состояние» (`/games/:id?tab=state`, этап 13) доступна в venue
|
||||
организатору, администратору и разработчику. Она показывает число доступных
|
||||
устройств, устройств без связи и подключений, UUID профиля и последнее
|
||||
обращение; раскрытие устройства показывает тип подключения, ID команды/экрана,
|
||||
статус, IP и User-Agent. Устройства без связи идут первыми, далее сортировка
|
||||
по UUID. Статусы и время обновления берутся из серверного снимка.
|
||||
Снимок запрашивается сразу и раз в 5 секунд только на активной вкладке,
|
||||
без параллельных запросов; поздние ответы после ухода игнорируются.
|
||||
При ошибке предыдущий снимок и время остаются видимыми, доступен повтор.
|
||||
«Нет связи» означает отсутствие heartbeat сайта, а не диагностику Wi-Fi.
|
||||
|
||||
Разработчик без роли организатора/администратора/автора в venue получает
|
||||
список игр и только вкладку состояния: сведения игры ограничены ID, названием,
|
||||
описанием, форматом, местом, плановым началом и статусом. Команды, пароли,
|
||||
сценарий, материалы и игровой прогресс не выдаются и не загружаются интерфейсом.
|
||||
Создание игр и результаты скрыты; существующий импорт сохраняется.
|
||||
Heartbeat разработчика остаётся браузерным без контекста организатора.
|
||||
В prod этот доступ и вкладка состояния не открываются.
|
||||
|
||||
UUID браузерного профиля хранится отдельно от авторизации в localStorage;
|
||||
каждая вкладка и перезагрузка страницы создают новый UUID подключения.
|
||||
|
||||
+6
-3
@@ -27,13 +27,13 @@
|
||||
|
||||
### ED-029 — Автономная площадка и панель организатора
|
||||
|
||||
- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–12 завершены; аппаратная приёмка этапа 8 подтверждена пользователем.
|
||||
- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–13 завершены; аппаратная приёмка этапа 8 подтверждена пользователем.
|
||||
- Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`.
|
||||
- Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт со статусом офлайн-режима; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки.
|
||||
- Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия.
|
||||
- Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без несогласованных новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy и автоматического переключения серверов; настройки роутера менять только по явному запросу пользователя; секреты и пакеты вне Git; commit/push выполняет пользователь.
|
||||
- Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15.
|
||||
- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–11 реализованы и проверены; этапы 12–15 ещё не завершены.
|
||||
- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–13 реализованы и проверены; этапы 14–15 ещё не завершены.
|
||||
- Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`.
|
||||
- Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke.
|
||||
- Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась.
|
||||
@@ -81,7 +81,10 @@
|
||||
- Результат этапа 12 (2026-10-11): добавлены потокобезопасный сервис присутствия в памяти, wiring, protobuf/OpenAPI `POST /api/devices/heartbeat` и `GET /api/devices` с фильтром игры. Проверяются UUID, взаимоисключающие контексты, JWT/существующие права игры, пароль команды и неотменённое участие без загрузки сценария/прогресса, существующая сессия экрана; ID игры команды/экрана определяет сервер. Реестр хранит только проверенные связи, UUID, серверные времена и последние IP/User-Agent; credentials не сохраняются и не выдаются. Полная замена/очистка контекста, граница связи строго менее 60 секунд, удаление через 15 минут при обновлении/чтении, предел 1024 с `ResourceExhausted` без вытеснения существующих записей. В prod heartbeat явно возвращает `enabled=false`, список недоступен.
|
||||
- Браузер этапа 12: общий heartbeat в App сразу/каждые 20 секунд без параллельных запросов, при видимости/восстановлении сети и смене контекста. UUID профиля отдельно от авторизации в localStorage с временным fallback, UUID подключения на экземпляр приложения. Контекст страницы очищается при уходе, отозванный lifecycle не может восстановить старые связи; смена игры/пароля пересоздаёт page-компонент. Generated client и обновление JWT организатора переиспользованы; публичные credentials команды/экрана не запускают обновление JWT. После `enabled=false` polling прекращается. Ошибки не показывают toast и повторяются по интервалу. UI списка устройств не добавлен.
|
||||
- Проверки этапа 12: штатная генерация обоих репозиториев, gofmt, `go test ./...`, race для presence service/app, frontend type-check/lint/lint:css/build, self-check UUID/store/composable и diff-проверки прошли. Unit-тесты с управляемыми часами покрывают повтор/вкладки/смену и очистку связей, границы 60 секунд/15 минут, лимит и конкурентное чтение/обновление; API-тесты — роли, режимы, safe errors и отсутствие секретов. `python3 tests/venue/flow.py --browser` прошёл изолированные реальные API/PostgreSQL/RustFS/Firefox: credentials/отменённое участие, UUID/поля, группировку вкладок игрока/организатора/экрана, фильтр и привязку экрана, SPA-навигацию, прекращение heartbeat и восстановление сети, повторную регистрацию штатным интервалом после перезапуска backend. Независимое ревью выявило два замечания (публичный heartbeat не должен обновлять JWT, обрезка User-Agent должна сохранять UTF-8); оба исправлены и покрыты регрессиями. Итоговое ручное ревью по Go/Frontend стандартам выполнено; замечаний не осталось.
|
||||
- Следующий шаг: этап 13 — панель состояния площадки. Этап 12 завершён; общая задача остаётся `IN_PROGRESS` и не переносится в архив. ZIP остаётся неподписанным. Фактический ZIP 1 ГиБ, нагрузка на 100 клиентов и покрытие Wi-Fi не проверялись; развёртывание, commit/push не выполнялись.
|
||||
- Результат этапа 13 (2026-10-11): venue-вкладка «Состояние» `/games/:id?tab=state` показывает серверные connected/serverTime, количество доступных устройств/устройств без связи/подключений, UUID профиля, последние обращения и раскрываемые подключения с типом, ID команды/экрана, статусом, IP и User-Agent. Нет связи идёт первым, затем UUID; непривязанные браузеры исключает фильтр игры. Generated GetDevices вызывается сразу и каждые 5 секунд через useSimplePolling только на активной вкладке, без параллельных запросов; поздние ответы после смены игры, ухода и уничтожения игнорируются. Первичная загрузка, пустой список, встроенная ошибка, сохранение предыдущего снимка/времени и повтор реализованы без повторных toast. Вкладка доступна с клавиатуры. Игровые показатели, Cudy и история присутствия не добавлялись.
|
||||
- Доступ этапа 13: GetGames/GetGame и UI permissions расширены для разработчика только в venue. Разработчик без организатора/администратора/автора получает явный whitelist ID/названия/описания/формата/места/планового начала/статуса; GetGame использует краткое чтение без сценария/команд. UI скрывает создание и результаты, показывает название и только состояние, нормализует другие tab, не загружает экраны/музыку/план/карту/ответы; heartbeat остаётся браузерным. Дополнительные роли, импорт и запреты игровых действий сохранены; prod новые права не получает. При смене роли полный поздний GetGame не применяется, приватные баннер/модалки скрываются.
|
||||
- Проверки этапа 13: gofmt, полный go test ./..., race-тесты device_presence_service/app с управляемыми часами этапа 12; frontend type-check/lint/lint:css/build и self-check gameState прошли. Self-check покрывает сводку/сортировку, активность, отсутствие параллельных запросов, поздние ответы, ошибку/восстановление. Изолированные PostgreSQL/RustFS API-регрессии и Firefox проверили организатора, три подключения одного профиля, клавиатуру, смену контекста, пустой список, потерю/восстановление GetDevices, остановку запросов на другой вкладке, разработчика venue/prod, безопасные GetGame/GetGames и отсутствие закрытых фоновых запросов. API/protobuf/миграции/зависимости не менялись. Ручное ревью diff по Go/frontend-стандартам и независимое read-only ревью выполнены; пользовательские изменения prod/reproxy/backend-бинарника сохранены. Работающие окружения и Cudy не менялись.
|
||||
- Следующий шаг: этап 14 — сбор данных Cudy. Этап 13 завершён; общая задача остаётся `IN_PROGRESS` и не переносится в архив. ZIP остаётся неподписанным. Фактический ZIP 1 ГиБ, нагрузка на 100 клиентов и покрытие Wi-Fi не проверялись; развёртывание, commit/push не выполнялись.
|
||||
|
||||
### ED-024 — Общая дизайн-система фронтенда
|
||||
|
||||
|
||||
+1
-1
Submodule prod updated: 46a9864c8c...218ed05fd5
@@ -137,3 +137,21 @@ Firefox проверяет игрока, организатора и экран
|
||||
отозванные lifecycle-контексты, отсутствие параллельных heartbeat,
|
||||
остановка после `enabled=false`, повтор ошибок по интервалу и независимость
|
||||
публичных credentials от JWT. Нагрузочный и аппаратный прогоны не включены.
|
||||
|
||||
|
||||
### Панель состояния (этап 13)
|
||||
|
||||
`python3 tests/venue/flow.py --browser` дополнительно проверяет Firefox:
|
||||
сводку и серверное время, встроенную ошибку с сохранением снимка, повтор,
|
||||
пустой список, остановку запросов после переключения вкладки; отдельного
|
||||
разработчика в venue/prod, безопасный состав GetGame/GetGames, скрытые создание
|
||||
и результаты, нормализацию `tab=answers` в `state`, единственную вкладку и
|
||||
обычный браузерный heartbeat. Проверки нескольких вкладок и смены контекста
|
||||
этапа 12 остаются частью прогона. Потеря API и пустой ответ воспроизводятся
|
||||
перехватом только GetDevices в браузере; остальные API реальные.
|
||||
|
||||
`node src/composables/__tests__/gameState.test.ts` (из frontend) проверяет
|
||||
счётчики, сортировку, отсутствие параллельных запросов, активность вкладки,
|
||||
ошибку/восстановление и отбрасывание ответов после смены игры, ухода и unmount.
|
||||
Временные границы connected остаются в тестах сервиса этапа 12 с управляемыми
|
||||
часами; клиент пороги не пересчитывает.
|
||||
|
||||
@@ -117,6 +117,7 @@ class Stand:
|
||||
|
||||
def handle_request(self):
|
||||
if self.path.startswith('/api/'):
|
||||
stand.request_paths.append((stand.current, urllib.parse.urlparse(self.path).path))
|
||||
if self.command == 'POST' and self.path.endswith('/results-package/apply'):
|
||||
time.sleep(stand.delay_results)
|
||||
if self.command == 'POST' and self.path.endswith('/actions'):
|
||||
@@ -159,6 +160,7 @@ class Stand:
|
||||
# Navigation may cancel an in-flight browser polling response.
|
||||
pass
|
||||
|
||||
self.request_paths = []
|
||||
self.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||
self.public = f'http://127.0.0.1:{self.server.server_port}'
|
||||
self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
|
||||
@@ -63,7 +63,7 @@ def check_browser_flow(stand, fixtures):
|
||||
sock.sendall(str(len(payload)).encode() + b':' + payload)
|
||||
result = read_message()
|
||||
if result[2]:
|
||||
raise RuntimeError('Firefox command failed: ' + name)
|
||||
raise RuntimeError('Firefox command failed: ' + name + ': ' + result[2].get('message', 'unknown error'))
|
||||
return result[3]
|
||||
|
||||
def js(script, *args):
|
||||
@@ -87,13 +87,13 @@ def check_browser_flow(stand, fixtures):
|
||||
def input_value(selector, value):
|
||||
assert js("const input=document.querySelector(arguments[0]);if(!input)return false;input.value=arguments[1];input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}));return true;", selector, value), 'Missing input: ' + selector
|
||||
|
||||
def login(mode):
|
||||
def login(mode, email='organizer@test.invalid'):
|
||||
stand.current = mode
|
||||
navigate('/login')
|
||||
js('localStorage.clear();return true;')
|
||||
navigate('/login')
|
||||
wait("document.querySelector('input[type=password]')!==null", 'login form')
|
||||
input_value('input[placeholder="detective@mail.ru"]', 'organizer@test.invalid')
|
||||
input_value('input[placeholder="detective@mail.ru"]', email)
|
||||
input_value('input[type=password]', 'test-only-password')
|
||||
click('Вход')
|
||||
wait("location.pathname==='/office'", 'login')
|
||||
@@ -132,6 +132,11 @@ def check_browser_flow(stand, fixtures):
|
||||
js("window.dispatchEvent(new Event('online'));return true;")
|
||||
wait_for(lambda: any(c['ID'] == screen_connection['ID'] and c.get('gameID') == data['game'] for c in connections()), 'bound screen presence')
|
||||
assert {c['kind'] for c in connections()} >= {'organizer', 'team', 'screen'}
|
||||
command('WebDriver:SwitchToWindow', {'handle': organizer_window})
|
||||
assert js("const tab=[...document.querySelectorAll('.n-tabs-tab[data-name]')].find(t=>t.textContent.trim()==='Состояние');tab.focus();tab.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',bubbles:true}));return true;")
|
||||
wait("document.body.innerText.includes('Подключения: 3')", 'three connections in one device panel')
|
||||
assert js("return [...document.querySelectorAll('.device-details strong')].filter(e=>e.textContent===arguments[0]).length===1", device_id)
|
||||
command('WebDriver:SwitchToWindow', {'handle': screen_window})
|
||||
command('WebDriver:CloseWindow')
|
||||
command('WebDriver:SwitchToWindow', {'handle': team_window})
|
||||
command('WebDriver:CloseWindow')
|
||||
@@ -157,13 +162,75 @@ def check_browser_flow(stand, fixtures):
|
||||
assert new_id != old_id and js("return localStorage.getItem('evening-detective-device-ID')") == device_id
|
||||
print('PASS: stage 12 real Firefox organizer/player/screen, grouped tabs, SPA context clearing, reload UUID, stopped heartbeat/network recovery and interval re-registration after restart', flush=True)
|
||||
|
||||
def check_state(data):
|
||||
from flow import API, ROOT, run
|
||||
game = data['game']
|
||||
navigate(f'/games/{game}?tab=state')
|
||||
wait("document.body.innerText.includes('Последнее обновление:')", 'state initial snapshot')
|
||||
assert js("return document.body.innerText.includes('Доступны:') && document.body.innerText.includes('Подключения:')")
|
||||
# Track only list requests; all other requests use the real server.
|
||||
js("window.stateFetch=window.fetch;window.stateCalls=0;window.stateMode='real';window.fetch=(url,options)=>{if(String(url).includes('/api/devices?')){window.stateCalls++;if(window.stateMode==='error')return Promise.reject(new Error('offline'));if(window.stateMode==='empty')return Promise.resolve(new Response(JSON.stringify({devices:[],serverTime:new Date().toISOString()}),{headers:{'Content-Type':'application/json'}}));}return window.stateFetch(url,options)};window.stateMode='error';return true;")
|
||||
wait("document.body.innerText.includes('Не удалось обновить состояние')", 'inline API failure', 10)
|
||||
assert js("return document.body.innerText.includes('Последнее обновление:')")
|
||||
js("window.stateMode='real';return true;")
|
||||
click('Повторить')
|
||||
wait("!document.body.innerText.includes('Не удалось обновить состояние')", 'state recovery')
|
||||
js("window.stateMode='empty';return true;")
|
||||
wait("document.body.innerText.includes('Устройства этой игры пока не зарегистрированы')", 'empty devices', 10)
|
||||
assert js("const tab=[...document.querySelectorAll('.n-tabs-tab[data-name]')].find(t=>t.textContent.trim()==='Игра');tab.click();return true;")
|
||||
wait("location.search.includes('tab=game')", 'leave state tab')
|
||||
calls = js('return window.stateCalls')
|
||||
time.sleep(6)
|
||||
assert js('return window.stateCalls') == calls, 'Inactive state still polls'
|
||||
js('window.fetch=window.stateFetch;return true;')
|
||||
|
||||
# Create a separate developer in each disposable database.
|
||||
bootstrap = (ROOT / 'evening_detective_server/scripts/dev-user.sql').read_text()
|
||||
for mode in ('venue', 'prod'):
|
||||
run('docker', 'exec', '-i', stand.name + '-' + mode + '-pg', 'psql', '-U', 'test', '-d', 'test', '-v', 'ON_ERROR_STOP=1', '-v', 'dev_username=TEST developer', '-v', 'dev_email=developer@test.invalid', '-v', 'dev_password=test-only-password', input=bootstrap)
|
||||
stand.sql(mode, "UPDATE users SET roles='[\"developer\"]'::jsonb WHERE email='developer@test.invalid';")
|
||||
api = API(stand.apis[mode].base)
|
||||
api.token = api.request('/api/auth/login', {'email':'developer@test.invalid','password':'test-only-password'})['accessToken']
|
||||
if mode == 'prod':
|
||||
api.request('/api/games', expected=403)
|
||||
api.request(f'/api/games/{game}', expected=403)
|
||||
else:
|
||||
summary = api.request(f'/api/games/{game}')['game']
|
||||
public_fields = {'ID','name','description','format','location','startAt','status'}
|
||||
# Gateway may emit unset protobuf fields as null/empty collections.
|
||||
assert all(not value for key, value in summary.items() if key not in public_fields)
|
||||
games = api.request('/api/games')['games']
|
||||
assert games and all(not value for g in games for key, value in g.items() if key not in public_fields)
|
||||
old_connections = {c['ID'] for d in stand.apis['venue'].request('/api/devices')['devices'] for c in d['connections']}
|
||||
login('venue', 'developer@test.invalid')
|
||||
navigate('/games')
|
||||
wait("document.body.innerText.includes('Импорт игры')", 'developer games')
|
||||
assert js("return !document.body.innerText.includes('Создать игру') && !document.body.innerText.includes('Результаты')")
|
||||
first_request = len(stand.request_paths)
|
||||
navigate(f'/games/{game}?tab=answers')
|
||||
wait("location.search.includes('tab=state') && document.body.innerText.includes('Последнее обновление:')", 'developer state default')
|
||||
assert js("return [...document.querySelectorAll('.n-tabs-tab[data-name]')].map(t=>t.textContent.trim()).join(',')==='Состояние'")
|
||||
time.sleep(6)
|
||||
assert not any(mode == 'venue' and path.startswith(f'/api/games/{game}/') for mode, path in stand.request_paths[first_request:]), 'Developer loads private game data'
|
||||
# Heartbeat is browser-only for this role.
|
||||
device_id = js("return localStorage.getItem('evening-detective-device-ID')")
|
||||
devices = stand.apis['venue'].request('/api/devices')['devices']
|
||||
current_connections = [c for d in devices if d['ID'] == device_id for c in d['connections'] if c['ID'] not in old_connections]
|
||||
assert current_connections and all(c['kind'] == 'browser' for c in current_connections)
|
||||
login('prod', 'developer@test.invalid')
|
||||
navigate(f'/games/{game}?tab=state')
|
||||
time.sleep(1)
|
||||
assert js("return ![...document.querySelectorAll('.n-tabs-tab[data-name]')].some(t=>t.textContent.trim()==='Состояние')")
|
||||
login('venue')
|
||||
print('PASS: stage 13 Firefox state counts, inline failure/recovery/empty, inactive polling, developer restricted games/default tab/browser context and prod denial', flush=True)
|
||||
|
||||
command('WebDriver:NewSession', {'capabilities': {'alwaysMatch': {'acceptInsecureCerts': True}}})
|
||||
for width, data in zip((1440, 390), fixtures, strict=True):
|
||||
game, captain = data['game'], data['teams'][0]
|
||||
command('WebDriver:SetWindowRect', {'width': width, 'height': 900})
|
||||
login('prod')
|
||||
navigate(f'/games/{game}?tab=settings')
|
||||
wait("document.body.innerText.includes('Подготовить пакет')", 'export button')
|
||||
wait("[...document.querySelectorAll('button')].some(b=>b.textContent.trim()==='Подготовить пакет' && !b.disabled)", 'export button')
|
||||
click('Подготовить пакет')
|
||||
wait("document.body.innerText.includes('Скачать пакет')", 'export ready', 60)
|
||||
previous_downloads = {p.name for p in downloads.glob('*.zip')}
|
||||
@@ -193,6 +260,7 @@ def check_browser_flow(stand, fixtures):
|
||||
wait(f"location.pathname==='/games/{game}'", 'open imported game')
|
||||
if width == 1440:
|
||||
check_presence(data)
|
||||
check_state(data)
|
||||
navigate(f'/games/{game}?tab=payment')
|
||||
wait("document.body.innerText.includes('Проверить билет')", 'ticket checking')
|
||||
click('Проверить билет')
|
||||
@@ -246,6 +314,7 @@ def check_browser_flow(stand, fixtures):
|
||||
navigate(f'/games/{game}?tab=game')
|
||||
wait("document.querySelector('[aria-label=\"Завершить игру\"]')!==null", 'finish control')
|
||||
click('Завершить игру')
|
||||
first_request = len(stand.request_paths)
|
||||
navigate(f'/games/{game}?tab=answers')
|
||||
wait("document.querySelector('.answers-score-input input')!==null", 'grading form')
|
||||
input_value('.answers-score-input input', '5')
|
||||
@@ -293,6 +362,7 @@ def check_browser_flow(stand, fixtures):
|
||||
assert stand.apis['prod'].request('/api' + urllib.parse.urlparse(data['tickets'][1]['URL']).path)['ticket']['status'] == 'used'
|
||||
assert stand.apis['prod'].request(f'/api/games/{game}/results')['teams'][0]['totalScore'] == 5
|
||||
login('venue')
|
||||
first_request = len(stand.request_paths)
|
||||
navigate(f'/games/{game}?tab=answers')
|
||||
wait("document.querySelector('.answers-score-input input')!==null", 'corrected grading')
|
||||
input_value('.answers-score-input input', '0')
|
||||
|
||||
Reference in New Issue
Block a user