118 lines
5.6 KiB
Python
118 lines
5.6 KiB
Python
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
from hockey_data.agent_bridge import _stable_vmix_inventory_fingerprint
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def test_stable_project_fingerprint_ignores_input_number_order_and_title_when_key_exists():
|
|
before = [
|
|
{
|
|
"key": "key-score",
|
|
"number": "22",
|
|
"title": "Верхний счёт",
|
|
"type": "GT",
|
|
"fields": [
|
|
{"name": "Таймер.Text", "type": "text", "index": "0"},
|
|
{"name": "HOME.Text", "type": "text", "index": "1"},
|
|
],
|
|
},
|
|
{
|
|
"key": "key-penalty",
|
|
"number": "114",
|
|
"title": "Удаление HOME",
|
|
"type": "GT",
|
|
"fields": [{"name": "Таймер.Text", "type": "text", "index": "0"}],
|
|
},
|
|
]
|
|
after = [
|
|
{
|
|
"key": "key-penalty",
|
|
"number": "7",
|
|
"title": "Удаление HOME (новое имя)",
|
|
"type": "GT",
|
|
"fields": [{"name": "Таймер.Text", "type": "text", "index": "4"}],
|
|
},
|
|
{
|
|
"key": "key-score",
|
|
"number": "91",
|
|
"title": "Счёт",
|
|
"type": "GT",
|
|
"fields": [
|
|
{"name": "HOME.Text", "type": "text", "index": "9"},
|
|
{"name": "Таймер.Text", "type": "text", "index": "2"},
|
|
],
|
|
},
|
|
]
|
|
assert _stable_vmix_inventory_fingerprint(before) == _stable_vmix_inventory_fingerprint(after)
|
|
|
|
|
|
def test_legacy_project_fingerprint_uses_title_instead_of_number():
|
|
before = [{"key": "", "number": "3", "title": "Score", "type": "GT", "fields": []}]
|
|
after = [{"key": "", "number": "99", "title": "Score", "type": "GT", "fields": []}]
|
|
assert _stable_vmix_inventory_fingerprint(before) == _stable_vmix_inventory_fingerprint(after)
|
|
|
|
|
|
def _run_functions(source, names, scenario):
|
|
# Execute the shipped function bodies; test doubles supply only page state.
|
|
functions = []
|
|
for name in names:
|
|
match = re.search(r"(?m)^ function " + re.escape(name) + r"\(", source)
|
|
assert match, name
|
|
following = re.search(r"(?m)^ (?:async )?function ", source[match.end():])
|
|
assert following, name
|
|
functions.append(source[match.start():match.end() + following.start()])
|
|
script = "const assert = require('node:assert/strict');\n" + "\n".join(functions) + "\n" + scenario
|
|
result = subprocess.run(["node", "-e", script], text=True, capture_output=True, timeout=10)
|
|
assert result.returncode == 0, result.stderr
|
|
|
|
|
|
def test_shortcut_editor_persists_title_and_migrates_legacy_selectors():
|
|
app_js = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8")
|
|
_run_functions(app_js, ["vmixInventoryInputs", "vmixInputRef", "vmixInputByRef"], r'''
|
|
const score = {key: 'score-guid', number: '5', title: 'Score'};
|
|
const other = {key: 'other-guid', number: '9', title: 'Other'};
|
|
const state = {shortcutInventory: {inventory: {inputs: [score, other]}}};
|
|
assert.equal(vmixInputRef(score), 'Score');
|
|
assert.equal(vmixInputRef({key: 'only-guid', number: '5'}), '');
|
|
for (const legacy of ['score-guid', '5']) assert.equal(vmixInputRef(vmixInputByRef(legacy)), 'Score');
|
|
const saved = vmixInputRef(score);
|
|
score.number = '9'; other.number = '5'; score.key = 'new-score-guid';
|
|
assert.equal(vmixInputByRef(saved), score);
|
|
assert.equal(vmixInputRef(vmixInputByRef(saved)), 'Score');
|
|
assert.equal(vmixInputByRef('missing'), null);
|
|
''')
|
|
|
|
|
|
def test_mapping_links_save_exact_title_and_clear_every_positional_number():
|
|
mapping_js = (ROOT / "hockey_data" / "static" / "admin-directories.js").read_text(encoding="utf-8")
|
|
_run_functions(mapping_js, ["mappingInventoryInputs", "mappingFieldKind", "mappingRowMatchesInput",
|
|
"mappingInputTitleProblem", "mappingSetLink", "collectMappingFields"], r'''
|
|
const input = {key: 'current-guid', number: '17', title: 'Score', fields: [{name: 'Home.Text', type: 'text'}]};
|
|
const profile = {inventory: {inputs: [input]}, fields: [{data_key:'old', vmix_input_key:'old-guid',
|
|
vmix_input_number:'5', vmix_input_title:'Score', vmix_field:'Home.Text', rule:{enabled:true, left_key:'old'}}]};
|
|
const state = {mappingActiveProfile:profile};
|
|
const statuses = []; function setStatus(message){statuses.push(message);}
|
|
mappingSetLink(profile, input, input.fields[0], {key:'score.home'});
|
|
assert.equal(profile.fields.length, 1);
|
|
assert.equal(profile.fields[0].vmix_input_title, 'Score');
|
|
assert.equal(profile.fields[0].vmix_input_key, 'current-guid');
|
|
assert.equal(profile.fields[0].vmix_input_number, '');
|
|
assert.equal(profile.fields[0].rule.left_key, 'score.home');
|
|
profile.fields.push({data_key:'legacy', vmix_input_key:'another-guid', vmix_input_number:'42', vmix_input_title:'Other', vmix_field:'Away.Text'});
|
|
profile.fields.push({data_key:'orphaned-legacy', vmix_input_key:'', vmix_input_number:'7', vmix_input_title:'', vmix_field:'Legacy.Text'});
|
|
const collected = collectMappingFields();
|
|
assert.deepEqual(collected.map(row=>row.vmix_input_number), ['', '', '']);
|
|
assert.deepEqual(collected.map(row=>row.vmix_input_title), ['Score', 'Other', '']);
|
|
assert.deepEqual(collected.map(row=>row.sort_order), [0, 1, 2]);
|
|
assert.equal(profile.fields[1].vmix_input_number, '42', 'Collecting must not mutate draft rows');
|
|
const previous = JSON.stringify(profile.fields);
|
|
profile.inventory.inputs.push({...input, key:'duplicate-guid', number:'18'});
|
|
mappingSetLink(profile, input, input.fields[0], {key:'unsafe'});
|
|
assert.equal(JSON.stringify(profile.fields), previous);
|
|
assert.ok(statuses.length && statuses[0].includes('не уникально'));
|
|
''')
|