fix(ui): OP3 review fixes — byte-verified Magic chat lines, Gameplay/OptionPage shape, mid-air tri-state, shared geometry
Consolidated fix round for the two OP3 dual-lens reviews
(docs/research/2026-08-11-op3-review-{mechanism,blast}.md), both
APPROVE-WITH-FIXES.
MUST-FIX:
- The six "Use Mouse Turning Settings" chat lines were typed
RetailLogTextType.ClientLocal (0x1A); retail types them 0x07 (Magic).
BYTE-VERIFIED against the PDB-paired binary at all six
gmConfigUI::SetMouseTurningDefaults call sites (0x0049E972/E9E2/EA52/
EAA4/EAF6/EB48): every site pushes `6a 07` (type=7) immediately before
the text-pointer push and the AddTextToScroll call. Added a dedicated
OptionsRuntimeBindings.DisplayMouseTurningMacroLine seam routed at
Magic (scrolling chat transcript, light blue, timestamped) instead of
the 4-slot SpewBox ClientLocal uses; the mid-air refusal and UA/RA
keep ClientLocal (both independently confirmed correct).
- Filed AD-77: the client-wide floating-only gmPanelUI host divergence
(retail also exposes a docked 0x21000017 host) the plan §5 delegated
to this review, scoped to every main panel, not just Options.
SHOULD-FIX:
- gmGameplayOptionsUI is not an OptionPage in retail (acclient.h:55857,
UIElement_Field). OptionsPanelController now constructs the Gameplay
slot's OptionPage with AfterApply deliberately null, so entering/
leaving that tab never publishes SaveCharacterOptionsRuntimeCmd.
Corrected OptionPageModel's doc comment and rewrote the two tests
that pinned the wrong (Gameplay-flushes) shape.
- Added the OptionPage.OnOptionChanged seam (PlayerOptionPage::
OnOptionChanged @0x004F27D0) — fires as the last step of Apply/
Reset/Defaults, plus once per live LED edit via a new
IOptionRow.AttachPageNotify hook (BoolOptionRow wires it into
SetCurrentValue only, matching retail's Apply(1)-only
HandleDialogAndNotices path). OP4-6 will bind Apply/Reset enable
state to this.
- Exit to Character Selection's mid-air refusal is now tri-state
(Func<bool?> IsGrounded): retail's UseTime only reaches the airborne
test inside `else if (smartbox->player)`, so outside player mode (or
with no live controller) the button is a SILENT no-op, not a
refusal. Fixed the inverted comment at both call sites.
- Options panel geometry now matches its nine gmPanelUI siblings
sharing RetailPanelUiController's one main-panel rectangle
(ResizeX=false, bottom-edge-only resize, no invented Min/MaxWidth/
Height) instead of being the only all-four-edge/horizontal-resize
outlier whose width silently reverted whenever a sibling was shown.
- Added the three missing test pins: Options/Character mutual
exclusion through a REAL RetailPanelUiController registration,
RetailDialogFactory.MakeConfirmation's omitted-queueKey overload
sharing DefaultQueueKey, and UiTabPanel.ActivePageChanged never
firing on a dormant (non-activated) host.
- TS-74's What/Where now names the five store-only CameraTurning
preferences explicitly instead of only mentioning them in Risk.
- Test script gains the toolbar-button ghosted->enabled+highlight
check, UseMouseTurning-survives-relogin and the five prefs-survive-
relaunch steps, a UA/RA legibility eye-item, and the corrected
bottom-edge-only geometry description for step 5.
One-liners fixed in files already touched: symmetric close-button
resolve-failure logging in OptionsPanelController.Bind (blast NOTE 8).
Full Release suite: 12,947 passed / 4 skipped / 0 failed (baseline
12,935/4/0 post-OP7 — 12 net new tests; the two OptionPageModelTests
"wrong-shape" tests were renamed/rewritten in place, not removed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
64898f1301
commit
386076af0f
11 changed files with 505 additions and 72 deletions
|
|
@ -69,6 +69,29 @@ public class OP2ReworkBlastRadiusConformanceTests
|
|||
Assert.True(host.ClickThrough);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="UiTabPanel.ActivateTabBehavior"/> — but nothing had pinned
|
||||
/// <see cref="UiTabPanel.ActivePageChanged"/> ITSELF against a dormant
|
||||
/// host (as opposed to <c>BehaviorActive</c>/<c>ActivePageElementId</c>,
|
||||
/// which the test above already covers). <c>SwitchTo</c> is public and
|
||||
/// its own doc invites a future direct call, so this guards the CURRENT
|
||||
/// dormant-by-default contract, not <c>SwitchTo</c> itself.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vendor_TabHost_DormantHost_NeverRaisesActivePageChanged()
|
||||
{
|
||||
var layout = FixtureLoader.LoadVendor();
|
||||
var host = Assert.IsType<UiTabPanel>(layout.FindElement(0x100000B8u));
|
||||
int raised = 0;
|
||||
host.ActivePageChanged += (_, _) => raised++;
|
||||
|
||||
Assert.False(host.BehaviorActive);
|
||||
Assert.Equal(0, raised);
|
||||
}
|
||||
|
||||
// ── Type 8 — panel roots with their own controller-owned switching ──────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -4,16 +4,28 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
|
||||
/// <summary>
|
||||
/// Pure logic tests for <see cref="OptionPage"/>/<see cref="BoolOptionRow"/> —
|
||||
/// no DAT, no widgets, no runtime. Campaign OP slice OP3 ships the model
|
||||
/// against an EMPTY page (the Gameplay tab has no options at all — research
|
||||
/// doc <c>2026-08-10-options-panel-structure.md</c> §6); this file also
|
||||
/// exercises a synthetic 2-option page to prove Apply/Reset/Defaults'
|
||||
/// per-row semantics before OP4-6 wire real DAT-backed rows into the same
|
||||
/// model.
|
||||
/// no DAT, no widgets, no runtime. Exercises a SYNTHETIC empty
|
||||
/// <c>PlayerOptionPage</c>-shaped page (zero registered rows — a shape
|
||||
/// Character/Chat/Config's pages briefly hold before OP4-6 register rows into
|
||||
/// them) to pin the generic model-level "empty page" property, and a
|
||||
/// synthetic 2-option page to prove Apply/Reset/Defaults' per-row semantics
|
||||
/// before OP4-6 wire real DAT-backed rows into the same model.
|
||||
///
|
||||
/// <para>
|
||||
/// NEITHER shape models the Gameplay tab. <c>gmGameplayOptionsUI</c>
|
||||
/// (<c>acclient.h:55857</c>) derives from <c>UIElement_Field</c>, not
|
||||
/// <c>OptionPage</c>/<c>PlayerOptionPage</c> at all — research doc
|
||||
/// <c>2026-08-10-options-panel-structure.md</c> §6, mechanism review S1
|
||||
/// (2026-08-11 fix round). <see cref="OptionsPanelControllerTests"/> pins
|
||||
/// the Gameplay-specific consequence (its <see cref="OptionPage"/> instance
|
||||
/// is constructed with <see cref="OptionPage.AfterApply"/> deliberately left
|
||||
/// null, so it never flushes).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class OptionPageModelTests
|
||||
{
|
||||
// ── Empty page (the Gameplay tab's own shape) ───────────────────────────
|
||||
// ── Empty page (a generic zero-row PlayerOptionPage-shaped page — NOT
|
||||
// modeling Gameplay, which is not an OptionPage at all) ─────────────
|
||||
|
||||
[Fact]
|
||||
public void EmptyPage_ChangedIsAlwaysFalse()
|
||||
|
|
@ -45,11 +57,15 @@ public sealed class OptionPageModelTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyPage_Apply_StillInvokesAfterApply()
|
||||
public void EmptyPlayerOptionPageShapedPage_WithAfterApplyWired_Apply_StillFlushes()
|
||||
{
|
||||
// Retail's PlayerOptionPage::SaveCurrentValues flushes the batched
|
||||
// module regardless of whether THIS page's own rows changed
|
||||
// anything — the module's dirty flag is global, not per-page.
|
||||
// anything — the module's dirty flag is global, not per-page. This
|
||||
// is a property of a page that IS AfterApply-wired (Character/Chat/
|
||||
// Config, briefly, before OP4-6 register their rows) — NOT of the
|
||||
// Gameplay tab, whose own OptionPage instance is constructed with
|
||||
// AfterApply left null (see OptionsPanelControllerTests).
|
||||
var page = new OptionPage();
|
||||
int flushCount = 0;
|
||||
page.AfterApply = () => flushCount++;
|
||||
|
|
@ -60,7 +76,7 @@ public sealed class OptionPageModelTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyPage_OnShown_InvokesAfterApply()
|
||||
public void EmptyPlayerOptionPageShapedPage_WithAfterApplyWired_OnShown_StillFlushes()
|
||||
{
|
||||
var page = new OptionPage();
|
||||
int flushCount = 0;
|
||||
|
|
@ -71,6 +87,19 @@ public sealed class OptionPageModelTests
|
|||
Assert.Equal(1, flushCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyPage_WithNoAfterApplyWired_Apply_NeverFlushes()
|
||||
{
|
||||
// The Gameplay tab's own shape: AfterApply is null by construction
|
||||
// (OptionsPanelController), so Apply (fired by the initial default-
|
||||
// tab activation and every later tab entry) never publishes a flush.
|
||||
var page = new OptionPage { AfterApply = null };
|
||||
|
||||
Exception? thrown = Record.Exception(page.Apply);
|
||||
|
||||
Assert.Null(thrown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyPage_ResetAndDefaults_DoNotInvokeAfterApply()
|
||||
{
|
||||
|
|
@ -231,4 +260,116 @@ public sealed class OptionPageModelTests
|
|||
Assert.False(page.Changed);
|
||||
Assert.False(a.Saved != a.Current);
|
||||
}
|
||||
|
||||
// ── OnOptionChanged seam (mechanism review S2, 2026-08-11 fix round) ────
|
||||
// PlayerOptionPage::OnOptionChanged @0x004F27D0: the sole Apply/Reset
|
||||
// enable-gate, run as the LAST statement of all three verbs
|
||||
// (0x004F2C95/0x004F2CE5/0x004F2D4A) plus once per live LED click via
|
||||
// UIOption::HandleDialogAndNotices @0x004EFB90.
|
||||
|
||||
[Fact]
|
||||
public void OnOptionChanged_FiresAsLastStepOf_Apply()
|
||||
{
|
||||
var (page, a, _) = MakeTwoOptionPage();
|
||||
a.SetCurrentValue(false);
|
||||
var order = new List<string>();
|
||||
page.AfterApply = () => order.Add("afterApply");
|
||||
page.OnOptionChanged = () => order.Add("onOptionChanged");
|
||||
|
||||
page.Apply();
|
||||
|
||||
Assert.Equal(["afterApply", "onOptionChanged"], order);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnOptionChanged_FiresOnReset_EvenWithNoAfterApply()
|
||||
{
|
||||
var (page, a, _) = MakeTwoOptionPage();
|
||||
a.SetCurrentValue(false);
|
||||
int notifyCount = 0;
|
||||
page.OnOptionChanged = () => notifyCount++;
|
||||
|
||||
page.Reset();
|
||||
|
||||
Assert.Equal(1, notifyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnOptionChanged_FiresOnDefaults()
|
||||
{
|
||||
var (page, _, _) = MakeTwoOptionPage();
|
||||
int notifyCount = 0;
|
||||
page.OnOptionChanged = () => notifyCount++;
|
||||
|
||||
page.Defaults();
|
||||
|
||||
Assert.Equal(1, notifyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnOptionChanged_FiresOnEmptyPage_ForEveryVerb()
|
||||
{
|
||||
// Defaults is NEVER gated by retail's own OnOptionChanged override
|
||||
// (it never fetches the Defaults child id at all), but the page-
|
||||
// level notify still fires from EVERY verb regardless of row count
|
||||
// — the model doesn't special-case "which button retail happens to
|
||||
// gate" here, only the seam itself.
|
||||
var page = new OptionPage();
|
||||
int notifyCount = 0;
|
||||
page.OnOptionChanged = () => notifyCount++;
|
||||
|
||||
page.Apply();
|
||||
page.Reset();
|
||||
page.Defaults();
|
||||
|
||||
Assert.Equal(3, notifyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolOptionRow_SetCurrentValue_NotifiesAttachedPage()
|
||||
{
|
||||
// Retail's Apply(1)-only HandleDialogAndNotices path: a live LED
|
||||
// click reaches the page's OnOptionChanged directly, distinct from
|
||||
// (and in addition to) the per-row apply callback.
|
||||
var row = new BoolOptionRow(initial: false, defaultValue: false);
|
||||
int notifyCount = 0;
|
||||
row.AttachPageNotify(() => notifyCount++);
|
||||
|
||||
row.SetCurrentValue(true);
|
||||
|
||||
Assert.Equal(1, notifyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoolOptionRow_RestoreSavedValueAndRestoreDefaultValue_DoNotNotifyAttachedPage()
|
||||
{
|
||||
// Apply(0) paths — retail's Reset/Defaults call these directly and
|
||||
// notify the page ONCE themselves at their own verb tail; a per-row
|
||||
// notify here would double-fire (or fire once per reverted row
|
||||
// instead of once per verb call).
|
||||
var row = new BoolOptionRow(initial: false, defaultValue: true);
|
||||
int notifyCount = 0;
|
||||
row.AttachPageNotify(() => notifyCount++);
|
||||
row.SetCurrentValue(true);
|
||||
notifyCount = 0; // drop the SetCurrentValue notify above
|
||||
|
||||
row.RestoreSavedValue();
|
||||
row.RestoreDefaultValue();
|
||||
|
||||
Assert.Equal(0, notifyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_AttachesPageNotify_SoSubsequentLiveEditsNotifyThePage()
|
||||
{
|
||||
var page = new OptionPage();
|
||||
var row = new BoolOptionRow(initial: false, defaultValue: false);
|
||||
int notifyCount = 0;
|
||||
page.OnOptionChanged = () => notifyCount++;
|
||||
|
||||
page.Register(row);
|
||||
row.SetCurrentValue(true);
|
||||
|
||||
Assert.Equal(1, notifyCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,8 +60,14 @@ public sealed class OptionsPanelControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ActivateTabs_SelectsGameplayAsDefault_AndAppliesItsPage()
|
||||
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<string>();
|
||||
int gameplayFlushCount = 0;
|
||||
|
|
@ -73,12 +79,11 @@ public sealed class OptionsPanelControllerTests
|
|||
|
||||
Assert.True(controller.TabPanel.BehaviorActive);
|
||||
Assert.Equal(0x10000212u, controller.TabPanel.ActivePageElementId); // Gameplay slot
|
||||
// OnShown() fired for the initial default tab -> Apply() -> AfterApply.
|
||||
Assert.Equal(1, gameplayFlushCount);
|
||||
Assert.Equal(0, gameplayFlushCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabSwitch_RevertsLeavingPage_AndAppliesEnteringPage()
|
||||
public void TabSwitch_RevertsLeavingGameplayPage_AndAppliesEnteringCharacterPage()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
||||
var calls = new List<string>();
|
||||
|
|
@ -86,12 +91,13 @@ public sealed class OptionsPanelControllerTests
|
|||
OptionsPanelController controller = OptionsPanelController.Bind(
|
||||
layout, MakeCallbacks(calls) with { AfterApply = () => flushes.Add("flush") })!;
|
||||
controller.ActivateTabs();
|
||||
flushes.Clear(); // drop the initial-activation flush
|
||||
flushes.Clear(); // drop the initial-activation flush (Gameplay never flushes anyway)
|
||||
|
||||
controller.TabPanel.SwitchTo(0x10000211u); // Character page slot
|
||||
|
||||
Assert.Equal(0x10000211u, controller.TabPanel.ActivePageElementId);
|
||||
// Both OnHidden (Gameplay, Reset — no flush) and OnShown (Character, Apply — flush).
|
||||
// OnHidden (Gameplay, Reset — no flush regardless) and OnShown
|
||||
// (Character, a REAL AfterApply-wired page — Apply flushes).
|
||||
Assert.Equal(["flush"], flushes);
|
||||
}
|
||||
|
||||
|
|
@ -116,12 +122,17 @@ public sealed class OptionsPanelControllerTests
|
|||
[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<string>();
|
||||
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();
|
||||
|
|
@ -129,6 +140,24 @@ public sealed class OptionsPanelControllerTests
|
|||
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<string>();
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -81,6 +81,35 @@ public sealed class RetailDialogFactoryTests
|
|||
Assert.False(factory.IsOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blast review SHOULD-FIX 3 / NOTE 3 (2026-08-11 fix round): the Options
|
||||
/// panel's Exit-to-Character-Selection confirmation goes through
|
||||
/// <c>RetailUiRuntime.ShowConfirmation</c> -> <c>MakeConfirmation(message,
|
||||
/// callback)</c> with NO explicit <c>queueKey</c> argument. Nothing pinned
|
||||
/// that the omitted-key overload actually shares
|
||||
/// <see cref="RetailDialogFactory.DefaultQueueKey"/> with every other
|
||||
/// confirmation in the codebase, rather than silently defaulting to a
|
||||
/// DIFFERENT key that would let two confirmations sit active
|
||||
/// simultaneously instead of queuing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MakeConfirmation_OmittedQueueKey_SharesDefaultQueueKey()
|
||||
{
|
||||
var root = new UiRoot { Width = 800f, Height = 600f };
|
||||
var layouts = new List<ImportedLayout>();
|
||||
var factory = CreateFactory(root, layouts);
|
||||
|
||||
factory.MakeConfirmation("uses the omitted-queueKey overload");
|
||||
factory.MakeConfirmation("explicit DefaultQueueKey", queueKey: RetailDialogFactory.DefaultQueueKey);
|
||||
|
||||
// If the omitted-queueKey call had used a DIFFERENT key, both would
|
||||
// be ACTIVE simultaneously (distinct queue groups run independently
|
||||
// — QueueGroupsAndNonQueuedDialogsCanBeActiveTogether below) instead
|
||||
// of the second queuing FIFO behind the first.
|
||||
Assert.Equal(1, factory.ActiveCount);
|
||||
Assert.Equal(1, factory.PendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueueGroupsAndNonQueuedDialogsCanBeActiveTogether()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -117,6 +117,37 @@ public sealed class RetailPanelUiControllerTests
|
|||
Assert.Equal((310f, 540f), (spellbook.Width, spellbook.Height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blast review SHOULD-FIX 3 (2026-08-11 fix round): OP3's headline claim
|
||||
/// is that registering the Options panel through
|
||||
/// <see cref="RetailPanelUiController.RegisterMainPanel"/> gives it
|
||||
/// retail's "one active gmPanelUI child" mutual exclusion against every
|
||||
/// OTHER already-shipped sibling "for free" — nothing pinned that against
|
||||
/// a REAL sibling (as opposed to the synthetic ids the other tests in
|
||||
/// this file use). Uses the actual <see cref="RetailPanelCatalog.Options"/>
|
||||
/// (10) and <see cref="RetailPanelCatalog.Character"/> (11) ids.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Options_AndCharacter_ShareExclusiveMainPanelLifecycle()
|
||||
{
|
||||
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||
RetailWindowHandle character = Mount(root, WindowNames.Character, 540f, 18f);
|
||||
RetailWindowHandle options = Mount(root, WindowNames.Options, 150f, 80f);
|
||||
using var controller = Create(root);
|
||||
controller.RegisterMainPanel(RetailPanelCatalog.Character, WindowNames.Character, character);
|
||||
controller.RegisterMainPanel(RetailPanelCatalog.Options, WindowNames.Options, options);
|
||||
|
||||
controller.SetPanelVisibility(RetailPanelCatalog.Character, visible: true);
|
||||
Assert.True(character.IsVisible);
|
||||
Assert.False(options.IsVisible);
|
||||
Assert.Equal(RetailPanelCatalog.Character, controller.ActivePanelId);
|
||||
|
||||
controller.SetPanelVisibility(RetailPanelCatalog.Options, visible: true);
|
||||
Assert.False(character.IsVisible);
|
||||
Assert.True(options.IsVisible);
|
||||
Assert.Equal(RetailPanelCatalog.Options, controller.ActivePanelId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndicatorDetailPanel_SharesPlacementAndRestoresPreviousPanel()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue