From ae77270939a4ceab61dfb284de78a02e7b042cba Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 03:44:05 +0200 Subject: [PATCH] fix(ui): fellowship checkbox count, row-text + ListBox Flush doc corrections, promote probe findings to assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanism SHOULD-FIX 4: SocialFellowshipPageController's class doc said 0x1000026B holds "all three visible option checkboxes" — the fixture and the decomp both authors FOUR (Ignore Fellowship Requests / Auto-Accept Requests / Share XP / Share Loot, 0x10000270-0x10000273). Corrected. Mechanism SHOULD-FIX 9: SocialPanelRowText.FindDeepest's doc promised "the deepest UiText descendant" but the implementation returns the LAST match in pre-order traversal order, which only equals the deepest when the subtree is a single chain. Both real row templates ARE single chains today, so behavior is unaffected — the doc now describes what the code actually does instead of a stronger guarantee it doesn't implement. Blast SHOULD-FIX 5: UiTemplateListBox.Flush()'s doc only mentioned the ContentHeight reset; UiScrollablePanel.ClearContent() also resets scroll position to 0, which the sibling UiItemList.Flush() (same method name, different semantics) does NOT do. Documented explicitly, including the UX cost this creates for a scrolled-in Friends/Squelch roster once its scrollbar is wired (this fix round's blast MF-1) — flagged for whoever revisits Friends/Squelch scrolling next rather than silently fixed as an unasked behavior change. Mechanism SHOULD-FIX 3: SocialPanelLiveMountProbeTests printed two headline findings (the 0x10000492-authored-twice count, page exclusivity after ActivateTabBehavior) without ever asserting them — a future importer regression collapsing/dropping an instance, or breaking exclusivity, could not fail this test. Both are now real assertions (Assert.Equal(2, passupCount); exactly one page Visible and it is Allegiance). Co-Authored-By: Claude Fable 5 --- .../Layout/SocialFellowshipPageController.cs | 4 +++- .../UI/Layout/SocialPanelRowText.cs | 17 +++++++++++--- src/AcDream.App/UI/UiTemplateListBox.cs | 19 +++++++++++++++- .../Layout/SocialPanelLiveMountProbeTests.cs | 22 ++++++++++++++++++- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs index d652fe8c..f94d6c9c 100644 --- a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs +++ b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs @@ -16,7 +16,9 @@ namespace AcDream.App.UI.Layout; /// m_pFellowship == null, and vice-versa. Both are AUTHORED sibling /// containers of the fellowship page (0x10000292) — confirmed by the /// FA3 live-mount probe: 0x1000026B holds the name-entry box, Create -/// button, and all three visible option checkboxes; 0x10000275 holds +/// button, and all FOUR visible option checkboxes (Ignore Fellowship +/// Requests / Auto-Accept Requests / Share XP / Share Loot — +/// 0x100002700x10000273, fix-round mechanism SF-4); 0x10000275 holds /// the roster list and all six member-management buttons. A single /// toggle on each container is therefore the /// WHOLE empty-state swap; no per-child hiding is needed (closes lane-A diff --git a/src/AcDream.App/UI/Layout/SocialPanelRowText.cs b/src/AcDream.App/UI/Layout/SocialPanelRowText.cs index 85e824af..eb1595ac 100644 --- a/src/AcDream.App/UI/Layout/SocialPanelRowText.cs +++ b/src/AcDream.App/UI/Layout/SocialPanelRowText.cs @@ -16,9 +16,20 @@ namespace AcDream.App.UI.Layout; /// internal static class SocialPanelRowText { - /// Returns the deepest descendant of - /// (pre-order, last match wins), or null if the - /// built row contains no text element at all. + /// + /// Returns the LAST found by a pre-order walk of + /// (row itself, then each child's own subtree in + /// order), or null if the built row contains no text element at all. + /// Fix-round mechanism SF-9: this equals the truly DEEPEST match + /// only when the subtree is a single chain — both real row templates + /// (Friends 0x10000519, Squelch 0x10000541) ARE single + /// chains (row root -> one Type-0xC text child -> one more Type-0xC + /// text grandchild, per the FA3 live-mount probe), so this method's + /// actual "last match in traversal order" behavior and the class doc's + /// "deepest" framing agree for both templates in production today. A + /// row template with a BRANCHING subtree (multiple text leaves at + /// different depths) would expose the difference. + /// public static UiText? FindDeepest(UiElement row) { UiText? found = row as UiText; diff --git a/src/AcDream.App/UI/UiTemplateListBox.cs b/src/AcDream.App/UI/UiTemplateListBox.cs index 7da7f4de..1560f404 100644 --- a/src/AcDream.App/UI/UiTemplateListBox.cs +++ b/src/AcDream.App/UI/UiTemplateListBox.cs @@ -239,7 +239,10 @@ public sealed class UiTemplateListBox : UiDatElement /// /// Campaign FA slice FA3: removes every row previously added via /// /, resetting - /// to 0 — the "Gap found" prerequisite lane A/D + /// to 0 AND the scroll position to 0 + /// ( calls + /// Scroll.SetScrollY(0) — fix-round blast SF-5: the ORIGINAL doc here + /// only mentioned the height reset) — the "Gap found" prerequisite lane A/D /// flagged (docs/research/2026-08-11-fa-panel-structure.md §6.6: "no Flush, no /// selection model, no per-row instance-id"). Needed for a poll-and-rebuild /// binding (Friends/Squelch read-only lists) where the row COUNT can shrink @@ -247,6 +250,20 @@ public sealed class UiTemplateListBox : UiDatElement /// grows the stack. A no-op, never allocating the viewport, when the box is /// still dormant (no row has ever been added) — mirrors every other dormancy /// guard on this class (see class doc). + /// + /// + /// Diverges from the sibling it shares a + /// name with (fix-round blast SF-5): that method does NOT touch scroll + /// position. For a poll-and-rebuild Friends/Squelch list, this means a user + /// scrolled partway into a long roster is yanked back to the top on every + /// server-side change (a friend's online status flips, etc.) — a real, + /// user-visible UX cost once is actually + /// wired (it now is — see ). + /// Not fixed here: doing so would require this class to preserve/re-clamp + /// the offset across a rebuild whose row COUNT can shrink, which is a + /// bigger behavior change than a fix-round doc correction should make + /// unasked; flagged for whoever revisits Friends/Squelch scrolling next. + /// /// public void Flush() => _viewport?.ClearContent(); } diff --git a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs index 9025e47f..d4fecac9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs @@ -67,6 +67,11 @@ public sealed class SocialPanelLiveMountProbeTests tabs.ActivateTabBehavior(); Assert.Empty(tabs.UnresolvedEntries); + // Fix-round mechanism SF-3: page exclusivity was activated but never + // asserted — the #372 class ("the panel mounts but the pages are + // wrong/blank") deserves a real assertion, not just a hope. + bool sawVisiblePage = false; + string? visiblePageName = null; foreach ((uint pageId, string name) in new[] { (0x10000513u, "Friends"), @@ -77,9 +82,20 @@ public sealed class SocialPanelLiveMountProbeTests { UiElement? page = UiElement.FindDescendant(tabs, pageId); Console.WriteLine( - $"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)}"); + $"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)} " + + $"Visible={page?.Visible}"); Assert.NotNull(page); + if (page!.Visible) + { + Assert.False( + sawVisiblePage, + $"page exclusivity violated: both '{visiblePageName}' and '{name}' report Visible=true after ActivateTabBehavior()"); + sawVisiblePage = true; + visiblePageName = name; + } } + Assert.True(sawVisiblePage, "no page reports Visible=true after ActivateTabBehavior()"); + Assert.Equal("Allegiance", visiblePageName); // Fellowship empty/full frame pair. foreach ((uint id, string name) in new[] @@ -116,6 +132,10 @@ public sealed class SocialPanelLiveMountProbeTests Assert.NotNull(allegiancePage); int passupCount = CountDescendants(allegiancePage!, 0x10000492u); Console.WriteLine($"[socialprobe] 0x10000492 occurrences under allegiance page = {passupCount}"); + // Fix-round mechanism SF-3: this was printed but never asserted — a + // future importer change that collapses or drops one instance must + // fail this test, not just the log. + Assert.Equal(2, passupCount); // Tab button captions — non-empty (the #375 resolver class: a missing // string resolver renders blank captions even though the layout mounts).