using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Chat; namespace AcDream.App.Tests.UI.Layout; /// /// Controller-level tests for against /// the committed options_panel_2100006E_1000018D.json fixture /// (Campaign OP slice OP3) — the SAME LayoutImporter.ImportInfos(dats, /// 0x2100006Eu, 0x1000018Du) catalog import /// performs against real /// DATs. No DAT access, no live runtime — dat-free per the established /// FixtureLoader pattern. /// public sealed class OptionsPanelControllerTests { private static UiButton Click(ImportedLayout layout, uint elementId) { var button = Assert.IsType(layout.FindElement(elementId)); button.OnEvent(new UiEvent(0, button, UiEventType.Click)); return button; } private static OptionsPanelController.Callbacks MakeCallbacks( List calls, Action? displaySystemMessage = null) => new( Toggle: () => calls.Add("toggle"), RequestExitToCharacterSelection: () => calls.Add("exit-to-char-select"), ExitGame: () => calls.Add("exit-game"), UseMouseTurningSettings: () => calls.Add("mouse-turning"), DisplaySystemMessage: displaySystemMessage ?? (text => calls.Add($"message:{text}"))); // ── Mount conformance ──────────────────────────────────────────────────── [Fact] public void Bind_RootBuildsAsUiTabPanel() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); Assert.IsType(layout.Root); } [Fact] public void Bind_Succeeds_AndExposesFourEmptyPages() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController? controller = OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Assert.NotNull(controller); Assert.Equal(4, controller!.Pages.Count); Assert.Empty(controller.GameplayPage.Rows); Assert.Empty(controller.CharacterPage.Rows); Assert.Empty(controller.ChatPage.Rows); Assert.Empty(controller.ConfigPage.Rows); } [Fact] public void ActivateTabs_SelectsGameplayAsDefault_ButNeverFlushesIt() { // Mechanism review S1 (2026-08-11 fix round): gmGameplayOptionsUI is // NOT an OptionPage in retail (acclient.h:55857) — its own OptionPage // instance is constructed with AfterApply deliberately left null // (OptionsPanelController's constructor), regardless of what the // controller's OWN AfterApply callback is, so the initial default- // tab activation (OnShown -> Apply) never publishes a flush. ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); int gameplayFlushCount = 0; OptionsPanelController controller = OptionsPanelController.Bind( layout, MakeCallbacks(calls) with { AfterApply = () => gameplayFlushCount++ })!; controller.ActivateTabs(); Assert.True(controller.TabPanel.BehaviorActive); Assert.Equal(0x10000212u, controller.TabPanel.ActivePageElementId); // Gameplay slot Assert.Equal(0, gameplayFlushCount); } [Fact] public void TabSwitch_RevertsLeavingGameplayPage_AndAppliesEnteringCharacterPage() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); var flushes = new List(); OptionsPanelController controller = OptionsPanelController.Bind( layout, MakeCallbacks(calls) with { AfterApply = () => flushes.Add("flush") })!; controller.ActivateTabs(); flushes.Clear(); // drop the initial-activation flush (Gameplay never flushes anyway) controller.TabPanel.SwitchTo(0x10000211u); // Character page slot Assert.Equal(0x10000211u, controller.TabPanel.ActivePageElementId); // OnHidden (Gameplay, Reset — no flush regardless) and OnShown // (Character, a REAL AfterApply-wired page — Apply flushes). Assert.Equal(["flush"], flushes); } [Fact] public void WholeWindowHide_RevertsCurrentlyActivePage() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController controller = OptionsPanelController.Bind(layout, MakeCallbacks(calls))!; controller.ActivateTabs(); var row = new BoolOptionRow(initial: false, defaultValue: false); controller.GameplayPage.Register(row); row.SetCurrentValue(true); Assert.True(controller.GameplayPage.Changed); controller.OnHidden(); // IRetainedPanelController hook — whole window closing Assert.False(controller.GameplayPage.Changed); } [Fact] public void WholeWindowShow_AppliesCurrentlyActivePage() { // Switches off the Gameplay default onto Character FIRST — Gameplay // never flushes (S1), so testing "OnShown applies the currently // active page" needs a REAL AfterApply-wired page active, exactly // like a returning user re-opening the window on a non-default tab. ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); int flushCount = 0; OptionsPanelController controller = OptionsPanelController.Bind( layout, MakeCallbacks(calls) with { AfterApply = () => flushCount++ })!; controller.ActivateTabs(); controller.TabPanel.SwitchTo(0x10000211u); // Character page slot flushCount = 0; controller.OnShown(); Assert.Equal(1, flushCount); } [Fact] public void GameplayPage_OnShownAndOnHidden_NeverFlush_EvenWhenControllerAfterApplyIsWired() { // Direct pin of S1's fix: cycling Gameplay's own OnShown/OnHidden // (the page-model hooks OnActivePageChanged drives) never publishes // a flush, independent of TabSwitch/ActivateTabs framing. ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); int flushCount = 0; OptionsPanelController controller = OptionsPanelController.Bind( layout, MakeCallbacks(calls) with { AfterApply = () => flushCount++ })!; controller.GameplayPage.OnShown(); controller.GameplayPage.OnHidden(); Assert.Equal(0, flushCount); } // ── Close button ───────────────────────────────────────────────────────── [Fact] public void CloseButton_FiresToggleCallback() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000210u); Assert.Equal(["toggle"], calls); } // ── The seven Gameplay-tab buttons ─────────────────────────────────────── [Fact] public void ExitToCharacterSelectionButton_FiresRequestCallback() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000203u); Assert.Equal(["exit-to-char-select"], calls); } [Fact] public void ExitGameButton_FiresExitGameCallback() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000617u); Assert.Equal(["exit-game"], calls); } [Fact] public void UseMouseTurningSettingsButton_FiresMacroCallback() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x100005CCu); Assert.Equal(["mouse-turning"], calls); } [Fact] public void UrgentAssistanceButton_DisplaysItsOwnByteVerifiedFailureText() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000206u); Assert.Equal([$"message:{OptionsPanelText.UrgentAssistanceUnavailable}"], calls); } [Fact] public void ReportAbuseButton_DisplaysItsOwnByteVerifiedFailureText() { ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000207u); Assert.Equal([$"message:{OptionsPanelText.ReportAbuseUnavailable}"], calls); } [Fact] public void UrgentAssistanceAndReportAbuse_UseDifferentText() { // The two buttons share retail's identical body template but differ // in ONE clause ("urgent assistance request" vs "abuse report"). Assert.NotEqual( OptionsPanelText.UrgentAssistanceUnavailable, OptionsPanelText.ReportAbuseUnavailable); Assert.Contains(OptionsPanelText.SupportUrl, OptionsPanelText.UrgentAssistanceUnavailable); Assert.Contains(OptionsPanelText.SupportUrl, OptionsPanelText.ReportAbuseUnavailable); } [Fact] public void ConfigureKeyboardButton_IsInert_ClickDoesNothing() { // D4/OP8: authored, clickable, no handler this slice. ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000204u); Assert.Empty(calls); } [Fact] public void InGameHelpFilesButton_IsInert_ClickDoesNothing() { // D5: retail's own KeyStone::OpenHelp fails silently without the // missing ACHelpPlugin.dll — mirrored as an inert button. ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); var calls = new List(); OptionsPanelController.Bind(layout, MakeCallbacks(calls)); Click(layout, 0x10000205u); Assert.Empty(calls); } [Fact] public void AllSevenGameplayButtons_ResolveInTheBuiltLayout() { // Guards against a future fixture regeneration silently dropping an // id (a missing button degrades to a logged no-op, not a test // failure, unless asserted here). ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); uint[] buttonIds = { 0x10000203u, // Exit to Character Selection 0x10000204u, // Configure Keyboard 0x10000205u, // In-Game Help Files 0x10000206u, // Urgent Assistance 0x10000207u, // Report Abuse 0x100005CCu, // Use Mouse Turning Settings 0x10000617u, // Exit Game }; foreach (uint id in buttonIds) Assert.IsType(layout.FindElement(id)); } }