Files
EscapistsReintroduced/tests/test_admin_menu_sources.py

132 lines
6.4 KiB
Python

"""Generated UI stays reproducible; design edits cannot silently break includes/navigation."""
import importlib.util
import json
from pathlib import Path
import re
import shutil
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('menu_builder', ROOT / 'tools/build_admin_menu.py')
builder = importlib.util.module_from_spec(spec)
spec.loader.exec_module(builder)
class MenuSourceTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.source = Path(self.temp.name) / 'menu'
shutil.copytree(builder.SOURCE, self.source)
def edit_json(self, name, edit):
path = self.source / name
data = json.loads(path.read_text())
edit(data)
path.write_text(json.dumps(data))
def test_committed_outputs_match_sources_and_widget_ids_are_unique(self):
for path, rendered in builder.render().items():
self.assertEqual((ROOT / path).read_text(), rendered, str(path))
self.assertNotIn('{{', rendered)
layout = builder.render()[builder.LAYOUTS / 'AdminMenu.layout']
names = re.findall(r'\bName "([^"]+)"', layout)
self.assertEqual(len(names), len(set(names)))
def test_theme_edit_reaches_pages_rows_and_runtime_colors(self):
def change(theme):
theme['layout']['COLOR_TEXT'] = '1 0.5 0.25 1'
theme['buttons']['POSITIVE'] = '0xFF123456'
self.edit_json('theme.json', change)
rendered = builder.render(self.source)
for name in ('AdminMenu.layout', 'ContentRow.layout', 'ItemRow.layout', 'ItemWeightRow.layout'):
self.assertIn('Color 1 0.5 0.25 1', rendered[builder.LAYOUTS / name])
self.assertNotIn('Color 0.9 0.92 0.94 1', rendered[builder.LAYOUTS / name])
self.assertIn('POSITIVE = 0xFF123456', rendered[builder.PALETTE])
def test_relabel_and_reorder_groups_preserves_stable_page_ids(self):
def change(nav):
nav['groups'].reverse()
nav['pages'][21]['label'] = 'Special forces'
self.edit_json('navigation.json', change)
rendered = builder.render(self.source)
self.assertIn('SARKA = 21', rendered[builder.PAGES])
layout = rendered[builder.LAYOUTS / 'AdminMenu.layout']
self.assertIn('m_sText "Special forces"', layout)
self.assertLess(layout.index('Name "Main_Status"'), layout.index('Name "Main_Mission"'))
def test_top_tabs_and_sidebar_do_not_overlap_content(self):
layout = builder.render()[builder.LAYOUTS / 'AdminMenu.layout']
slots = dict(re.findall(r'Name "([^"]+)" Slot FrameWidgetSlot \{([^}]+)\}', layout))
def rect(name):
slot = slots[name]
return [int(re.search(r'\b' + key + r' (-?\d+)', slot)[1])
for key in ('PositionX', 'PositionY', 'SizeX', 'SizeY')]
nav = json.loads((builder.SOURCE / 'navigation.json').read_text())
pages = {p['key']: p for p in nav['pages']}
panel_width, panel_height = rect('Panel')[2:]
previous_right = 0
for group in nav['groups']:
x, y, width, height = rect('Main_' + group['name'])
self.assertGreaterEqual(x, previous_right)
self.assertLessEqual(x + width, panel_width)
previous_right = x + width
sx, sy, sw, sh = rect('Sidebar_' + group['name'])
previous_bottom = 0
for key in group['pages']:
page = pages[key]
px, py, pw, ph = rect(page['widget'])
bx, by, bw, bh = rect(page['button'])
self.assertLessEqual(y + height, py)
self.assertLessEqual(sx + bx + bw, px)
self.assertGreaterEqual(by, previous_bottom)
self.assertLessEqual(bx + bw, sw)
self.assertLessEqual(by + bh, sh)
self.assertGreaterEqual(sy + by, py)
self.assertLessEqual(sy + by + bh, py + ph)
self.assertLessEqual(px + pw, panel_width)
self.assertLessEqual(py + ph, panel_height)
previous_bottom = by + bh
def test_editor_initial_visibility_matches_mission_page_even_after_reordering(self):
self.edit_json('navigation.json', lambda nav: nav['groups'].reverse())
layout = builder.render(self.source)[builder.LAYOUTS / 'AdminMenu.layout']
visibility = dict((name, int(value)) for value, name in re.findall(
r'WidgetClass \{ "Is Visible" ([01]) Name "([^"]+)"', layout))
nav = json.loads((self.source / 'navigation.json').read_text())
for page in nav['pages']:
self.assertEqual(visibility[page['widget']], int(page['key'] == 'MISSION'))
self.assertEqual(visibility['Selected_' + page['button']], int(page['key'] == 'MISSION'))
for group in nav['groups']:
expected = int('MISSION' in group['pages'])
self.assertEqual(visibility['Sidebar_' + group['name']], expected)
self.assertEqual(visibility['Selected_Main_' + group['name']], expected)
def test_unknown_token_missing_include_cycle_and_escape_fail(self):
shell = self.source / 'shell.layout.in'
for content in ('{{TYPO}}', '{{include:missing.layout.in}}',
'{{include:shell.layout.in}}', '{{include:../outside.layout.in}}'):
with self.subTest(content=content):
shell.write_text(content)
with self.assertRaises(ValueError):
builder.render(self.source)
def test_duplicate_membership_missing_page_and_invalid_theme_fail(self):
original = (self.source / 'navigation.json').read_text()
for change in (lambda n: n['groups'][0]['pages'].append('SARKA'),
lambda n: n['groups'][0]['pages'].pop(),
lambda n: n['pages'][0].update(id=99)):
(self.source / 'navigation.json').write_text(original)
self.edit_json('navigation.json', change)
with self.assertRaises(ValueError):
builder.render(self.source)
(self.source / 'navigation.json').write_text(original)
self.edit_json('theme.json', lambda t: t['layout'].update(COLOR_TEXT='1 1 1 2'))
with self.assertRaises(ValueError):
builder.render(self.source)
if __name__ == '__main__':
unittest.main()