using System.IO; using System.Text.Json; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; /// /// Built-widget behavior pins for the pre-existing, already-shipped Type-8/Type-5 /// elements the OP2 double-REJECT findings named /// (`docs/research/2026-08-11-op2-review-blast.md` §1.1/§1.2, /// `docs/research/2026-08-11-op2-review-mechanism.md` §2.1/§2.2). These fixtures are /// NOT Options-panel data — they are the character sheet, spellbook, vendor, and /// combat panels' own committed fixtures — and every assertion here is a regression /// guard for the OP2 rework's dormancy model: a Type-8/Type-5 element that reaches /// without a controller opting it into the new /// mechanism must render and hit-test EXACTLY like the pre-OP2 /// fallback. /// /// /// Before this rework, these five Type-8 elements and every pre-existing Type-5 /// element silently re-classed to a bare--derived /// UiTabControl/UiTemplateListBox in production while every fixture- /// driven test still exercised the OLD stale-fixture shape (empty TabTable/ /// TemplateList) — a structural false negative the blast-radius review named as the /// root enabler. These tests build through the REAL (regenerated) fixtures, so a /// future regression that reintroduces eager wiring, media loss, or a ClickThrough /// flip WILL fail here. /// /// public class OP2ReworkBlastRadiusConformanceTests { // ── Type 8 — vendor's media-bearing, tab-table-less backdrop ──────────── /// /// Vendor `0x1000008D` (800x20) authors a DirectState fill and NO tab table — /// `RetailUiRuntime.cs` documents it by name as the vendor panel's own backdrop /// fill. Blast-review MUST-FIX 1(a): the OP2-as-landed unconditional-but-bare /// mapping stopped this drawing entirely. /// [Fact] public void Vendor_BackdropElement_DrawsItsAuthoredMedia_AndStaysClickThrough() { var layout = FixtureLoader.LoadVendor(); var backdrop = Assert.IsType(layout.FindElement(0x1000008Du)); Assert.Empty(backdrop.Tabs); // no authored 0x2E on this element Assert.False(backdrop.BehaviorActive); // nobody activates it — dormant Assert.True(backdrop.ClickThrough); // UiDatElement's generic-decoration default (uint file, int _) = backdrop.ActiveMedia(); Assert.NotEqual(0u, file); // the authored fill still draws } /// /// Vendor `0x100000B8` (the ACTUAL tab host, distinct from the `0x1000008D` /// backdrop above) authors a 3-entry tab table that `VendorUiController` /// already switches end-to-end. Same claim as the character/spellbook roots /// below: dormant on import, controller owns activation. /// [Fact] public void Vendor_TabHost_StaysDormant_ControllerOwnsSwitching() { var layout = FixtureLoader.LoadVendor(); var host = Assert.IsType(layout.FindElement(0x100000B8u)); Assert.Equal(3, host.Tabs.Count); // the authored table IS present… Assert.False(host.BehaviorActive); // …but nobody has activated it Assert.Equal(0u, host.ActivePageElementId); Assert.True(host.ClickThrough); } /// /// Blast review SHOULD-FIX 3 / NOTE 2 (2026-08-11 fix round): AD-73's /// dormancy contract is that a Type-8 host with an authored tab table /// performs NO switching until a controller calls /// — but nothing had pinned /// ITSELF against a dormant /// host (as opposed to BehaviorActive/ActivePageElementId, /// which the test above already covers). SwitchTo is public and /// its own doc invites a future direct call, so this guards the CURRENT /// dormant-by-default contract, not SwitchTo itself. /// [Fact] public void Vendor_TabHost_DormantHost_NeverRaisesActivePageChanged() { var layout = FixtureLoader.LoadVendor(); var host = Assert.IsType(layout.FindElement(0x100000B8u)); int raised = 0; host.ActivePageChanged += (_, _) => raised++; Assert.False(host.BehaviorActive); // R3 (OP3 re-review, 2026-08-11): a subscribe-then-assert-zero pin // passes even with dormancy fully broken. The REAL dormancy // mechanism is that ActivateTabBehavior (via RetailTabBinding. // SetClick) was never called, so the authored tab BUTTONS carry no // click handler — clicking a dormant host's tab can invoke nothing. // SwitchTo itself deliberately has no dormancy guard (a controller // may drive it directly), so the button wiring IS the contract. foreach (UiTabTableEntry tab in host.Tabs) { var button = layout.FindElement(tab.ButtonElementId); Assert.NotNull(button); Assert.False(button!.HandlesClick, $"dormant tab button 0x{tab.ButtonElementId:X8} has a click " + "handler — import-time activation regressed (AD-73)"); } Assert.Equal(0, raised); } // ── Type 8 — panel roots with their own controller-owned switching ────── /// /// Character sheet root `0x10000227` authors a 3-entry tab table but /// `CharacterStatController` already owns tab switching for this panel /// end-to-end. Blast-review MUST-FIX 1(b)/(c): the OP2-as-landed mapping made /// this element claim every unclaimed click in its bounds /// (ClickThrough false→ silently) and stop propagating /// state into its children. /// [Fact] public void CharacterRoot_StaysClickThrough_AndPropagatesState_WithoutActivation() { var layout = FixtureLoader.LoadCharacter(); var root = Assert.IsType(layout.Root); Assert.Equal(0x10000227u, root.DatElementId); Assert.NotEmpty(root.Tabs); // the authored table IS present… Assert.False(root.BehaviorActive); // …but nobody has activated it Assert.Equal(0u, root.ActivePageElementId); Assert.True(root.ClickThrough); Assert.IsAssignableFrom(root); } /// Spellbook root `0x100002A8` — same shape/claim as the character /// sheet, owned end-to-end by SpellbookWindowController. [Fact] public void SpellbookRoot_StaysClickThrough_AndPropagatesState_WithoutActivation() { var layout = FixtureLoader.LoadSpellbook(); var root = Assert.IsType(layout.Root); Assert.Equal(0x100002A8u, root.DatElementId); Assert.NotEmpty(root.Tabs); Assert.False(root.BehaviorActive); Assert.Equal(0u, root.ActivePageElementId); Assert.True(root.ClickThrough); Assert.IsAssignableFrom(root); } // ── Type 8 — combat, which has NO controller re-binder at all ─────────── /// /// Combat `0x100000A2` authors an 8-entry tab table and — unlike character/ /// spellbook/vendor — has NO controller that re-binds it after import. Blast- /// review MUST-FIX 1(d): the OP2-as-landed OnChildrenAttached ran at /// import time regardless, so this element would have taken over its 8 stance /// pages outright with nothing to override it. Every child must remain exactly /// as imported: all Visible (retail's plain default), no page hidden, no click /// bound. /// [Fact] public void Combat_TabHost_PerformsNoImportTimeTakeover() { var layout = FixtureLoader.LoadCombat(); var host = Assert.IsType(layout.FindElement(0x100000A2u)); Assert.Equal(8, host.Tabs.Count); // the authored table IS present… Assert.False(host.BehaviorActive); // …but nothing ever activates it Assert.Equal(0u, host.ActivePageElementId); Assert.Equal(16, host.Children.Count); // no children lost/consumed Assert.All(host.Children, child => Assert.True(child.Visible)); } // ── Type 5 — a representative pre-existing ListBox with an authored template ── /// /// Effects list `0x10000123` (`EffectsUiController.ListId`) authors a 1-row /// template array. Blast-review MUST-FIX 2: the OP2-as-landed unconditional /// mapping lost the authored media, defaulted ClickThrough=false, and /// unconditionally injected a hit-testable viewport as child 0 — before /// EffectsUiController.Bind ever runs. This pins the dormant baseline /// directly; EffectsUiControllerTests pins the controller's own /// subsequent behavior. /// [Fact] public void EffectsList_StaysDormant_ClickThrough_NoInjectedViewport() { var layout = FixtureLoader.LoadPositiveEffects(); var host = Assert.IsType( layout.FindElement(EffectsUiController.ListId)); Assert.NotEmpty(host.Templates); // the authored 0x64 array IS present… Assert.Empty(host.Children); // …but no viewport was injected Assert.Equal(0, host.ContentHeight); Assert.True(host.ClickThrough); } /// /// AD-73 addendum (OP2 re-review closure, 2026-08-11): /// is true where the /// pre-rework generic fallback's was not. That delta drops nothing ONLY while no /// authored Type-5 element carries children — a premise the re-review verified by /// hand across all 32 fixtures. This test turns that manual sweep into a build /// gate: if a future DAT regeneration surfaces a Type-5 element WITH authored /// children, this fails loudly instead of the children silently vanishing. /// [Fact] public void EveryAuthoredType5Element_HasZeroChildren_AcrossAllFixtures() { string fixturesDir = Path.Combine( AppContext.BaseDirectory, "UI", "Layout", "fixtures"); string[] fixtures = Directory.GetFiles(fixturesDir, "*.json"); Assert.NotEmpty(fixtures); var opts = new JsonSerializerOptions { IncludeFields = true }; int type5Seen = 0; foreach (string file in fixtures) { var root = JsonSerializer.Deserialize( File.ReadAllText(file), opts); Assert.NotNull(root); Walk(root!, file); } // The premise is only meaningful if the sweep actually saw Type-5 // elements (16 across the current fixture set). Assert.True(type5Seen >= 10, $"sweep saw only {type5Seen} Type-5 elements"); void Walk(ElementInfo node, string file) { if (node.Type == 5u) { type5Seen++; Assert.True( node.Children.Count == 0, $"{Path.GetFileName(file)}: Type-5 element 0x{node.Id:X8} authors " + $"{node.Children.Count} children — UiTemplateListBox.ConsumesDatChildren " + "would drop them; see AD-73's addendum."); } foreach (ElementInfo child in node.Children) Walk(child, file); } } }