using AcDream.App.UI; namespace AcDream.App.Tests.UI; /// /// #374 (Campaign OP gate 2): an OPEN UiMenu dropdown must get first claim on /// pointer events. UiElement.HitTest walks siblings front-to-back by z-order, /// so any sibling added AFTER the menu whose rect overlaps the popup area used /// to win the walk before the menu's extended OnHitTest union was consulted — /// on the Options panel's Config tab every dropdown has rows below it, so /// picking a Resolution item actually toggled the Full Screen / VSync rows /// underneath the popup. These tests drive the REAL UiRoot input path /// (synthetic OnMouseDown/OnMouseUp on a mounted tree), the exact class of /// coverage the fixture-conformance suites cannot provide (#372's lesson). /// public sealed class UiMenuPopupRoutingTests { private sealed class ClickRecorder : UiElement { public int MouseDowns; public int Clicks; public override bool HandlesClick => true; public override bool OnEvent(in UiEvent e) { if (e.Type == UiEventType.MouseDown) { MouseDowns++; return true; } if (e.Type == UiEventType.Click) { Clicks++; return true; } return false; } } private static (UiRoot root, UiMenu menu, ClickRecorder stealer, Func selected) BuildOverlappingTree() { var root = new UiRoot { Width = 800, Height = 600 }; var panel = new UiPanel { Left = 0, Top = 0, Width = 300, Height = 300 }; string? picked = null; var menu = new UiMenu { Left = 10, Top = 20, Width = 46, Height = 20, OpenUpward = false, Items = new[] { new UiMenu.MenuItem("A", "a"), new UiMenu.MenuItem("B", "b"), }, }; menu.OnSelect = p => picked = p as string; // The sibling "row below" — added AFTER the menu, so the front-to-back // sibling walk consults it FIRST. Its rect (screen y 44..74) overlaps // the open popup's first item row (screen y 45..62). var stealer = new ClickRecorder { Left = 0, Top = 44, Width = 250, Height = 30 }; panel.AddChild(menu); panel.AddChild(stealer); root.AddChild(panel); return (root, menu, stealer, () => picked); } private static void Click(UiRoot root, int x, int y) { root.OnMouseDown(UiMouseButton.Left, x, y); root.OnMouseUp(UiMouseButton.Left, x, y); } [Fact] public void OpenPopup_ItemClick_ReachesTheMenu_NotTheOverlappingFrontSibling() { var (root, menu, stealer, selected) = BuildOverlappingTree(); // CONTROL: with the menu closed, this exact point belongs to the // stealer — proving the overlap is real, so the popup assertion below // cannot pass vacuously on wrong geometry. Click(root, 30, 50); Assert.Equal(1, stealer.MouseDowns); Click(root, 30, 30); // the menu button (screen 10..56 x 20..40) Assert.True(menu.IsOpen); // First popup item row: menu-local (20, 30) → interior (15, 5) → row 0. // The same screen point the control click just proved the stealer owns. Click(root, 30, 50); Assert.Equal("a", selected()); Assert.False(menu.IsOpen); Assert.Equal(1, stealer.MouseDowns); // unchanged — the popup won Assert.Equal(1, stealer.Clicks); } [Fact] public void OpenPopup_OutsidePress_DismissesAndSwallows_ThenNormalRoutingResumes() { var (root, menu, stealer, selected) = BuildOverlappingTree(); Click(root, 30, 30); Assert.True(menu.IsOpen); // (230, 60): inside the stealer's rect (x < 250, y in 44..74) but past // the popup's right edge (screen x 10 + outer width 201 = 211). The // dismissing press must close the popup WITHOUT acting on the widget // underneath — the standard dropdown-dismiss gesture, and exactly the // accidental-toggle hazard #374's gate session demonstrated. Click(root, 230, 60); Assert.False(menu.IsOpen); Assert.Null(selected()); Assert.Equal(0, stealer.MouseDowns); Assert.Equal(0, stealer.Clicks); // With the popup gone, the same spot routes normally again. Click(root, 230, 60); Assert.Equal(1, stealer.MouseDowns); Assert.Equal(1, stealer.Clicks); } [Fact] public void ReopeningAfterDismiss_StillRoutesItemClicks() { var (root, menu, _, selected) = BuildOverlappingTree(); Click(root, 30, 30); // open Click(root, 230, 60); // dismiss (swallowed) Assert.False(menu.IsOpen); Click(root, 30, 30); // reopen Assert.True(menu.IsOpen); Click(root, 30, 50); // pick item A again Assert.Equal("a", selected()); } [Fact] public void HidingThePopupsWindowWhileOpen_SelfHeals_NoStalePopupRouting() { var (root, menu, stealer, selected) = BuildOverlappingTree(); Click(root, 30, 30); Assert.True(menu.IsOpen); // The panel hosting the menu closes while the popup is open (e.g. the // Options panel is toggled shut). The stale registration must not keep // swallowing clicks for an invisible popup. menu.Parent!.Visible = false; Click(root, 230, 60); // self-heal press (dismissed, swallowed) Assert.False(menu.IsOpen); Click(root, 230, 60); // The stealer is inside the hidden panel too, so the press falls // through to nothing — the point is that the popup no longer claims it. Assert.Equal(0, stealer.MouseDowns); menu.Parent!.Visible = true; Click(root, 230, 60); Assert.Equal(1, stealer.MouseDowns); Assert.Null(selected()); } }