Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host 0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import pattern CharacterController already validates, registered through RetailPanelUiController so it shares retail's "one active gmPanelUI child" mutual exclusion with every other sibling panel for free. F11 and the toolbar's options button (0x1000019B, already authoring panel id 10) both now open it; the close button fires the same ToggleOptionsPanel action. OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact Apply/Reset/Defaults/visibility semantics from UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately, Apply commits every row unconditionally + flushes the batched blob, Reset reverts only Changed rows, Defaults restores without committing, and tab-switch/window-hide revert uncommitted edits. Wired for all four tabs; this slice registers real rows on none of them (Gameplay authentically has none — a pure button list). UiTabPanel gains an ActivePageChanged event so the page model can hook every tab transition, including the initial default-tab activation. The seven Gameplay-tab buttons: Exit Game reuses the existing graceful window-close path; Exit to Character Selection gets retail's confirmation dialog and byte-verified mid-air refusal but still behaves as Exit Game (AD-74 — no pre-world character-select flow exists); Configure Keyboard and In-Game Help Files are inert this slice (AD-76 for Help — the plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse short-circuit to their own byte-verified failure text through the interface-text seam instead of ShellExecute against a dead URL (AD-75); Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port, persisting five new CameraTurningSettings preferences and sending PlayerOption.UseMouseTurning — TS-74 records that acdream has no persistent mouse-turning camera mode for the bit to drive yet. Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline 12,871/4/0 — only new tests added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
234 lines
6.7 KiB
C#
234 lines
6.7 KiB
C#
using AcDream.App.UI.Layout;
|
|
|
|
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.
|
|
/// </summary>
|
|
public sealed class OptionPageModelTests
|
|
{
|
|
// ── Empty page (the Gameplay tab's own shape) ───────────────────────────
|
|
|
|
[Fact]
|
|
public void EmptyPage_ChangedIsAlwaysFalse()
|
|
{
|
|
var page = new OptionPage();
|
|
Assert.False(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPage_ApplyResetDefaults_AreNoOps()
|
|
{
|
|
var page = new OptionPage();
|
|
|
|
page.Apply();
|
|
page.Reset();
|
|
page.Defaults();
|
|
|
|
Assert.Empty(page.Rows);
|
|
Assert.False(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPage_OnShownAndOnHidden_DoNotThrow()
|
|
{
|
|
var page = new OptionPage();
|
|
|
|
page.OnShown();
|
|
page.OnHidden();
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPage_Apply_StillInvokesAfterApply()
|
|
{
|
|
// 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.
|
|
var page = new OptionPage();
|
|
int flushCount = 0;
|
|
page.AfterApply = () => flushCount++;
|
|
|
|
page.Apply();
|
|
|
|
Assert.Equal(1, flushCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPage_OnShown_InvokesAfterApply()
|
|
{
|
|
var page = new OptionPage();
|
|
int flushCount = 0;
|
|
page.AfterApply = () => flushCount++;
|
|
|
|
page.OnShown();
|
|
|
|
Assert.Equal(1, flushCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyPage_ResetAndDefaults_DoNotInvokeAfterApply()
|
|
{
|
|
var page = new OptionPage();
|
|
int flushCount = 0;
|
|
page.AfterApply = () => flushCount++;
|
|
|
|
page.Reset();
|
|
page.Defaults();
|
|
|
|
Assert.Equal(0, flushCount);
|
|
}
|
|
|
|
// ── BoolOptionRow leaf semantics (UIOption_Checkbox port) ───────────────
|
|
|
|
[Fact]
|
|
public void BoolOptionRow_SetCurrentValue_AppliesLiveImmediately_WithoutCommitting()
|
|
{
|
|
var applied = new List<bool>();
|
|
var row = new BoolOptionRow(initial: false, defaultValue: false, apply: v => applied.Add(v));
|
|
|
|
row.SetCurrentValue(true);
|
|
|
|
Assert.True(row.Current);
|
|
Assert.False(row.Saved); // NOT committed — SetCurrentValue never touches Saved.
|
|
Assert.True(row.Changed);
|
|
Assert.Equal([true], applied);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoolOptionRow_SaveCurrentValue_CommitsBaseline()
|
|
{
|
|
var row = new BoolOptionRow(initial: false, defaultValue: false);
|
|
row.SetCurrentValue(true);
|
|
|
|
row.SaveCurrentValue();
|
|
|
|
Assert.True(row.Saved);
|
|
Assert.False(row.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoolOptionRow_RestoreSavedValue_RevertsAndReapplies()
|
|
{
|
|
var applied = new List<bool>();
|
|
var row = new BoolOptionRow(initial: false, defaultValue: false, apply: v => applied.Add(v));
|
|
row.SetCurrentValue(true);
|
|
applied.Clear();
|
|
|
|
row.RestoreSavedValue();
|
|
|
|
Assert.False(row.Current);
|
|
Assert.False(row.Changed);
|
|
Assert.Equal([false], applied);
|
|
}
|
|
|
|
[Fact]
|
|
public void BoolOptionRow_RestoreDefaultValue_AppliesLiveWithoutCommitting()
|
|
{
|
|
var applied = new List<bool>();
|
|
var row = new BoolOptionRow(initial: false, defaultValue: true, apply: v => applied.Add(v));
|
|
|
|
row.RestoreDefaultValue();
|
|
|
|
Assert.True(row.Current);
|
|
Assert.False(row.Saved); // uncommitted — Changed re-arms Apply/Reset.
|
|
Assert.True(row.Changed);
|
|
Assert.Equal([true], applied);
|
|
}
|
|
|
|
// ── Synthetic 2-option page ──────────────────────────────────────────────
|
|
|
|
private static (OptionPage Page, BoolOptionRow A, BoolOptionRow B) MakeTwoOptionPage()
|
|
{
|
|
var page = new OptionPage();
|
|
var a = new BoolOptionRow(initial: true, defaultValue: true);
|
|
var b = new BoolOptionRow(initial: false, defaultValue: true);
|
|
page.Register(a);
|
|
page.Register(b);
|
|
return (page, a, b);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_Changed_TrueWhenAnyRowChanged()
|
|
{
|
|
var (page, a, _) = MakeTwoOptionPage();
|
|
Assert.False(page.Changed);
|
|
|
|
a.SetCurrentValue(false);
|
|
|
|
Assert.True(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_Apply_CommitsEveryRowUnconditionally()
|
|
{
|
|
var (page, a, b) = MakeTwoOptionPage();
|
|
a.SetCurrentValue(false); // a changed
|
|
// b left at its initial (unchanged) value.
|
|
|
|
page.Apply();
|
|
|
|
Assert.False(page.Changed);
|
|
Assert.True(a.Saved == a.Current);
|
|
Assert.True(b.Saved == b.Current);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_Reset_RevertsOnlyChangedRows()
|
|
{
|
|
var (page, a, b) = MakeTwoOptionPage();
|
|
a.SetCurrentValue(false); // a: true -> false, changed
|
|
// b stays at its initial false (unchanged, Saved == Current == false).
|
|
|
|
page.Reset();
|
|
|
|
Assert.True(a.Current); // reverted to its original saved baseline
|
|
Assert.False(b.Current); // untouched — was never Changed
|
|
Assert.False(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_Defaults_RestoresEveryRow_WithoutCommitting()
|
|
{
|
|
var (page, a, b) = MakeTwoOptionPage();
|
|
a.SetCurrentValue(false);
|
|
page.Apply(); // commit a's change so Changed starts false
|
|
|
|
page.Defaults();
|
|
|
|
Assert.True(a.Current); // default is true
|
|
Assert.True(b.Current); // default is true (was false)
|
|
// Defaults never commits — b's baseline (Saved) is still its old
|
|
// committed value (false), so Changed re-arms Apply/Reset.
|
|
Assert.True(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_OnHidden_RevertsUncommittedEdits()
|
|
{
|
|
var (page, a, _) = MakeTwoOptionPage();
|
|
a.SetCurrentValue(false);
|
|
|
|
page.OnHidden();
|
|
|
|
Assert.True(a.Current);
|
|
Assert.False(page.Changed);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoOptionPage_OnShown_AppliesAndCommits()
|
|
{
|
|
var (page, a, _) = MakeTwoOptionPage();
|
|
a.SetCurrentValue(false);
|
|
|
|
page.OnShown();
|
|
|
|
Assert.False(page.Changed);
|
|
Assert.False(a.Saved != a.Current);
|
|
}
|
|
}
|