258 lines
11 KiB
Python
258 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: "PauseCountdown"' 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: "SetCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name' in app_js
|
|
assert 'Function: "SetCountdown", 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: "PauseCountdown"' 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
|