acdream/tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs
Erik e71e5a9614 feat(ui): Campaign OP slice OP5 — the Chat tab
Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and
OP3's per-page OptionPage model: the General Options header + two
DualHash-linked opacity sliders (Option_DefaultOpacity_Property
0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on
drag through RetailWindowOpacityController, defaults read from the
installed DAT's DBProperties collection at DID 0x78000001 via
ChatOptionsDatDefaults), and the five per-window text-filter blocks
(main window 12 rows minus Gameplay, four floaties 13 rows each — the
byte-verified authored order cross-checked against the raw
gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not
just the research doc's own table) writing AcDream.Core.Chat.
ChatWindowState directly, the same state CH6's chat windows already
read.

AP-195 retired: ported both halves left open at the OP2 re-review —
the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by
the block-level P0x10000082/P0x10000083 sprites now threaded through
ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail
(UiCheckboxBitfield64.Height grows with its stacked row content; the
enclosing ListBox reflows around the block's FINAL height via the new
UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking
rather than a third stacking path). AP-187 broadened to cover the main
window's own filter (previously only the four floaties) and the new
live-editing write path.

The main chat window's filter (retail window id 8, ChatWindowState id
0) gains its own settings.json persistence (ChatSettings.
ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields;
opacity persistence is now wired on every live slider change, not only
through the old dev-scaffold Settings panel.

Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the
new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all
19 committed layout fixtures — purely additive, confirmed against the
live installed DAT (0x10000520's own 0x82/0x83 properties resolve to
0x06004D17/0x06004D19 exactly as AP-195 documented).

Conformance: FilterRows/FilterBlocks pinned against the byte-verified
authored order and ChatWindowState's own default constants; the AP-195
LED swap and self-sizing behavior; the DAT opacity-default extraction
against the live installed DAT; live filter/opacity writes reaching
ChatWindowState/RetailWindowOpacityController; OnShown re-seed and
Reset/Defaults ghosting per the OP4 binding-pattern discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 06:25:59 +02:00

607 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Chat;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Campaign OP slice OP5 (2026-08-11) conformance + behavior tests for
/// <see cref="ChatOptionsPageController"/> — the same hermetic pattern
/// <c>CharacterOptionsPageControllerTests</c> established: pure-data conformance
/// against the byte-verified research doc/decomp tables, then behavioral tests
/// against the committed <c>options_panel_2100006E_1000018D.json</c> +
/// <c>options_2100002B.json</c> fixtures — no live DAT access.
/// </summary>
public sealed class ChatOptionsPageControllerTests
{
// ── Pure data conformance ────────────────────────────────────────────────
[Fact]
public void FilterRows_Has13EntriesInByteVerifiedAuthoredOrder()
{
// docs/research/2026-08-10-options-panel-structure.md §5.2, cross-checked
// against the raw gmChatOptionsUI::InitOptions / AddCheckboxBitfield64Option
// pseudo-C AddChild call sequence (this class's own doc trace).
(ulong Mask, string Key)[] expected =
{
(0x0000000083912021ul, "ID_ChatOption_TextFilter_Gameplay"),
(0x0000000000600040ul, "ID_ChatOption_TextFilter_Combat"),
(0x0000000000020080ul, "ID_ChatOption_TextFilter_Magic"),
(0x0000000000001004ul, "ID_ChatOption_TextFilter_AreaSpeech"),
(0x0000000000000018ul, "ID_ChatOption_TextFilter_Tells"),
(0x0000000000040C00ul, "ID_ChatOption_TextFilter_Allegience"),
(0x0000000000080000ul, "ID_ChatOption_TextFilter_Fellowship"),
(0x0000000008000000ul, "ID_ChatOption_TextFilter_General"),
(0x0000000010000000ul, "ID_ChatOption_TextFilter_Trade"),
(0x0000000020000000ul, "ID_ChatOption_TextFilter_LFG"),
(0x0000000040000000ul, "ID_ChatOption_TextFilter_Roleplay"),
(0x0000000100000000ul, "ID_ChatOption_TextFilter_Society"),
(0x0000000004000000ul, "ID_ChatOption_TextFilter_Error"),
};
Assert.Equal(13, ChatOptionsPageController.FilterRows.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i].Mask, ChatOptionsPageController.FilterRows[i].Mask);
Assert.Equal(expected[i].Key, ChatOptionsPageController.FilterRows[i].RetailLabelKey);
}
}
[Fact]
public void FilterRows_EveryMaskIsPairwiseDistinct_AndNonZero()
{
var masks = ChatOptionsPageController.FilterRows.Select(static r => r.Mask).ToList();
Assert.Equal(masks.Count, masks.Distinct().Count());
Assert.All(masks, m => Assert.NotEqual(0ul, m));
}
[Fact]
public void FilterBlocks_Has5EntriesInAuthoredOrder_WithRetailWindowIds()
{
// Research doc §5: main(8), floaty1(2), floaty2(3), floaty3(4), floaty4(5) —
// authored top-to-bottom order in gmChatOptionsUI::InitOptions.
(int RetailId, int CompactId, string HeaderKey, ulong Default, bool IncludesGameplay)[] expected =
{
(8, ChatWindowState.MainWindowId, "ID_ChatOption_MainChatWindow_Section",
0xFBFFFFFFul, false),
(2, 1, "ID_ChatOption_FloatyChatWindow1_Section", 0x0000101Cul, true),
(3, 2, "ID_ChatOption_FloatyChatWindow2_Section", 0x00040C00ul, true),
(4, 3, "ID_ChatOption_FloatyChatWindow3_Section", 0x00080000ul, true),
(5, 4, "ID_ChatOption_FloatyChatWindow4_Section", 0x78000000ul, true),
};
Assert.Equal(5, ChatOptionsPageController.FilterBlocks.Length);
for (int i = 0; i < expected.Length; i++)
{
ChatOptionsPageController.FilterBlockSpec spec = ChatOptionsPageController.FilterBlocks[i];
Assert.Equal(expected[i].RetailId, spec.RetailWindowId);
Assert.Equal(expected[i].CompactId, spec.CompactWindowId);
Assert.Equal(expected[i].HeaderKey, spec.HeaderKey);
Assert.Equal(expected[i].Default, spec.DefaultFilter);
Assert.Equal(expected[i].IncludesGameplay, spec.IncludesGameplayRow);
}
}
[Fact]
public void FilterBlocks_DefaultsMatchChatWindowStateNamedConstants()
{
// These MUST be the exact same source (ChatWindowState's own constants),
// not two independently-typed literal tables that could drift apart.
Assert.Equal(ChatWindowState.MainWindowDefaultFilter, ChatOptionsPageController.FilterBlocks[0].DefaultFilter);
Assert.Equal(ChatWindowState.Floaty1DefaultFilter, ChatOptionsPageController.FilterBlocks[1].DefaultFilter);
Assert.Equal(ChatWindowState.Floaty2DefaultFilter, ChatOptionsPageController.FilterBlocks[2].DefaultFilter);
Assert.Equal(ChatWindowState.Floaty3DefaultFilter, ChatOptionsPageController.FilterBlocks[3].DefaultFilter);
Assert.Equal(ChatWindowState.Floaty4DefaultFilter, ChatOptionsPageController.FilterBlocks[4].DefaultFilter);
}
// ── Row/header string-hash conformance ───────────────────────────────────
[Theory]
[InlineData("ID_ChatOption_TextFilter_Gameplay")]
[InlineData("ID_ChatOption_TextFilter_Combat")]
[InlineData("ID_ChatOption_TextFilter_Magic")]
[InlineData("ID_ChatOption_TextFilter_AreaSpeech")]
[InlineData("ID_ChatOption_TextFilter_Tells")]
[InlineData("ID_ChatOption_TextFilter_Allegience")]
[InlineData("ID_ChatOption_TextFilter_Fellowship")]
[InlineData("ID_ChatOption_TextFilter_General")]
[InlineData("ID_ChatOption_TextFilter_Trade")]
[InlineData("ID_ChatOption_TextFilter_LFG")]
[InlineData("ID_ChatOption_TextFilter_Roleplay")]
[InlineData("ID_ChatOption_TextFilter_Society")]
[InlineData("ID_ChatOption_TextFilter_Error")]
public void EveryFilterRowKey_IsAuthoredOnTheFilterRowsTable(string key)
{
Assert.Contains(ChatOptionsPageController.FilterRows, r => r.RetailLabelKey == key);
}
// ── Behavioral: built against the committed fixtures ─────────────────────
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
private static ElementInfo? Find(ElementInfo n, uint id)
{
if (n.Id == id) return n;
foreach (ElementInfo c in n.Children)
{
ElementInfo? f = Find(c, id);
if (f is not null) return f;
}
return null;
}
/// <summary>The same "resolve a row template from the standalone 0x2100002B
/// fixture" resolver <c>CharacterOptionsPageControllerTests</c> uses.</summary>
private static Func<uint, uint, UiElement?> MakeTemplateResolver()
{
ElementInfo panelRoot = FixtureLoader.LoadOptionsPanelInfos();
return (layoutId, elementId) =>
{
if (layoutId != 0x2100002Bu) return null;
ElementInfo? templateInfo = Find(panelRoot, elementId);
return templateInfo is null ? null : LayoutImporter.Build(templateInfo, NoTex, null).Root;
};
}
private sealed class FakeBindings
{
public float DefaultOpacity = 0.5f;
public float ActiveOpacity = 1.0f;
public float DefaultOpacityDatDefault = 0.5f;
public float ActiveOpacityDatDefault = 1.0f;
public List<float> DefaultOpacitySets { get; } = new();
public List<float> ActiveOpacitySets { get; } = new();
public Dictionary<int, ulong> Filters { get; } = new()
{
[ChatWindowState.MainWindowId] = ChatWindowState.MainWindowDefaultFilter,
[1] = ChatWindowState.Floaty1DefaultFilter,
[2] = ChatWindowState.Floaty2DefaultFilter,
[3] = ChatWindowState.Floaty3DefaultFilter,
[4] = ChatWindowState.Floaty4DefaultFilter,
};
public List<(int WindowId, ulong Value)> FilterSets { get; } = new();
public ChatOptionsPageController.Bindings ToBindings() => new(
CurrentDefaultOpacity: () => DefaultOpacity,
CurrentActiveOpacity: () => ActiveOpacity,
SetDefaultOpacity: value =>
{
(DefaultOpacity, ActiveOpacity) = ChatOpacityLinkFor(ActiveOpacity, value, isDefault: true);
DefaultOpacitySets.Add(value);
},
SetActiveOpacity: value =>
{
(DefaultOpacity, ActiveOpacity) = ChatOpacityLinkFor(DefaultOpacity, value, isDefault: false);
ActiveOpacitySets.Add(value);
},
DefaultOpacityDatDefault: DefaultOpacityDatDefault,
ActiveOpacityDatDefault: ActiveOpacityDatDefault,
CurrentFilter: windowId => Filters[windowId],
SetFilter: (windowId, value) =>
{
Filters[windowId] = value;
FilterSets.Add((windowId, value));
});
// Mirrors AcDream.UI.Abstractions.Panels.Settings.ChatOpacityLink's own
// math exactly (the fake stands in for RetailWindowOpacityController,
// which itself just calls that link) — kept local so this test file has
// no cross-project dependency for a two-line clamp/drag rule.
private static (float Default, float Active) ChatOpacityLinkFor(
float other, float value, bool isDefault)
{
value = Math.Clamp(value, 0f, 1f);
if (isDefault)
return (value, other < value ? value : other);
return (other > value ? value : other, value);
}
}
private static (OptionsPanelController Panel, FakeBindings Bindings, bool Bound) BindReal(
Func<uint, uint, string?>? resolveString = null)
{
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
var fakeBindings = new FakeBindings();
bool bound = ChatOptionsPageController.Bind(
layout,
controller.ChatPage,
MakeTemplateResolver(),
resolveString ?? ((_, _) => null),
fakeBindings.ToBindings());
return (controller, fakeBindings, bound);
}
[Fact]
public void Bind_Succeeds_AndRegistersExactlySevenRows()
{
// 2 opacity sliders + 5 filter blocks = 7 IOptionRow registrations
// (each block is ONE composite row, not one row per checkbox).
(OptionsPanelController controller, _, bool bound) = BindReal();
Assert.True(bound);
Assert.Equal(7, controller.ChatPage.Rows.Count);
}
[Fact]
public void Bind_MainWindowBlock_Has12Rows_FloatyBlocks_Have13Rows()
{
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
var fakeBindings = new FakeBindings();
bool bound = ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
Assert.True(bound);
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
Assert.Equal(5, blocks.Count);
Assert.Equal(12, blocks[0].Rows.Count); // main window — no Gameplay row
for (int i = 1; i < 5; i++)
Assert.Equal(13, blocks[i].Rows.Count); // the four floaties
}
[Fact]
public void Bind_FilterBlocks_ResolveTheAP195LedSprites()
{
// AP-195 end-to-end: the regenerated fixture now carries dat properties
// 0x10000082/0x10000083 on template consumer 0x10000520
// (0x06004D17/0x06004D19), and DatWidgetFactory threads them into the
// constructed UiCheckboxBitfield64 — every one of the five blocks shares
// the SAME authored template, so every block resolves the same sprites.
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
var fakeBindings = new FakeBindings();
ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
Assert.Equal(5, blocks.Count);
Assert.All(blocks, b =>
{
Assert.Equal(0x06004D17u, b.CheckedLedSprite);
Assert.Equal(0x06004D19u, b.UncheckedLedSprite);
});
}
[Fact]
public void Bind_FilterBlocks_SelfSizeToFitTheirRows()
{
// AP-195's self-sizing tail — the block grows past its authored 272x100
// extent to fit N rows (13 floaty rows * 20px template height > 100).
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
var fakeBindings = new FakeBindings();
ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
Assert.All(blocks, b => Assert.True(b.Height > 100f, $"block Height={b.Height}"));
Assert.Equal(272f, blocks[0].Width); // width (GetWidth()) untouched
}
private static List<UiCheckboxBitfield64> CollectBlocks(UiElement root)
{
var found = new List<UiCheckboxBitfield64>();
Walk(root, found);
return found;
static void Walk(UiElement node, List<UiCheckboxBitfield64> acc)
{
if (node is UiCheckboxBitfield64 block) acc.Add(block);
foreach (UiElement child in node.Children) Walk(child, acc);
}
}
[Fact]
public void Bind_SeedsSlidersFromCurrentOpacity_NotTheDatDefault()
{
var fakeBindings = new FakeBindings { DefaultOpacity = 0.25f, ActiveOpacity = 0.75f };
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
bool bound = ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
Assert.True(bound);
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
Assert.Equal(0.25f, defaultRow.Current);
Assert.Equal(0.75f, activeRow.Current);
// Never wrote back just from seeding.
Assert.Empty(fakeBindings.DefaultOpacitySets);
Assert.Empty(fakeBindings.ActiveOpacitySets);
}
[Fact]
public void DraggingDefaultSlider_AppliesLive_AndDragsActiveUp_NeverClamping()
{
var fakeBindings = new FakeBindings { DefaultOpacity = 0.3f, ActiveOpacity = 0.3f };
(OptionsPanelController controller, FakeBindings bindings, bool bound) =
BindRealWith(fakeBindings);
Assert.True(bound);
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
// Raise Default ABOVE the current Active value — retail's link drags
// Active UP to match, never clamps Default down.
defaultRow.SetCurrentValue(0.9f);
Assert.Equal(0.9f, bindings.DefaultOpacity);
Assert.Equal(0.9f, bindings.ActiveOpacity); // dragged up
Assert.Equal(0.9f, defaultRow.Current);
Assert.Equal(0.9f, activeRow.Current); // the OTHER row's own value moved too
Assert.Single(bindings.DefaultOpacitySets);
// Live-apply: Changed reflects immediately (retail's Apply(1) shape).
Assert.True(controller.ChatPage.Changed);
}
[Fact]
public void DraggingActiveSliderBelowDefault_DragsDefaultDown()
{
var fakeBindings = new FakeBindings { DefaultOpacity = 0.6f, ActiveOpacity = 0.6f };
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
activeRow.SetCurrentValue(0.1f);
Assert.Equal(0.1f, bindings.ActiveOpacity);
Assert.Equal(0.1f, bindings.DefaultOpacity); // dragged down
Assert.Equal(0.1f, defaultRow.Current);
Assert.Equal(0.1f, activeRow.Current);
}
[Fact]
public void OnShown_ReSeedsBothSliders_FromLiveOpacity()
{
var fakeBindings = new FakeBindings { DefaultOpacity = 0.5f, ActiveOpacity = 1.0f };
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
bindings.DefaultOpacity = 0.2f;
bindings.ActiveOpacity = 0.9f;
controller.ChatPage.OnShown();
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
Assert.Equal(0.2f, defaultRow.Current);
Assert.Equal(0.2f, defaultRow.Saved);
Assert.Equal(0.9f, activeRow.Current);
Assert.Equal(0.9f, activeRow.Saved);
Assert.False(controller.ChatPage.Changed);
}
[Fact]
public void Defaults_RestoresDatExtractedSliderDefaults()
{
var fakeBindings = new FakeBindings
{
DefaultOpacity = 0.1f,
ActiveOpacity = 0.1f,
DefaultOpacityDatDefault = 0.5f,
ActiveOpacityDatDefault = 1.0f,
};
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
controller.ChatPage.Defaults();
var defaultRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType<FloatOptionRow>(controller.ChatPage.Rows[1]);
Assert.Equal(0.5f, defaultRow.Current);
Assert.Equal(1.0f, activeRow.Current);
Assert.Equal(0.5f, bindings.DefaultOpacity);
Assert.Equal(1.0f, bindings.ActiveOpacity);
}
[Fact]
public void CheckingAFilterRow_PublishesSetFilter_ForItsOwnCompactWindowId()
{
var fakeBindings = new FakeBindings();
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
bool bound = ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
Assert.True(bound);
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List<UiCheckboxBitfield64> blocks = CollectBlocks(listBox);
UiCheckboxBitfield64 floaty1Block = blocks[1]; // compact window id 1
UiCheckboxBitfield64.Row row = floaty1Block.Rows[0]; // Gameplay (floaty has it)
row.Toggle.OnClick!.Invoke();
var (windowId, value) = Assert.Single(fakeBindings.FilterSets);
Assert.Equal(1, windowId);
Assert.Equal(fakeBindings.Filters[1], value);
}
[Fact]
public void MainWindowBlock_HasNoGameplayRow()
{
var fakeBindings = new FakeBindings();
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
UiCheckboxBitfield64 mainBlock = CollectBlocks(listBox)[0];
Assert.All(mainBlock.Rows, r => Assert.NotEqual("Gameplay", r.Label));
Assert.DoesNotContain(mainBlock.Rows, r => r.LowMask == 0x83912021ul);
}
[Fact]
public void OnShown_ReSeedsFilterBlock_FromLiveChatWindowState()
{
var fakeBindings = new FakeBindings();
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
// Simulate the filter having changed behind the panel's back (e.g. a
// future second write path) between mount and the next tab-show.
bindings.Filters[ChatWindowState.MainWindowId] = 0x1ul;
controller.ChatPage.OnShown();
var mainRow = Assert.IsType<BitfieldOptionRow>(controller.ChatPage.Rows[2]); // 2 sliders + main block
Assert.Equal(0x1ul, mainRow.Current);
Assert.Equal(0x1ul, mainRow.Saved);
Assert.False(controller.ChatPage.Changed);
}
[Fact]
public void Reset_RevertsAFilterBlock_ToItsSavedBaseline()
{
var fakeBindings = new FakeBindings();
(OptionsPanelController controller, FakeBindings bindings, _) = BindRealWith(fakeBindings);
var mainRow = Assert.IsType<BitfieldOptionRow>(controller.ChatPage.Rows[2]);
ulong initial = mainRow.Current;
bindings.FilterSets.Clear();
mainRow.SetCurrentValue(0x0ul);
Assert.True(controller.ChatPage.Changed);
controller.ChatPage.Reset();
Assert.Equal(initial, mainRow.Current);
Assert.Contains(bindings.FilterSets, s => s.WindowId == ChatWindowState.MainWindowId && s.Value == initial);
}
[Fact]
public void LabelResolutionFailure_LeavesRowsBuilt_NeverInventsEnglish()
{
(OptionsPanelController controller, _, bool bound) = BindReal(resolveString: (_, _) => null);
Assert.True(bound);
Assert.Equal(7, controller.ChatPage.Rows.Count);
}
[Fact]
public void Bind_MissingListBox_ReturnsFalse_AndDoesNotThrow()
{
var emptyRoot = new ElementInfo { Id = 0, Type = 3 };
ImportedLayout emptyLayout = LayoutImporter.Build(emptyRoot, NoTex, null);
var page = new OptionPage();
bool bound = ChatOptionsPageController.Bind(
emptyLayout, page, MakeTemplateResolver(), (_, _) => null,
new FakeBindings().ToBindings());
Assert.False(bound);
Assert.Empty(page.Rows);
}
[Fact]
public void ScrollbarLinkage_ModelPointsAtTheChatListBoxScroll()
{
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
var fakeBindings = new FakeBindings();
ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
var listBox = Assert.IsType<UiTemplateListBox>(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
var scrollbar = Assert.IsType<UiScrollbar>(
layout.FindElement(ChatOptionsPageController.ScrollbarElementId));
Assert.Same(listBox.Scroll, scrollbar.Model);
}
private static (OptionsPanelController Panel, FakeBindings Bindings, bool Bound) BindRealWith(
FakeBindings fakeBindings)
{
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
OptionsPanelController controller = OptionsPanelController.Bind(
layout,
new OptionsPanelController.Callbacks(
Toggle: () => { },
RequestExitToCharacterSelection: () => { },
ExitGame: () => { },
UseMouseTurningSettings: () => { },
DisplaySystemMessage: _ => { }))!;
bool bound = ChatOptionsPageController.Bind(
layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null,
fakeBindings.ToBindings());
return (controller, fakeBindings, bound);
}
}