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;
///
/// Campaign OP slice OP5 (2026-08-11) conformance + behavior tests for
/// — the same hermetic pattern
/// CharacterOptionsPageControllerTests established: pure-data conformance
/// against the byte-verified research doc/decomp tables, then behavioral tests
/// against the committed options_panel_2100006E_1000018D.json +
/// options_2100002B.json fixtures — no live DAT access.
///
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;
}
/// The same "resolve a row template from the standalone 0x2100002B
/// fixture" resolver CharacterOptionsPageControllerTests uses.
private static Func 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 DefaultOpacitySets { get; } = new();
public List ActiveOpacitySets { get; } = new();
public Dictionary 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? 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(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List 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(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List 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(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List 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 CollectBlocks(UiElement root)
{
var found = new List();
Walk(root, found);
return found;
static void Walk(UiElement node, List 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(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType(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(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType(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(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType(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(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType(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(controller.ChatPage.Rows[0]);
var activeRow = Assert.IsType(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(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
List 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(
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(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(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(
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
var scrollbar = Assert.IsType(
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);
}
}