Files
hockey_new/tests/test_shortcut_sequences.py

263 lines
11 KiB
Python

from __future__ import annotations
from pathlib import Path
from ui_builder.manager import UIBuilderManager
def test_shortcut_sequences_are_normalized_and_persisted(tmp_path: Path) -> None:
manager = UIBuilderManager(tmp_path, filename="ui.json")
saved = manager.save({
"version": 20,
"project_name": "Hockey",
"data_source": "hockey",
"canvas": {"width": 1440, "height": 900},
"tabs": [{"id": "main", "label": "Main"}],
"components": [],
"triggers": [{
"id": "trigger-1",
"name": "Start from event",
"enabled": True,
"source_action_id": "clock",
"event": "click",
"action": {"type": "run_sequence", "sequence_id": "start-clock"},
}],
"shortcut_sequences": [{
"id": "start-clock",
"name": "Старт времени",
"description": "Запуск веба и vMix",
"combo": "F1",
"steps": [{
"id": "sync",
"type": "hockey_vmix_timers_start",
"condition": "has_penalties",
"game_timer_action_id": "hockey_game_timer",
"hockey_timer_command": "toggle",
"game_vmix_mode": "countdown",
"game_vmix_input": "53",
"game_vmix_selected_name": "CLOCK.Text",
"penalty_display_mode": "soonest",
"home_penalty_inputs": "54,55",
"away_penalty_inputs": "56,57",
}, {
"id": "pause",
"type": "delay",
"milliseconds": "not-a-number",
}],
}],
})
assert saved["shortcut_sequences"][0]["combo"] == "F1"
assert saved["shortcut_sequences"][0]["steps"][0]["type"] == "hockey_vmix_timers_start"
assert saved["shortcut_sequences"][0]["steps"][0]["condition"] == "has_penalties"
assert saved["shortcut_sequences"][0]["steps"][0]["hockey_timer_command"] == "toggle"
assert saved["shortcut_sequences"][0]["steps"][0]["game_vmix_mode"] == "countdown"
assert saved["shortcut_sequences"][0]["steps"][0]["game_vmix_selected_name"] == "CLOCK.Text"
assert saved["shortcut_sequences"][0]["steps"][0]["penalty_display_mode"] == "soonest"
assert saved["shortcut_sequences"][0]["steps"][1]["milliseconds"] == 0
assert saved["triggers"][0]["action"]["sequence_id"] == "start-clock"
def test_shortcut_ui_contains_editor_reference_and_vmix_timer_sequence() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
index_html = Path("ui_builder/static/index.html").read_text(encoding="utf-8")
assert "showShortcutsEditor" in app_js
assert "showShortcutsReference" in app_js
assert "hockey_vmix_timers_start" in app_js
assert 'Function: "SetCountdown"' in app_js
assert 'Function: "StartCountdown"' in app_js
assert 'id="runtimeShortcutsBtn"' in index_html
assert 'id="shortcutsBtn"' in index_html
def test_shortcut_vmix_editor_uses_searchable_inventory_controls() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'api("/vmix-inventory")' in app_js
assert "vmixFunctionOptions" in app_js
assert "vmixInputOptions" in app_js
assert "vmixSelectedNameOptions" in app_js
assert 'data-vmix-function-select' in app_js
assert 'data-step-field="input"' in app_js
assert 'data-step-field="selected_name"' in app_js
assert "SetTextColour" in app_js
assert "OverlayInput1In" in app_js
def test_shortcut_editor_collapses_existing_sequences_and_is_locked() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert "shortcutSequenceOpenId" in app_js
assert "shortcut-sequence-compact" in app_js
assert "data-sequence-edit" in app_js
assert '{ locked: true, className: "modal-shortcuts-full" }' in app_js
assert "Это окно защищено от случайного закрытия" in app_js
def test_shortcut_editor_layout_fixes_add_step_buttons_and_fullscreen_modal() -> None:
css = Path("ui_builder/static/styles.css").read_text(encoding="utf-8")
assert ".shortcut-add-steps .mini-btn" in css
assert "width: auto;" in css
assert ".modal-card.modal-shortcuts-full" in css
assert "height: calc(100vh - 20px);" in css
def test_hockey_timer_shortcut_supports_native_countdown_and_legacy_text_mirror() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'Function: "StopCountdown"' in app_js
assert 'Function: "SuspendCountdown"' not in app_js
assert 'Function: "StopCountdown"' in app_js
assert 'hockey_timer_command' in app_js
assert 'game_vmix_mode' in app_js
assert 'game_vmix_selected_name' in app_js
assert 'vmixTimerMirrors' in app_js
assert 'Function: "SetText"' in app_js
assert 'combo: "Space"' in app_js
def test_global_shortcuts_capture_keys_before_focused_runtime_controls() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'window.addEventListener("keydown", (event) => {' in app_js
assert ' }, true);\n\n window.addEventListener("keyup"' in app_js
assert 'event.stopImmediatePropagation()' in app_js
assert 'function shortcutSequenceAllowed(sequence, event)' in app_js
# Global shortcut sequences are intentionally independent of the active UI tab.
sequence_allowed = app_js.split('function shortcutSequenceAllowed(sequence, event)', 1)[1].split('function currentHockeyPenaltyEntries()', 1)[0]
assert 'isEffectivelyOnActiveTab' not in sequence_allowed
assert 'isEditingTarget(event.target)' in sequence_allowed
def test_hockey_timer_targets_persist_selected_names_and_finish_actions(tmp_path: Path) -> None:
manager = UIBuilderManager(tmp_path, filename="ui.json")
saved = manager.save({
"version": 20,
"project_name": "Hockey",
"data_source": "hockey",
"canvas": {"width": 1440, "height": 900},
"tabs": [{"id": "main", "label": "Main"}],
"components": [],
"triggers": [],
"shortcut_sequences": [{
"id": "timers",
"name": "Timers",
"combo": "Space",
"steps": [{
"id": "sync",
"type": "hockey_vmix_timers_start",
"game_vmix_input": "53",
"game_vmix_selected_name": "CLOCK.Text",
"home_penalty_targets": [
{"id": "home-1", "input": "54", "selected_name": "PENALTY1.Text"},
{"id": "home-2", "input": "55", "selected_name": "PENALTY2.Text"},
],
"away_penalty_targets": [
{"id": "away-1", "input": "56", "selected_name": "PENALTY1.Text"},
],
"timer_finish_actions": [{
"id": "finish-1",
"enabled": True,
"source": "game",
"input": "70",
"overlay": "3",
"duration_ms": 4500,
}],
}],
}],
})
step = saved["shortcut_sequences"][0]["steps"][0]
assert step["game_vmix_selected_name"] == "CLOCK.Text"
assert step["penalty_display_mode"] == "soonest"
assert step["home_penalty_targets"][0] == {
"id": "home-1", "input": "54", "selected_name": "PENALTY1.Text",
"overlay": "2", "auto_hide_on_finish": True,
}
assert step["away_penalty_targets"][0]["selected_name"] == "PENALTY1.Text"
assert step["timer_finish_actions"][0]["source"] == "game"
assert step["timer_finish_actions"][0]["overlay"] == "3"
assert step["timer_finish_actions"][0]["duration_ms"] == 4500
def test_hockey_countdown_uses_selected_name_for_game_and_penalty_targets() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'function vmixCountdownSyncCommands(input, selectedName' in app_js
assert 'SelectedName: String(selectedName || "").trim()' in app_js
assert 'step.game_vmix_selected_name, () => gameTimerState.currentMs' in app_js
assert 'target.input, target.selected_name, () => event.remainingMs' in app_js
assert 'Input: target.input, SelectedName: target.selected_name' in app_js
assert 'vmixTextSelectedNameOptions' in app_js
assert 'data-penalty-target-field="selected_name"' in app_js
assert 'data-finish-action-add' in app_js
assert 'OverlayInput${overlay}In' in app_js
assert 'OverlayInput${overlay}Out' in app_js
def test_overlay_group_repeat_uses_transition_out_not_all_off():
js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'Function: "OverlayInputAllOff"' not in js
for layer in range(1, 5):
assert f'Function: "OverlayInput{layer}Out"' in js
def test_build90_migrates_old_text_timer_transport_to_native_countdown(tmp_path: Path) -> None:
manager = UIBuilderManager(tmp_path, filename="ui.json")
saved = manager.save({
"version": 21,
"project_name": "Hockey",
"data_source": "hockey",
"canvas": {"width": 1440, "height": 900},
"tabs": [{"id": "main", "label": "Main"}],
"components": [],
"triggers": [],
"shortcut_sequences": [{
"id": "timers", "name": "Timers", "combo": "Space",
"steps": [{
"id": "sync", "type": "hockey_vmix_timers_start",
"game_vmix_mode": "text", "penalty_vmix_mode": "text",
}],
}],
})
step = saved["shortcut_sequences"][0]["steps"][0]
assert saved["version"] == 22
assert step["game_vmix_mode"] == "countdown"
assert step["penalty_vmix_mode"] == "countdown"
def test_build90_keeps_explicit_legacy_text_mode_after_v22(tmp_path: Path) -> None:
manager = UIBuilderManager(tmp_path, filename="ui.json")
saved = manager.save({
"version": 22,
"project_name": "Hockey",
"data_source": "hockey",
"canvas": {"width": 1440, "height": 900},
"tabs": [{"id": "main", "label": "Main"}],
"components": [], "triggers": [],
"shortcut_sequences": [{
"id": "timers", "name": "Timers", "combo": "Space",
"steps": [{
"id": "sync", "type": "hockey_vmix_timers_start",
"game_vmix_mode": "text", "penalty_vmix_mode": "text",
}],
}],
})
step = saved["shortcut_sequences"][0]["steps"][0]
assert step["game_vmix_mode"] == "text"
assert step["penalty_vmix_mode"] == "text"
def test_build90_native_countdown_avoids_per_second_transport() -> None:
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
assert 'game_vmix_mode !== "countdown"' in app_js
assert 'const countdownMode = step.penalty_vmix_mode === "countdown"' in app_js
assert 'Function: "StopCountdown"' in app_js
assert 'Function: "SuspendCountdown"' not in app_js
assert 'Function: "StopCountdown"' in app_js
assert 'signal: controller.signal' in app_js
assert 'vmixCommandQueue: Promise.resolve()' in app_js
# Per-second pushes are restricted to the legacy Text mirror map.
assert 'eventName === "timer_tick" && state.vmixTimerMirrors.has(component.action_id)' in app_js