acdream/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
Erik 74c3d85d37 test(ui): FA3 -- social panel fixture dump, live-mount probe, conformance tests
- RetailLayoutFixtureGenerator dumps LayoutDesc 0x2100006E slot
  0x1000018F to the committed social_panel_2100006E_1000018F.json
  fixture, closing lane-A unknowns U3/U4/U6/U7/U10 with real DAT data
  (geometry/media/fonts/base refs, the fellowship empty/full frame
  containment, the panel/page P0x57 properties, the row-template arrays).
- SocialPanelLiveMountProbeTests exercises the PRODUCTION mount path
  against the live DATs (ACDREAM_PROBE_LIVE_MOUNT=1): the tab host
  resolves as UiTabPanel with its 4-entry table, all four pages resolve,
  the fellowship frame pair and allegiance signature elements resolve,
  0x10000492 is confirmed authored twice under the allegiance page, and
  every tab button caption is non-empty (the #375 resolver class).
- SocialPanelControllerTests pins the fixture-driven conformance: the
  real (coordinator-addendum-correcting) tab table and default entry,
  the Fellowship/Allegiance empty-state gates, Friends/Squelch row
  population and revision-driven rebuilds, and the D1 INERT-button
  contract (AD-79) via
  FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler.
- RetailPanelCatalogTests gains the SocialPanel id/window-name/
  Mounted-not-Toolbar pins (lane A §6.1: no toolbar button).

Note: the ACDREAM_REGENERATE_UI_FIXTURES=1 run used to produce the new
fixture also touched keyboard_config_21000009.json and
options_2100002B.json on this machine (unrelated installed-DAT drift,
likely from local DAT-editing tooling) -- both were reverted to HEAD
before this commit; only the new social panel fixture is included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 02:58:28 +02:00

209 lines
9.4 KiB
C#

using System.IO;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using DatReaderWriter;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Campaign FA slice FA3, item 7: extends the ACDREAM_PROBE_LIVE_MOUNT pattern
/// (the #372/#375/#378 lesson — fixture-green alone is NOT acceptance for
/// anything mounted) to the four-tab social panel. Asserts the PRODUCTION
/// mount path resolves: the tab host as <see cref="UiTabPanel"/> with its
/// 4-entry table, all four page elements, the fellowship empty/full frame
/// pair, the allegiance signature elements, and non-empty captions on the
/// tab buttons (the #375 resolver class). Also dumps the raw tab table so
/// its button→page pairing + default entry can be recorded — coordinator
/// addendum, docs/research/2026-08-11-fa-panel-structure.md §10.
/// </summary>
public sealed class SocialPanelLiveMountProbeTests
{
[Fact]
public void ProbeLiveMountShapes()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
return;
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var strings = new DatStringResolver(dats);
ElementInfo? root = LayoutImporter.ImportInfos(
dats, SocialPanelController.HostLayoutId, SocialPanelController.SlotElementId);
Assert.NotNull(root);
ImportedLayout layout = LayoutImporter.Build(
root!, _ => (1u, 8, 8), null, null, strings.Resolve);
Console.WriteLine(
$"[socialprobe] root id=0x{root!.Id:X8} type={root.Type} "
+ $"({root.X},{root.Y} {root.Width}x{root.Height}) children={root.Children.Count} "
+ $"restorePrevious={(root.TryGetEffectiveBool(RetailPanelUiController.RestorePreviousPropertyId, out bool rp) && rp)}");
foreach (ElementInfo c in root.Children)
{
string p12 = c.TryGetEffectiveProperty(0x12u, out var v12) ? $"0x{v12.UnsignedValue:X8}" : "ABSENT";
string p57 = c.TryGetEffectiveProperty(0x57u, out var v57) ? $"{v57.Kind}=0x{v57.UnsignedValue:X8}" : "ABSENT";
Console.WriteLine(
$"[socialprobe] root-child 0x{c.Id:X8} type=0x{c.Type:X8} ({c.X},{c.Y} {c.Width}x{c.Height}) "
+ $"kids={c.Children.Count} P0x12={p12} P0x57={p57}");
}
{
string rootP57 = root.TryGetEffectiveProperty(0x57u, out var rv57) ? $"{rv57.Kind}=0x{rv57.UnsignedValue:X8}" : "ABSENT";
Console.WriteLine($"[socialprobe] root P0x57={rootP57}");
}
UiTabPanel tabs = Assert.IsType<UiTabPanel>(layout.Root);
Console.WriteLine($"[socialprobe] tab table entries={tabs.Tabs.Count}");
foreach (UiTabTableEntry t in tabs.Tabs)
Console.WriteLine(
$"[socialprobe] button=0x{t.ButtonElementId:X8} page=0x{t.PageElementId:X8} default={t.IsDefault}");
Assert.True(tabs.Tabs.Count >= 4);
// Production mount: activate behavior exactly like SocialPanelController.ActivateTabs.
tabs.ActivateTabBehavior();
Assert.Empty(tabs.UnresolvedEntries);
foreach ((uint pageId, string name) in new[]
{
(0x10000513u, "Friends"),
(0x10000291u, "Allegiance"),
(0x10000292u, "Fellowship"),
(0x1000054Au, "Squelch"),
})
{
UiElement? page = UiElement.FindDescendant(tabs, pageId);
Console.WriteLine(
$"[socialprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : page.GetType().Name)}");
Assert.NotNull(page);
}
// Fellowship empty/full frame pair.
foreach ((uint id, string name) in new[]
{
(0x1000026Bu, "NotInAFellowshipFrame"),
(0x10000275u, "InAFellowshipFrame"),
})
{
UiElement? el = UiElement.FindDescendant(tabs, id);
Console.WriteLine($"[socialprobe] fellowship frame {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
Assert.NotNull(el);
}
// Allegiance signature elements.
foreach ((uint id, string name) in new[]
{
(0x10000255u, "MonarchField"),
(0x1000025Au, "PatronField"),
(0x10000260u, "VassalListBox"),
(0x10000263u, "SwearButton"),
(0x10000264u, "BreakButton"),
(0x10000265u, "KickButton"),
})
{
UiElement? el = UiElement.FindDescendant(tabs, id);
Console.WriteLine($"[socialprobe] allegiance element {name} 0x{id:X8} -> {(el is null ? "MISSING" : el.GetType().Name)}");
Assert.NotNull(el);
}
// 0x10000492 is authored TWICE inside the allegiance page (monarch
// block child AND patron block child) — count occurrences under the
// allegiance page root specifically, not the whole tree.
UiElement? allegiancePage = UiElement.FindDescendant(tabs, 0x10000291u);
Assert.NotNull(allegiancePage);
int passupCount = CountDescendants(allegiancePage!, 0x10000492u);
Console.WriteLine($"[socialprobe] 0x10000492 occurrences under allegiance page = {passupCount}");
// Tab button captions — non-empty (the #375 resolver class: a missing
// string resolver renders blank captions even though the layout mounts).
foreach (UiTabTableEntry t in tabs.Tabs)
{
UiElement? button = UiElement.FindDescendant(tabs, t.ButtonElementId);
string? caption = button switch
{
UiText text => text.LinesProvider?.Invoke() is { Count: > 0 } lines ? lines[0].Text : null,
UiButton btn => btn.Label,
_ => null,
};
Console.WriteLine(
$"[socialprobe] tab button 0x{t.ButtonElementId:X8} ({button?.GetType().Name}) caption='{caption}'");
Assert.False(string.IsNullOrEmpty(caption));
}
// U6: containment of the fellowship empty/full frames.
ElementInfo? notInFellowshipFrame = FindInfo(root, 0x1000026Bu);
ElementInfo? inFellowshipFrame = FindInfo(root, 0x10000275u);
Console.WriteLine(
$"[socialprobe] 0x1000026B (NotInAFellowshipFrame) children: "
+ string.Join(",", (notInFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}")));
Console.WriteLine(
$"[socialprobe] 0x10000275 (InAFellowshipFrame) children: "
+ string.Join(",", (inFellowshipFrame?.Children ?? new()).ConvertAll(c => $"0x{c.Id:X8}")));
// Dump two levels of children under each page slot for U3/U4/U6/U7 sweep.
foreach (uint pageId in new[] { 0x10000513u, 0x10000291u, 0x10000292u, 0x1000054Au })
{
ElementInfo? pageInfo = FindInfo(root, pageId);
if (pageInfo is null)
{
Console.WriteLine($"[socialprobe] page-info 0x{pageId:X8} MISSING from ElementInfo tree");
continue;
}
Console.WriteLine(
$"[socialprobe] page-info 0x{pageId:X8} P0x57={(pageInfo.TryGetEffectiveProperty(0x57u, out var p57) ? p57.UnsignedValue.ToString() : "ABSENT")} children={pageInfo.Children.Count}");
DumpInfoTree(pageInfo, 1, maxDepth: 3);
}
// Friends/Squelch row templates — find the name-text child.
foreach ((uint tLayout, uint tElement, string name) in new[]
{
(0x2100005Du, 0x10000519u, "FriendsRow"),
(0x21000060u, 0x10000541u, "SquelchRow"),
})
{
ElementInfo? row = LayoutImporter.ImportInfos(dats, tLayout, tElement);
if (row is null)
{
Console.WriteLine($"[socialprobe] {name} template 0x{tLayout:X8}/0x{tElement:X8} IMPORT NULL");
continue;
}
Console.WriteLine($"[socialprobe] {name} template:");
DumpInfoTree(row, 1, maxDepth: 3);
}
}
private static void DumpInfoTree(ElementInfo info, int depth, int maxDepth)
{
if (depth > maxDepth) return;
string templates = info.TemplateList.Count > 0
? $" templates={info.TemplateList.Count}[{string.Join(",", info.TemplateList.ConvertAll(t => $"0x{t.TemplateLayoutId:X8}/0x{t.TemplateElementId:X8}"))}] scrollbar=0x{info.ScrollbarElementId:X8}"
: "";
Console.WriteLine(
$"[socialprobe] {new string(' ', depth * 2)}0x{info.Id:X8} type=0x{info.Type:X8} "
+ $"({info.X},{info.Y} {info.Width}x{info.Height}) kids={info.Children.Count}{templates}");
foreach (ElementInfo c in info.Children)
DumpInfoTree(c, depth + 1, maxDepth);
}
private static int CountDescendants(UiElement root, uint id)
{
int count = root.DatElementId == id ? 1 : 0;
foreach (UiElement child in root.Children)
count += CountDescendants(child, id);
return count;
}
private static ElementInfo? FindInfo(ElementInfo root, uint id)
{
if (root.Id == id) return root;
foreach (ElementInfo c in root.Children)
{
ElementInfo? found = FindInfo(c, id);
if (found is not null) return found;
}
return null;
}
}