экспорт маппинга
This commit is contained in:
45
tests/test_build69_runtime_entry_admin_editor.py
Normal file
45
tests/test_build69_runtime_entry_admin_editor.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNTIME = (ROOT / "ui_builder" / "static" / "runtime.html").read_text(encoding="utf-8")
|
||||
EDITOR = (ROOT / "ui_builder" / "static" / "index.html").read_text(encoding="utf-8")
|
||||
INTEGRATION = (ROOT / "ui_builder" / "integration.py").read_text(encoding="utf-8")
|
||||
APP_JS = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8")
|
||||
TOURNAMENT_JS = (ROOT / "hockey_data" / "static" / "tournament-menu.js").read_text(encoding="utf-8")
|
||||
APP_PY = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_runtime_has_no_builder_dom():
|
||||
assert 'id="editor"' not in RUNTIME
|
||||
assert 'id="componentLibrary"' not in RUNTIME
|
||||
assert 'id="canvasStage"' not in RUNTIME
|
||||
assert 'id="inspector"' not in RUNTIME
|
||||
assert 'class="topbar editor-only"' not in RUNTIME
|
||||
assert 'id="runtimeView"' in RUNTIME
|
||||
assert 'runtime-view runtime-only hidden' not in RUNTIME
|
||||
|
||||
|
||||
def test_editor_keeps_full_builder():
|
||||
assert 'id="editor"' in EDITOR
|
||||
assert 'id="componentLibrary"' in EDITOR
|
||||
assert 'id="canvasStage"' in EDITOR
|
||||
assert 'id="inspector"' in EDITOR
|
||||
|
||||
|
||||
def test_integration_uses_separate_runtime_template():
|
||||
assert 'runtime_template = (assets_dir / "runtime.html")' in INTEGRATION
|
||||
assert 'template = editor_template if mode == "editor" else runtime_template' in INTEGRATION
|
||||
|
||||
|
||||
def test_editor_button_is_admin_only_and_hotkey_respects_it():
|
||||
assert 'id="runtimeEditorBtn" class="icon-btn hidden"' in RUNTIME
|
||||
assert 'editorButton?.classList.toggle("hidden", !state.isAdmin);' in TOURNAMENT_JS
|
||||
assert 'document.body.dataset.hockeyAdmin = state.isAdmin ? "1" : "0";' in TOURNAMENT_JS
|
||||
assert 'el.runtimeEditorBtn?.classList.contains("hidden")' in APP_JS
|
||||
|
||||
|
||||
def test_editor_server_paths_are_admin_protected():
|
||||
assert 'path == "/editor"' in APP_PY
|
||||
assert 'path.startswith("/api/ui-builder/editor/")' in APP_PY
|
||||
assert 'path.startswith("/api/ui-builder/auth/")' in APP_PY
|
||||
assert '(khl_admin_path or editor_path) and not user.is_admin' in APP_PY
|
||||
30
tests/test_build70_crisp_runtime_rendering.py
Normal file
30
tests/test_build70_crisp_runtime_rendering.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JS = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8")
|
||||
CSS = (ROOT / "ui_builder" / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
APP = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_build70_prefers_css_zoom_for_runtime_canvas():
|
||||
assert 'CSS.supports("zoom", "1")' in JS
|
||||
assert 'el.runtimeStage.style.zoom = String(scale);' in JS
|
||||
assert 'el.runtimeStage.style.transform = "none";' in JS
|
||||
|
||||
|
||||
def test_build70_fallback_does_not_force_translate3d_layer():
|
||||
start = JS.index('const canUseCssZoom')
|
||||
snippet = JS[start:start + 1500]
|
||||
assert 'style.transform = `translate3d' not in snippet
|
||||
assert 'el.runtimeStage.style.transform = `scale(${scale})`;' in snippet
|
||||
|
||||
|
||||
def test_build70_runtime_topbar_has_no_backdrop_blur():
|
||||
start = CSS.index('.runtime-topbar {')
|
||||
runtime_topbar = CSS[start:CSS.index('}', start) + 1]
|
||||
assert 'backdrop-filter' not in runtime_topbar
|
||||
assert 'background: #080d15' in runtime_topbar
|
||||
|
||||
|
||||
def test_build70_version():
|
||||
assert 'BUILD_VERSION = "2026.08.19.28"' in APP
|
||||
52
tests/test_build71_penalty_side_identifiers.py
Normal file
52
tests/test_build71_penalty_side_identifiers.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||
MAPPING = (ROOT / "hockey_data/mapping_context.py").read_text(encoding="utf-8")
|
||||
SERVICE = (ROOT / "hockey_data/service.py").read_text(encoding="utf-8")
|
||||
CSS = (ROOT / "ui_builder/static/styles.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_player_payload_exposes_database_id_without_replacing_external_id():
|
||||
assert '"id": player.external_id' in SERVICE
|
||||
assert '"external_id": player.external_id' in SERVICE
|
||||
assert '"db_id": player.id' in SERVICE
|
||||
|
||||
|
||||
def test_mapping_has_independent_home_and_away_penalty_identifiers():
|
||||
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",
|
||||
"selected_penalty_player_db_id",
|
||||
"selected_penalty_team_penalty",
|
||||
):
|
||||
assert f'"key": "{key}"' in MAPPING
|
||||
|
||||
|
||||
def test_penalty_preview_writes_side_specific_context_and_team_flag():
|
||||
for key in (
|
||||
"selected_home_penalty_id",
|
||||
"selected_home_penalty_player_db_id",
|
||||
"selected_home_penalty_team_penalty",
|
||||
"selected_away_penalty_id",
|
||||
"selected_away_penalty_player_db_id",
|
||||
"selected_away_penalty_team_penalty",
|
||||
):
|
||||
assert f"{key}:" in JS
|
||||
assert 'selected_penalty_team_penalty: detail.team_penalty ? "1" : "0"' in JS
|
||||
assert 'selected_penalty_player_db_id: detail.player_db_id' in JS
|
||||
|
||||
|
||||
def test_preview_selection_is_kept_per_side_and_visible():
|
||||
assert 'selectedPreviewEventIds: { home: "", away: "" }' in JS
|
||||
assert 'board.selectedPreviewEventIds[side] = event.id' in JS
|
||||
assert 'hockeyPenaltyIsPreviewSelected(board, event)' in JS
|
||||
assert 'Player DB ID' in JS
|
||||
assert 'Командное</b>${event.teamPenalty ? "1" : "0"}' in JS
|
||||
assert '.hpd-team-card-identifiers' in CSS
|
||||
145
tests/test_build72_portable_mapping_transfer.py
Normal file
145
tests/test_build72_portable_mapping_transfer.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from hockey_data.agent_bridge import VmixAgentHub
|
||||
from hockey_data.auth_bridge import HockeyUser
|
||||
from hockey_data.models import VmixDevice, VmixMappingField, VmixMappingProfile
|
||||
from tests.support import LocalTestDatabase
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JS = (ROOT / "hockey_data/static/admin-directories.js").read_text(encoding="utf-8")
|
||||
CSS = (ROOT / "hockey_data/static/admin-directories.css").read_text(encoding="utf-8")
|
||||
BRIDGE = (ROOT / "hockey_data/agent_bridge.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_portable_rebind_uses_title_when_input_key_changed() -> None:
|
||||
fields = [{
|
||||
"graphic": "score",
|
||||
"data_key": "game.home_score",
|
||||
"vmix_input_key": "old-key",
|
||||
"vmix_input_number": "5",
|
||||
"vmix_input_title": "SCORE BUG",
|
||||
"vmix_field": "HomeScore.Text",
|
||||
"field_type": "text",
|
||||
"enabled": True,
|
||||
"rule": {"enabled": True, "operator": "eq", "right_value": "1"},
|
||||
}]
|
||||
inventory = {"inputs": [{
|
||||
"key": "new-key",
|
||||
"number": "17",
|
||||
"title": "SCORE BUG",
|
||||
"type": "GT",
|
||||
"fields": [{"name": "HomeScore.Text", "type": "text", "index": "0"}],
|
||||
}]}
|
||||
|
||||
rebound, report = VmixAgentHub._rebind_portable_mapping_fields(fields, inventory)
|
||||
assert report["mapped"] == 1
|
||||
assert report["skipped"] == 0
|
||||
assert report["matched_by"]["title"] == 1
|
||||
assert rebound[0]["vmix_input_key"] == "new-key"
|
||||
assert rebound[0]["vmix_input_number"] == "17"
|
||||
assert rebound[0]["vmix_input_title"] == "SCORE BUG"
|
||||
assert rebound[0]["vmix_field"] == "HomeScore.Text"
|
||||
assert rebound[0]["rule"]["operator"] == "eq"
|
||||
|
||||
|
||||
def test_portable_rebind_reports_missing_field_without_guessing() -> None:
|
||||
fields = [{
|
||||
"data_key": "game.clock",
|
||||
"vmix_input_key": "old-key",
|
||||
"vmix_input_title": "SCORE BUG",
|
||||
"vmix_field": "Clock.Text",
|
||||
"field_type": "text",
|
||||
"enabled": True,
|
||||
}]
|
||||
inventory = {"inputs": [{
|
||||
"key": "new-key",
|
||||
"number": "2",
|
||||
"title": "SCORE BUG",
|
||||
"fields": [{"name": "Period.Text", "type": "text", "index": "0"}],
|
||||
}]}
|
||||
rebound, report = VmixAgentHub._rebind_portable_mapping_fields(fields, inventory)
|
||||
assert rebound == []
|
||||
assert report["mapped"] == 0
|
||||
assert report["skipped"] == 1
|
||||
assert report["skipped_items"][0]["reason"] == "field_not_found"
|
||||
|
||||
|
||||
def test_copy_profile_creates_target_specific_mapping_and_keeps_source(tmp_path: Path) -> None:
|
||||
database = LocalTestDatabase(tmp_path / "portable-mapping.sqlite3")
|
||||
database.create_all()
|
||||
hub = VmixAgentHub(database) # type: ignore[arg-type]
|
||||
user = HockeyUser(id="admin-1", login="admin", display_name="Admin")
|
||||
|
||||
source_inventory = {
|
||||
"fingerprint": "a" * 64,
|
||||
"inputs": [{
|
||||
"key": "source-score-key", "number": "3", "title": "SCORE BUG", "type": "GT",
|
||||
"fields": [{"name": "HomeScore.Text", "type": "text", "index": "0"}],
|
||||
}],
|
||||
"input_count": 1, "field_count": 1,
|
||||
}
|
||||
target_inventory = {
|
||||
"fingerprint": "b" * 64,
|
||||
"inputs": [{
|
||||
"key": "target-score-key", "number": "11", "title": "SCORE BUG", "type": "GT",
|
||||
"fields": [{"name": "HomeScore.Text", "type": "text", "index": "4"}],
|
||||
}],
|
||||
"input_count": 1, "field_count": 1,
|
||||
}
|
||||
|
||||
with database.session() as session:
|
||||
source = VmixMappingProfile(
|
||||
name="KHL MAIN", description="portable", project_fingerprint="a" * 64,
|
||||
inventory_json=json.dumps(source_inventory), version=7, active=True,
|
||||
created_by="admin", updated_by="admin",
|
||||
)
|
||||
session.add(source)
|
||||
session.flush()
|
||||
source_id = source.id
|
||||
session.add(VmixMappingField(
|
||||
profile_id=source.id, graphic="score", data_key="game.home_score",
|
||||
vmix_input_key="source-score-key", vmix_input_number="", vmix_input_title="SCORE BUG",
|
||||
vmix_field="HomeScore.Text", field_type="text", rule_json="{}", enabled=True, sort_order=0,
|
||||
))
|
||||
session.add(VmixDevice(
|
||||
device_uuid="GFX-PORTABLE-02", device_secret_hash="x" * 64, name="GFX 2",
|
||||
project_fingerprint="b" * 64, project_inventory_json=json.dumps(target_inventory),
|
||||
project_input_count=1, project_field_count=1, vmix_connected=True,
|
||||
))
|
||||
|
||||
payload = SimpleNamespace(device_id="GFX-PORTABLE-02", name="", replace_existing=False, apply_now=False)
|
||||
result = asyncio.run(hub.copy_mapping_profile_to_device(source_id, payload, user))
|
||||
assert result["report"]["mapped"] == 1
|
||||
assert result["report"]["matched_by"]["title"] == 1
|
||||
assert result["profile"]["project_fingerprint"] == "b" * 64
|
||||
assert result["profile"]["fields"][0]["vmix_input_key"] == "target-score-key"
|
||||
assert result["profile"]["fields"][0]["vmix_input_title"] == "SCORE BUG"
|
||||
|
||||
with database.session() as session:
|
||||
profiles = list(session.scalars(select(VmixMappingProfile).order_by(VmixMappingProfile.id)))
|
||||
assert len(profiles) == 2
|
||||
assert profiles[0].id == source_id and profiles[0].active is True
|
||||
assert profiles[0].project_fingerprint == "a" * 64
|
||||
assert profiles[1].active is True
|
||||
assert profiles[1].project_fingerprint == "b" * 64
|
||||
|
||||
|
||||
def test_admin_ui_has_export_import_and_copy_to_agent_controls() -> None:
|
||||
assert "data-map-export" in JS
|
||||
assert "data-map-copy-to-device" in JS
|
||||
assert "data-mapping-import" in JS
|
||||
assert "/copy-to-device" in JS
|
||||
assert "/api/hockey/admin/vmix-mapping/import" in JS
|
||||
assert "hockey-mapping.json" in JS
|
||||
assert "Input ищется по key → названию → номеру" in JS
|
||||
assert "portable vMix Mapping transfer/import" in CSS
|
||||
assert '"/api/hockey/admin/vmix-mapping/profiles/{profile_id}/export"' in BRIDGE
|
||||
assert '"/api/hockey/admin/vmix-mapping/import"' in BRIDGE
|
||||
Reference in New Issue
Block a user