from pathlib import Path from hockey_data.agent_bridge import VmixAgentHub from hockey_data.models import VmixPreparedTitle ROOT = Path(__file__).resolve().parents[1] APP = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8") MAPPING = (ROOT / "hockey_data/mapping_context.py").read_text(encoding="utf-8") BRIDGE = (ROOT / "hockey_data/agent_bridge.py").read_text(encoding="utf-8") STYLES = (ROOT / "ui_builder/static/styles.css").read_text(encoding="utf-8") def test_home_and_away_penalties_have_independent_manual_selection(): assert 'selectedPreviewEventIds: { home: "", away: "" }' in APP assert 'board.selectedPreviewEventIds[side] = event.id' in APP assert 'card.classList.toggle("is-preview-selected", hockeyPenaltyIsPreviewSelected(board, event));' in APP # The old global selectedEventId must not drive the visual selection class. penalty_start = APP.index("function hockeyPenaltyCard") penalty_end = APP.index("function createHockeyDraftFromDrop", penalty_start) penalty_card = APP[penalty_start:penalty_end] assert 'card.classList.toggle("is-selected", board.selectedEventId === event.id)' not in penalty_card assert '.hpd-team-penalty-card.team-home.is-preview-selected' in STYLES assert '.hpd-team-penalty-card.team-away.is-preview-selected' in STYLES def test_selected_penalty_context_is_manual_and_active_penalty_context_is_separate(): select_start = APP.index("async function hockeySelectPenaltyForPreview") select_end = APP.index("function hockeyPenaltyCard", select_start) select_block = APP[select_start:select_end] for key in ( "selected_home_penalty_id", "selected_home_penalty_player_id", "selected_home_penalty_player_db_id", "selected_home_penalty_team_penalty", "selected_away_penalty_id", "selected_away_penalty_player_id", "selected_away_penalty_player_db_id", "selected_away_penalty_team_penalty", ): assert key in select_block assert key in MAPPING sync_start = APP.index("async function hockeySyncPenaltySideMappingContext") sync_end = APP.index("function penaltyTargetAssignmentKey", sync_start) sync_block = APP[sync_start:sync_end] assert "active_home_penalty_id:" in sync_block assert "active_away_penalty_id:" in sync_block assert "selected_home_penalty_id:" not in sync_block assert "selected_away_penalty_id:" not in sync_block def test_player_selection_values_survive_reload_using_server_state_and_browser_backup(): for token in ( "function hockeyPlayerSelectionStorageKey", "function hockeyBackupPlayerSelectionValues", "function hockeyStoredPlayerSelectionValues", "hockeyBackupPlayerSelectionValues(gameId, payload?.values || {})", 'if (!control || String(control.game_id || "") !== String(gameId)) return false;', "const hasServerSelectionKeys", ): assert token in APP assert "localStorage.setItem(hockeyPlayerSelectionStorageKey(id)" in APP def test_player_block_editor_is_compact_single_line(): assert "/* BUILD84 — compact, single-line player-block editor. */" in STYLES assert "min-height:48px" in STYLES assert "grid-template-columns:24px 66px" in STYLES assert '> Главный' in APP assert '> Свёрнут' in APP def test_prepared_titles_workspace_exists_and_clones_to_vmix(): assert '{ id: "prepared_titles", label: "Заготовки" }' in APP for token in ( "function renderHockeyPreparedTitlesWorkspace()", "function hockeyOpenPreparedFromPlayerPanel(panel)", "data-player-panel-prepared", "/api/hockey/prepared-titles", "Сохранить новым Input", "Подставить из Mapping", "▶ Preview", ): assert token in APP assert VmixPreparedTitle.__tablename__ == "hockey_vmix_prepared_titles" assert '{"Function": "CreateVirtualInput", "Input": source_ref}' in BRIDGE assert '{"Function": "PreviewInput", "Input": input_ref}' in BRIDGE assert '@router.post("/api/hockey/prepared-titles")' in BRIDGE assert '@router.get("/api/hockey/prepared-titles/mapping-snapshot")' in BRIDGE def test_prepared_input_resolution_prefers_stable_key_then_number_then_unique_title(): inventory = { "inputs": [ {"key": "stable-a", "number": "7", "title": "TITLE"}, {"key": "stable-b", "number": "9", "title": "OTHER"}, ] } assert VmixAgentHub._prepared_find_input(inventory, key="stable-b")["number"] == "9" assert VmixAgentHub._prepared_find_input(inventory, number="7")["key"] == "stable-a" assert VmixAgentHub._prepared_find_input(inventory, title="OTHER")["key"] == "stable-b" duplicate = {"inputs": [{"key": "a", "title": "SAME"}, {"key": "b", "title": "SAME"}]} assert VmixAgentHub._prepared_find_input(duplicate, title="SAME") is None def test_prepared_title_creation_clones_last_input_and_applies_field_snapshot(tmp_path): import asyncio import json import types from types import SimpleNamespace from sqlalchemy import select from hockey_data.auth_bridge import HockeyUser from hockey_data.models import VmixDevice, VmixPreparedTitle from tests.support import LocalTestDatabase database = LocalTestDatabase(tmp_path / "prepared.sqlite3") database.create_all() hub = VmixAgentHub(database) # type: ignore[arg-type] user = HockeyUser(id="prep-user", login="prep", display_name="Prep") source_inventory = { "inputs": [ { "key": "source-key", "number": "12", "title": "COMPARE", "type": "GT", "fields": [ {"name": "Name.Text", "type": "text", "index": "0"}, {"name": "Photo.Source", "type": "image", "index": "1"}, ], } ] } with database.session() as session: session.add(VmixDevice( device_uuid="prep-device", device_secret_hash="x" * 64, name="Prep device", wfl_user_id=user.id, is_active_for_account=True, vmix_connected=True, current_match_external_id="9001", project_inventory_json=json.dumps(source_inventory), )) calls = [] async def fake_sequence(self, _user, commands, *, device_id="", session_token="", timeout=0): calls.append(commands) if commands and commands[0].get("Function") == "CreateVirtualInput": cloned = { "inputs": source_inventory["inputs"] + [ { "key": "clone-key", "number": "13", "title": "COMPARE", "type": "GT", "fields": source_inventory["inputs"][0]["fields"], } ] } with database.session() as session: device = session.scalar(select(VmixDevice).where(VmixDevice.device_uuid == "prep-device")) device.project_inventory_json = json.dumps(cloned) return {"ok": True, "device_id": device_id or "prep-device", "match_id": "9001"} hub.run_vmix_sequence_for_user = types.MethodType(fake_sequence, hub) payload = SimpleNamespace( name="Compare ready", device_id="prep-device", session_token="session", source_input_key="source-key", source_input_number="12", source_input_title="COMPARE", source_kind="player_selection", source_ref="compare_home", field_values={ "Name.Text": {"type": "text", "value": "Иванов"}, "Photo.Source": {"type": "image", "value": r"D:\\Photo\\ivanov.png"}, }, ) result = asyncio.run(hub.create_prepared_title(user, payload)) assert result["clone_input"]["number"] == "13" assert calls[0] == [{"Function": "CreateVirtualInput", "Input": "source-key"}] assert any(command.get("Function") == "SetText" and command.get("Input") == "clone-key" for command in calls[1]) assert any(command.get("Function") == "SetImage" and command.get("SelectedName") == "Photo.Source" for command in calls[1]) with database.session() as session: row = session.scalar(select(VmixPreparedTitle)) assert row is not None assert row.clone_input_key == "clone-key" assert row.clone_input_number == "13" assert row.source_ref == "compare_home"