- 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>
292 lines
12 KiB
C#
292 lines
12 KiB
C#
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Social;
|
|
using AcDream.Runtime;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Controller-level tests for <see cref="SocialPanelController"/> against the
|
|
/// committed <c>social_panel_2100006E_1000018F.json</c> fixture (Campaign FA
|
|
/// slice FA3) — the SAME <c>LayoutImporter.ImportInfos(dats, 0x2100006Eu,
|
|
/// 0x1000018Fu)</c> catalog import <see cref="RetailUiRuntime.MountSocialPanel"/>
|
|
/// performs against real DATs. No DAT access, no live runtime — dat-free per
|
|
/// the established <see cref="FixtureLoader"/> pattern.
|
|
/// </summary>
|
|
public sealed class SocialPanelControllerTests
|
|
{
|
|
private static SocialPanelController.Callbacks MakeCallbacks(
|
|
List<string>? calls = null,
|
|
RuntimeFellowshipSnapshot fellowship = default,
|
|
RuntimeAllegianceSnapshot allegiance = default,
|
|
FriendsState? friends = null,
|
|
SquelchState? squelch = null)
|
|
{
|
|
calls ??= new List<string>();
|
|
return new SocialPanelController.Callbacks(
|
|
Toggle: () => calls.Add("toggle"),
|
|
FellowshipSnapshot: () => fellowship,
|
|
AllegianceSnapshot: () => allegiance,
|
|
Friends: friends ?? new FriendsState(),
|
|
Squelch: squelch ?? new SquelchState(),
|
|
TemplateResolver: FakeRowTemplateResolver);
|
|
}
|
|
|
|
/// <summary>Mirrors the real Friends/Squelch row templates' own two-deep
|
|
/// nested-text shape (FA3 live-mount probe) so
|
|
/// <see cref="SocialPanelRowText.FindDeepest"/> has something realistic
|
|
/// to resolve, without needing the templates' own (uncommitted, separate
|
|
/// LayoutDesc) fixtures.</summary>
|
|
private static UiElement? FakeRowTemplateResolver(uint layoutId, uint elementId)
|
|
{
|
|
var outer = new UiText();
|
|
var inner = new UiText();
|
|
outer.AddChild(inner);
|
|
return outer;
|
|
}
|
|
|
|
// ── Mount conformance ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Bind_RootBuildsAsUiTabPanel()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
|
|
Assert.IsType<UiTabPanel>(layout.Root);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pins the authored <c>0x2E</c> tab table exactly as read from the live
|
|
/// DATs (FA3 live-mount probe) — CORRECTING the coordinator addendum's
|
|
/// x-order guess (see <see cref="SocialPanelController"/>'s own class
|
|
/// doc). Four entries; Allegiance is the sole default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TabTable_MatchesLiveDatPairing_AllegianceIsDefault()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
var tabs = Assert.IsType<UiTabPanel>(layout.Root);
|
|
|
|
Assert.Equal(4, tabs.Tabs.Count);
|
|
Assert.Contains(tabs.Tabs, e =>
|
|
e.ButtonElementId == 0x1000028Cu && e.PageElementId == 0x10000291u && e.IsDefault);
|
|
Assert.Contains(tabs.Tabs, e =>
|
|
e.ButtonElementId == 0x1000028Eu && e.PageElementId == 0x10000292u && !e.IsDefault);
|
|
Assert.Contains(tabs.Tabs, e =>
|
|
e.ButtonElementId == 0x10000512u && e.PageElementId == 0x10000513u && !e.IsDefault);
|
|
Assert.Contains(tabs.Tabs, e =>
|
|
e.ButtonElementId == 0x1000053Bu && e.PageElementId == 0x1000054Au && !e.IsDefault);
|
|
Assert.Single(tabs.Tabs, e => e.IsDefault);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_Succeeds_AndActivateTabs_SelectsTheAuthoredDefault()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
|
|
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
|
|
|
Assert.NotNull(controller);
|
|
controller!.ActivateTabs();
|
|
Assert.Empty(controller.TabPanel.UnresolvedEntries);
|
|
Assert.True(controller.IsShowingAllegiance);
|
|
Assert.False(controller.IsShowingFellowship);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowFellowship_SwitchesTheActiveTab()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
|
Assert.NotNull(controller);
|
|
controller!.ActivateTabs();
|
|
|
|
controller.ShowFellowship();
|
|
|
|
Assert.True(controller.IsShowingFellowship);
|
|
Assert.False(controller.IsShowingAllegiance);
|
|
}
|
|
|
|
[Fact]
|
|
public void CloseButton_InvokesToggleCallback()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
var calls = new List<string>();
|
|
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks(calls));
|
|
Assert.NotNull(controller);
|
|
|
|
var close = Assert.IsType<UiButton>(layout.FindElement(0x10000290u));
|
|
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
|
|
|
Assert.Contains("toggle", calls);
|
|
}
|
|
|
|
// ── Fellowship empty state (item 4) ─────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Fellowship_NoFellowship_ShowsNotInFellowshipFrame()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout,
|
|
MakeCallbacks(fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = false }));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement notIn = UiElement.FindDescendant(controller!.TabPanel, 0x1000026Bu)!;
|
|
UiElement inFellowship = UiElement.FindDescendant(controller.TabPanel, 0x10000275u)!;
|
|
Assert.True(notIn.Visible);
|
|
Assert.False(inFellowship.Visible);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fellowship_HasFellowship_ShowsInFellowshipFrame()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout,
|
|
MakeCallbacks(fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true }));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement notIn = UiElement.FindDescendant(controller!.TabPanel, 0x1000026Bu)!;
|
|
UiElement inFellowship = UiElement.FindDescendant(controller.TabPanel, 0x10000275u)!;
|
|
Assert.False(notIn.Visible);
|
|
Assert.True(inFellowship.Visible);
|
|
}
|
|
|
|
// ── Allegiance empty state (item 4) ─────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Allegiance_NoProfile_HidesBlocksAndBlanksNames()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout,
|
|
MakeCallbacks(allegiance: new RuntimeAllegianceSnapshot { HasProfile = false }));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement monarchField = UiElement.FindDescendant(controller!.TabPanel, 0x10000255u)!;
|
|
UiElement patronField = UiElement.FindDescendant(controller.TabPanel, 0x1000025Au)!;
|
|
Assert.False(monarchField.Visible);
|
|
Assert.False(patronField.Visible);
|
|
|
|
var monarchName = Assert.IsType<UiText>(
|
|
UiElement.FindDescendant(monarchField, 0x10000257u));
|
|
var patronName = Assert.IsType<UiText>(
|
|
UiElement.FindDescendant(patronField, 0x1000025Cu));
|
|
Assert.Equal(" ", Assert.Single(monarchName.LinesProvider()).Text);
|
|
Assert.Equal(" ", Assert.Single(patronName.LinesProvider()).Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Allegiance_HasProfile_ShowsBothBlocks()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout,
|
|
MakeCallbacks(allegiance: new RuntimeAllegianceSnapshot { HasProfile = true }));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement monarchField = UiElement.FindDescendant(controller!.TabPanel, 0x10000255u)!;
|
|
UiElement patronField = UiElement.FindDescendant(controller.TabPanel, 0x1000025Au)!;
|
|
Assert.True(monarchField.Visible);
|
|
Assert.True(patronField.Visible);
|
|
}
|
|
|
|
// ── Friends/Squelch read-only lists (item 5) ────────────────────────────
|
|
|
|
[Fact]
|
|
public void Friends_PopulatesOneRowPerEntry()
|
|
{
|
|
var friends = new FriendsState();
|
|
friends.Apply(new FriendsUpdate(
|
|
FriendsUpdateType.Full,
|
|
new List<FriendEntry>
|
|
{
|
|
new(1u, "Alice", true, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
|
new(2u, "Bob", false, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
|
}));
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout, MakeCallbacks(friends: friends));
|
|
Assert.NotNull(controller);
|
|
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
UiElement.FindDescendant(controller!.TabPanel, 0x10000513u) is { } friendsPage
|
|
? UiElement.FindDescendant(friendsPage, 0x10000517u)
|
|
: null);
|
|
Assert.Equal(2, listBox.ViewportForTest!.Children.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Friends_ReactsToRevisionChange_OnTick()
|
|
{
|
|
var friends = new FriendsState();
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout, MakeCallbacks(friends: friends));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement friendsPage = UiElement.FindDescendant(controller!.TabPanel, 0x10000513u)!;
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
UiElement.FindDescendant(friendsPage, 0x10000517u));
|
|
Assert.Equal(0, listBox.ViewportForTest?.Children.Count ?? 0);
|
|
|
|
friends.Apply(new FriendsUpdate(
|
|
FriendsUpdateType.Add,
|
|
new List<FriendEntry>
|
|
{
|
|
new(1u, "Alice", true, false, System.Array.Empty<uint>(), System.Array.Empty<uint>()),
|
|
}));
|
|
controller.Tick();
|
|
|
|
Assert.Single(listBox.ViewportForTest!.Children);
|
|
}
|
|
|
|
[Fact]
|
|
public void Squelch_PopulatesCharacterAndAccountRows()
|
|
{
|
|
var squelch = new SquelchState();
|
|
squelch.Replace(new SquelchDatabase(
|
|
new Dictionary<string, uint>(System.StringComparer.OrdinalIgnoreCase) { ["BadAccount"] = 1u },
|
|
new Dictionary<uint, SquelchInfo>
|
|
{
|
|
[7u] = new SquelchInfo("Grief", false, new HashSet<uint>()),
|
|
},
|
|
new SquelchInfo(string.Empty, false, new HashSet<uint>())));
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
|
|
SocialPanelController? controller = SocialPanelController.Bind(
|
|
layout, MakeCallbacks(squelch: squelch));
|
|
Assert.NotNull(controller);
|
|
|
|
UiElement squelchPage = UiElement.FindDescendant(controller!.TabPanel, 0x1000054Au)!;
|
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
|
UiElement.FindDescendant(squelchPage, 0x1000053Eu));
|
|
Assert.Equal(2, listBox.ViewportForTest!.Children.Count);
|
|
}
|
|
|
|
// ── D1: Friends/Squelch action buttons are honest INERT ────────────────
|
|
|
|
[Fact]
|
|
public void FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
|
SocialPanelController? controller = SocialPanelController.Bind(layout, MakeCallbacks());
|
|
Assert.NotNull(controller);
|
|
|
|
foreach (uint buttonId in new[] { 0x10000514u, 0x10000515u, 0x10000516u })
|
|
{
|
|
var button = Assert.IsType<UiButton>(
|
|
UiElement.FindDescendant(controller!.TabPanel, buttonId));
|
|
Assert.Null(button.OnClick);
|
|
}
|
|
foreach (uint buttonId in new[] { 0x10000547u, 0x1000054Bu, 0x1000054Cu })
|
|
{
|
|
var button = Assert.IsType<UiButton>(
|
|
UiElement.FindDescendant(controller!.TabPanel, buttonId));
|
|
Assert.Null(button.OnClick);
|
|
}
|
|
}
|
|
}
|