"""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()