feat(ui): Campaign OP slice OP2 — tab control, template ListBox, UIOption widget mappings
Ships the two new widget primitives the retail Options panel needs plus the
four remaining UIOption_* factory mappings, so every tab page (OP3-OP6) has
somewhere to mount.
- ElementReader/ElementInfo gain three new dat-property readers, following
the existing effective-state-resolution pattern (never a per-state
first-wins scan, per the round-5 N1 lesson): the Type-8 tab table
(property 0x2E -> TabTable), a ListBox's row-template list (property
0x64 -> TemplateList), and scrollbar linkage (property 0x72 ->
ScrollbarElementId). LayoutImporter gains one hook
(IUiChildrenAttachedListener) so a widget can resolve cross-references
its own dat properties name by id once its subtree actually exists.
- UiTabControl (Type 8): switches exactly one page-slot child visible,
syncs each tab button's Open/Closed state via the existing
RetailTabBinding helper, and honors the authored default tab on mount.
- UiTemplateListBox (Type 5 with an authored template list): wraps a
UiScrollablePanel viewport (sealed, so composition not inheritance) and
ports AddItemFromTemplateList(index) — the resolver seam a page
controller wires with real DAT access via the SAME
LayoutImporter.ImportInfos(dats, layoutId, elementId) overload
RetailDialogFactory already uses for its catalog LayoutDesc.
- DatWidgetFactory maps the four remaining UIOption_* widgets, each
verified against the regenerated options_2100002B.json fixture before
writing any code: 0x10000037 (Slider) is structurally an ordinary
horizontal UIElement_Scrollbar, so it reuses BuildScrollbar directly;
0x10000038 (Menu) is structurally identical to the vendor category
dropdown UiMenu already models, so it reuses `new UiMenu()` like the
Type-6 case; 0x10000036 (CheckboxSlider) composes an existing
UIOption_Checkbox child + UIOption_Slider child via the new
UiOptionToggleSlider wrapper; 0x10000044 (CheckboxBitfield64) authors
zero children in the dat (every row is added at runtime via retail's own
AddChild(lowMask, highMask, label, tooltip) call shape), so it's a new
UiCheckboxBitfield64 composing UiButton per row. No new drawing code
anywhere in this set.
- Five new committed fixtures (options_2100002B/2100002A/21000028/
2100005C/21000029) plus 25 new conformance tests pinning the tab table
(4 entries, Gameplay default), all three template arrays, scrollbar
linkage, every new widget-type mapping, and a UiTabControl behavioral
test (switch -> exactly one page visible, click-through the tab
button). The Character ListBox's authored 6-header/49-toggle shape
(lane B section counts) is proven reachable end-to-end through
AddItemFromTemplateList against the committed fixture.
- Regenerating fixtures also touched 27 PRE-EXISTING, unrelated fixtures
(an Outline/OutlineColor field pair added by an earlier commit,
bcc34ee3, that predates when those fixtures were last regenerated).
Per the slice contract, that drift was NOT committed — reverted back to
HEAD, only the five new Options-panel fixtures are new files here.
- Filed TS-72: UiCheckboxBitfield64's click-toggle bit math (AND/OR
set/clear semantics) is a documented approximation — the decompiled
excerpt this campaign pulled covers UIOption_CheckboxBitfield64::Apply's
WRITE side, not its own click-handler's bit math. Flagged for OP5 (the
Chat tab controller, the first consumer that reaches the wire) to
verify against the real decomp before any live transaction depends on
it; nothing user-reachable can observe this yet.
Full Release suite: 12,770 passed / 4 skipped / 0 failed (was 12,745/4/0
post-OP1 — 25 net new tests, zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0df0a60424
commit
df9c7a35eb
17 changed files with 51896 additions and 1 deletions
|
|
@ -211,6 +211,43 @@ public static class FixtureLoader
|
|||
public static ElementInfo LoadVendorInfos()
|
||||
=> LoadInfos("vendor_21000012_100000B7.json");
|
||||
|
||||
/// <summary>Options panel LayoutDesc <c>0x2100002B</c> — the tab control + all four
|
||||
/// mounted pages (via BaseLayoutId/BaseElement) + the seven row-template elements,
|
||||
/// as one combined tree (Campaign OP slice OP2).</summary>
|
||||
public static ImportedLayout LoadOptionsPanel()
|
||||
=> LayoutImporter.Build(LoadOptionsPanelInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadOptionsPanelInfos()
|
||||
=> LoadInfos("options_2100002B.json");
|
||||
|
||||
/// <summary>Gameplay Options page LayoutDesc <c>0x2100002A</c> (standalone import).</summary>
|
||||
public static ImportedLayout LoadOptionsGameplay()
|
||||
=> LayoutImporter.Build(LoadOptionsGameplayInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadOptionsGameplayInfos()
|
||||
=> LoadInfos("options_gameplay_2100002A.json");
|
||||
|
||||
/// <summary>Character page LayoutDesc <c>0x21000028</c> (standalone import).</summary>
|
||||
public static ImportedLayout LoadOptionsCharacter()
|
||||
=> LayoutImporter.Build(LoadOptionsCharacterInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadOptionsCharacterInfos()
|
||||
=> LoadInfos("options_character_21000028.json");
|
||||
|
||||
/// <summary>Chat page LayoutDesc <c>0x2100005C</c> (standalone import).</summary>
|
||||
public static ImportedLayout LoadOptionsChat()
|
||||
=> LayoutImporter.Build(LoadOptionsChatInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadOptionsChatInfos()
|
||||
=> LoadInfos("options_chat_2100005C.json");
|
||||
|
||||
/// <summary>Config page LayoutDesc <c>0x21000029</c> (standalone import).</summary>
|
||||
public static ImportedLayout LoadOptionsConfig()
|
||||
=> LayoutImporter.Build(LoadOptionsConfigInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadOptionsConfigInfos()
|
||||
=> LoadInfos("options_config_21000029.json");
|
||||
|
||||
// ── Shared loader ────────────────────────────────────────────────────────
|
||||
|
||||
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
using System.Linq;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Dat-free conformance tests for the committed <c>options_2100002B.json</c> /
|
||||
/// <c>options_gameplay_2100002A.json</c> / <c>options_character_21000028.json</c> /
|
||||
/// <c>options_chat_2100005C.json</c> / <c>options_config_21000029.json</c> golden
|
||||
/// fixtures (Campaign OP slice OP2). Pins the tab table (dat property <c>0x2E</c>),
|
||||
/// the three ListBox row-template lists (dat property <c>0x64</c>), scrollbar
|
||||
/// linkage (dat property <c>0x72</c>), the new Type-8/5/<c>0x10000036..0x44</c>
|
||||
/// widget mappings, and <see cref="UiTabControl"/>'s switch behavior. Retail anchors:
|
||||
/// <c>docs/research/2026-08-10-options-panel-structure.md</c> §1.3, §1.5, §10.1.
|
||||
/// </summary>
|
||||
public class OptionsPanelLayoutConformanceTests
|
||||
{
|
||||
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 (var c in n.Children)
|
||||
{
|
||||
var f = Find(c, id);
|
||||
if (f is not null) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Tab table (property 0x2E) ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_TabControl_HasFourEntriesInAuthoredOrder_GameplayDefault()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsPanelInfos();
|
||||
var tabControl = Find(root, 0x10000208u);
|
||||
Assert.NotNull(tabControl);
|
||||
Assert.Equal(8u, tabControl!.Type);
|
||||
|
||||
var tabs = tabControl.TabTable;
|
||||
Assert.Equal(4, tabs.Count);
|
||||
|
||||
Assert.Equal(new UiTabTableEntry(0x1000020Du, 0x10000212u, true), tabs[0]); // Gameplay Options (default)
|
||||
Assert.Equal(new UiTabTableEntry(0x1000020Eu, 0x10000211u, false), tabs[1]); // Character
|
||||
Assert.Equal(new UiTabTableEntry(0x1000050Bu, 0x1000050Cu, false), tabs[2]); // Chat
|
||||
Assert.Equal(new UiTabTableEntry(0x1000020Fu, 0x10000213u, false), tabs[3]); // Config
|
||||
|
||||
Assert.Single(tabs, t => t.IsDefault);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_TabControl_BuildsAsUiTabControl_WithSameTabTable()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
Assert.Equal(4, tabControl.Tabs.Count);
|
||||
Assert.True(tabControl.Tabs[0].IsDefault);
|
||||
Assert.Equal(0x10000212u, tabControl.Tabs[0].PageElementId);
|
||||
}
|
||||
|
||||
// ── Row-template lists (property 0x64) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CharacterListBoxFixture_TemplateList_MatchesThreeRowTemplates()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsCharacterInfos();
|
||||
var listBox = Find(root, 0x100001FAu);
|
||||
Assert.NotNull(listBox);
|
||||
Assert.Equal(5u, listBox!.Type);
|
||||
|
||||
var templates = listBox.TemplateList;
|
||||
Assert.Equal(3, templates.Count);
|
||||
Assert.Equal(new UiTemplateListEntry(0x2100002Bu, 0x10000216u), templates[0]); // header
|
||||
Assert.Equal(new UiTemplateListEntry(0x2100002Bu, 0x10000217u), templates[1]); // separator
|
||||
Assert.Equal(new UiTemplateListEntry(0x2100002Bu, 0x10000218u), templates[2]); // toggle
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigListBoxFixture_TemplateList_MatchesEightRowTemplates()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsConfigInfos();
|
||||
var listBox = Find(root, 0x10000200u);
|
||||
Assert.NotNull(listBox);
|
||||
|
||||
var templates = listBox!.TemplateList;
|
||||
Assert.Equal(8, templates.Count);
|
||||
uint[] expectedElementIds =
|
||||
{
|
||||
0x10000216u, 0x10000217u, 0x10000218u, 0x1000021Au,
|
||||
0x10000222u, 0x10000220u, 0x1000021Du, 0x10000221u,
|
||||
};
|
||||
for (int i = 0; i < expectedElementIds.Length; i++)
|
||||
{
|
||||
Assert.Equal(0x2100002Bu, templates[i].TemplateLayoutId);
|
||||
Assert.Equal(expectedElementIds[i], templates[i].TemplateElementId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatListBoxFixture_TemplateList_MatchesNineRowTemplatesInclBitfield64()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsChatInfos();
|
||||
var listBox = Find(root, 0x1000050Du);
|
||||
Assert.NotNull(listBox);
|
||||
|
||||
var templates = listBox!.TemplateList;
|
||||
Assert.Equal(9, templates.Count);
|
||||
uint[] expectedElementIds =
|
||||
{
|
||||
0x10000216u, 0x10000217u, 0x10000218u, 0x1000021Au, 0x10000222u,
|
||||
0x10000220u, 0x1000021Du, 0x10000221u, 0x10000520u, // bitfield64
|
||||
};
|
||||
for (int i = 0; i < expectedElementIds.Length; i++)
|
||||
{
|
||||
Assert.Equal(0x2100002Bu, templates[i].TemplateLayoutId);
|
||||
Assert.Equal(expectedElementIds[i], templates[i].TemplateElementId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scrollbar linkage (property 0x72) ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CharacterListBoxFixture_ScrollbarLinkage_Names0x100001FB()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsCharacterInfos();
|
||||
var listBox = Find(root, 0x100001FAu);
|
||||
Assert.Equal(0x100001FBu, listBox!.ScrollbarElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigListBoxFixture_ScrollbarLinkage_Names0x10000201()
|
||||
{
|
||||
var root = FixtureLoader.LoadOptionsConfigInfos();
|
||||
var listBox = Find(root, 0x10000200u);
|
||||
Assert.Equal(0x10000201u, listBox!.ScrollbarElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatListBoxFixture_ScrollbarLinkage_SharesElement0x10000201WithConfig()
|
||||
{
|
||||
// Research doc §10.1: Config's and Chat's ListBoxes both name the SAME
|
||||
// scrollbar element id — the two tabs share one gutter widget authored twice.
|
||||
var root = FixtureLoader.LoadOptionsChatInfos();
|
||||
var listBox = Find(root, 0x1000050Du);
|
||||
Assert.Equal(0x10000201u, listBox!.ScrollbarElementId);
|
||||
}
|
||||
|
||||
// ── Built-widget mapping: ListBox → UiTemplateListBox ───────────────────
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_CharacterListBox_BuildsAsUiTemplateListBoxWithSameData()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(layout.FindElement(0x100001FAu));
|
||||
Assert.Equal(3, listBox.Templates.Count);
|
||||
Assert.Equal(0x100001FBu, listBox.ScrollbarElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_CharacterScrollbar_StillBuildsAsUiScrollbar()
|
||||
{
|
||||
// Regression guard: a real scrollbar sibling (Type 11) must keep building
|
||||
// through the EXISTING BuildScrollbar mapping — OP2 must not disturb it.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
Assert.IsType<UiScrollbar>(layout.FindElement(0x100001FBu));
|
||||
}
|
||||
|
||||
// ── The four remaining UIOption_* widget mappings ───────────────────────
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_SliderControl_BuildsAsHorizontalUiScrollbar()
|
||||
{
|
||||
// UIOption_Slider (0x10000037), template element 0x1000021C: 120x12,
|
||||
// W>H so BuildScrollbar's existing horizontal branch applies, with the
|
||||
// child-id-1 thumb convention it already implements.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var slider = Assert.IsType<UiScrollbar>(layout.FindElement(0x1000021Cu));
|
||||
Assert.True(slider.Horizontal);
|
||||
Assert.NotEqual(0u, slider.ThumbSprite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_MenuControl_BuildsAsUiMenu()
|
||||
{
|
||||
// UIOption_Menu (0x10000038), template element 0x10000224.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
Assert.IsType<UiMenu>(layout.FindElement(0x10000224u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_ToggleSliderRow_ComposesCheckboxAndSlider()
|
||||
{
|
||||
// UIOption_CheckboxSlider (0x10000036), unlabelled row template 0x10000220:
|
||||
// composes an existing UIOption_Checkbox (0x10000219 -> UiButton) and an
|
||||
// existing UIOption_Slider (0x1000021C -> UiScrollbar) — no new drawing code.
|
||||
// (0x10000219/0x1000021C are reused verbatim across every checkbox/slider
|
||||
// template instance, so this asserts identity against row.Children directly
|
||||
// rather than layout.FindElement — which, like CharacterStatController's own
|
||||
// documented caveat, returns only the LAST of several same-id duplicates.)
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var row = Assert.IsType<UiOptionToggleSlider>(layout.FindElement(0x10000220u));
|
||||
Assert.NotNull(row.Toggle);
|
||||
Assert.NotNull(row.Slider);
|
||||
Assert.Contains(row.Toggle, row.Children);
|
||||
Assert.Contains(row.Slider, row.Children);
|
||||
Assert.Equal(0x10000219u, row.Toggle!.DatElementId);
|
||||
Assert.Equal(0x1000021Cu, row.Slider!.DatElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_LabelledToggleSliderRow_AlsoComposesCheckboxAndSlider()
|
||||
{
|
||||
// Labelled variant 0x10000221 (four children: checkbox + slider + two range
|
||||
// labels) — the two extra Text children must not confuse the composition.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var row = Assert.IsType<UiOptionToggleSlider>(layout.FindElement(0x10000221u));
|
||||
Assert.NotNull(row.Toggle);
|
||||
Assert.NotNull(row.Slider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_Bitfield64Template_BuildsAsEmptyUiCheckboxBitfield64()
|
||||
{
|
||||
// UIOption_CheckboxBitfield64 (0x10000044), template element 0x10000520:
|
||||
// authors zero children/media in the dat — every row is added at runtime.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var bitfield = Assert.IsType<UiCheckboxBitfield64>(layout.FindElement(0x10000520u));
|
||||
Assert.Empty(bitfield.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsPanelFixture_LabelledSliderRow_StillPlainContainer()
|
||||
{
|
||||
// Template idx 6 (0x1000021D) is the LABELLED slider (label + slider + two
|
||||
// range labels) but its OWN root Type is 3 (plain container), not
|
||||
// UIOption_CheckboxSlider — distinguishing this from 0x10000221 is exactly
|
||||
// what makes the mapping data-driven rather than guessed.
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var row = layout.FindElement(0x1000021Du);
|
||||
Assert.IsNotType<UiOptionToggleSlider>(row);
|
||||
var slider = Assert.IsType<UiScrollbar>(layout.FindElement(0x1000021Cu));
|
||||
Assert.NotNull(slider);
|
||||
}
|
||||
|
||||
// ── UiCheckboxBitfield64 behavior (structural, click semantics flagged) ──
|
||||
|
||||
[Fact]
|
||||
public void UiCheckboxBitfield64_AddChild_TracksMaskAndTogglesOnClick()
|
||||
{
|
||||
var bitfield = new UiCheckboxBitfield64 { Width = 272f };
|
||||
bitfield.SetDefaultValue(0x00000000_FBFFFFFFul, 0ul); // retail main-window default (research §5.2)
|
||||
|
||||
// "Error" (mask 0x04000000) is the ONE bit clear in the FBFFFFFF default —
|
||||
// research §5.2's own cross-check ("Society bit opt-in") confirms every
|
||||
// other row's bits are already set in that default, so Error is the only
|
||||
// mask that starts observably OFF and round-trips cleanly through OR/AND-NOT.
|
||||
var row = bitfield.AddChild(0x00000000_04000000ul, 0ul, "Error");
|
||||
Assert.False(row.Selected);
|
||||
|
||||
ulong? changedLow = null;
|
||||
bitfield.ValueChanged += (low, _) => changedLow = low;
|
||||
row.OnClick!.Invoke();
|
||||
|
||||
Assert.True(row.Selected);
|
||||
Assert.Equal(0x00000000_FFFFFFFFul, changedLow); // FBFFFFFF | 04000000 == FFFFFFFF
|
||||
|
||||
row.OnClick!.Invoke();
|
||||
Assert.False(row.Selected);
|
||||
Assert.Equal(0x00000000_FBFFFFFFul, changedLow); // back to the seeded default
|
||||
}
|
||||
|
||||
// ── The template mechanism, exercised end-to-end ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Proves the Character ListBox's authored structure (6 headers + 49 toggles,
|
||||
/// research doc §7 / §10.1) is REACHABLE through
|
||||
/// <see cref="UiTemplateListBox.AddItemFromTemplateList"/> — each call resolves
|
||||
/// its template through the SAME <c>LayoutImporter.Build</c> machinery a real
|
||||
/// controller would use, sourced from the committed <c>options_2100002B.json</c>
|
||||
/// fixture (the templates' one true home).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CharacterListBox_TemplateMechanism_ReachesSixHeadersAndFortyNineToggles()
|
||||
{
|
||||
ElementInfo panelRoot = FixtureLoader.LoadOptionsPanelInfos();
|
||||
UiElement? Resolve(uint layoutId, uint elementId)
|
||||
{
|
||||
if (layoutId != 0x2100002Bu) return null;
|
||||
ElementInfo? templateInfo = Find(panelRoot, elementId);
|
||||
return templateInfo is null ? null : LayoutImporter.Build(templateInfo, NoTex, null).Root;
|
||||
}
|
||||
|
||||
ElementInfo characterListBoxInfo = Find(FixtureLoader.LoadOptionsCharacterInfos(), 0x100001FAu)!;
|
||||
var listBox = new UiTemplateListBox(characterListBoxInfo.TemplateList, characterListBoxInfo.ScrollbarElementId)
|
||||
{
|
||||
TemplateResolver = Resolve,
|
||||
};
|
||||
|
||||
// Lane B §7 authored section row counts (docs/research/2026-08-10-options-panel-structure.md):
|
||||
// User Interface Behavior=3, User Interface Display=15, Grouping=6,
|
||||
// Other Players=11, Character Behavior=7, Chat=7 -> 49 toggles under 6 headers.
|
||||
int[] sectionToggleCounts = { 3, 15, 6, 11, 7, 7 };
|
||||
int headerCalls = 0, toggleCalls = 0;
|
||||
foreach (int toggleCount in sectionToggleCounts)
|
||||
{
|
||||
var header = listBox.AddItemFromTemplateList(0); // header template
|
||||
Assert.IsType<UiText>(header);
|
||||
headerCalls++;
|
||||
|
||||
for (int i = 0; i < toggleCount; i++)
|
||||
{
|
||||
var toggleRow = listBox.AddItemFromTemplateList(2); // toggle template
|
||||
Assert.NotNull(toggleRow);
|
||||
// Toggle template 0x10000218 is a plain Type-3 container wrapping ONE
|
||||
// UIOption_Checkbox child — proves the SAME BuildCheckbox mapping
|
||||
// fires through the template mechanism, not just for authored trees.
|
||||
Assert.IsType<UiButton>(Assert.Single(toggleRow!.Children));
|
||||
toggleCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(6, headerCalls);
|
||||
Assert.Equal(49, toggleCalls);
|
||||
|
||||
// 6 headers @22px + 49 toggles @20px (template heights 0x10000216/0x10000218).
|
||||
Assert.Equal(6 * 22 + 49 * 20, listBox.ContentHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TemplateListBox_AddItemFromTemplateList_WithoutResolver_ReturnsNull()
|
||||
{
|
||||
ElementInfo characterListBoxInfo = Find(FixtureLoader.LoadOptionsCharacterInfos(), 0x100001FAu)!;
|
||||
var listBox = new UiTemplateListBox(
|
||||
characterListBoxInfo.TemplateList, characterListBoxInfo.ScrollbarElementId);
|
||||
// No TemplateResolver wired — matches DatWidgetFactory's own construction
|
||||
// (OP2 ships the mechanism; a page controller supplies the resolver).
|
||||
Assert.Null(listBox.AddItemFromTemplateList(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TemplateListBox_AddItemFromTemplateList_OutOfRangeIndex_ReturnsNull()
|
||||
{
|
||||
var listBox = new UiTemplateListBox(new[] { new UiTemplateListEntry(1u, 2u) }, 0u)
|
||||
{
|
||||
TemplateResolver = (_, _) => new UiDatElement(new ElementInfo(), NoTex),
|
||||
};
|
||||
Assert.Null(listBox.AddItemFromTemplateList(-1));
|
||||
Assert.Null(listBox.AddItemFromTemplateList(1));
|
||||
Assert.NotNull(listBox.AddItemFromTemplateList(0));
|
||||
}
|
||||
|
||||
// ── UiTabControl behavior ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UiTabControl_DefaultMount_ShowsOnlyTheGameplayPage()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
|
||||
Assert.Equal(0x10000212u, tabControl.ActivePageElementId); // Gameplay page slot
|
||||
|
||||
UiElement gameplayPage = layout.FindElement(0x10000212u)!;
|
||||
UiElement characterPage = layout.FindElement(0x10000211u)!;
|
||||
UiElement chatPage = layout.FindElement(0x1000050Cu)!;
|
||||
UiElement configPage = layout.FindElement(0x10000213u)!;
|
||||
|
||||
Assert.True(gameplayPage.Visible);
|
||||
Assert.False(characterPage.Visible);
|
||||
Assert.False(chatPage.Visible);
|
||||
Assert.False(configPage.Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiTabControl_SwitchTo_ShowsExactlyOnePage()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
|
||||
tabControl.SwitchTo(0x1000050Cu); // Chat page slot
|
||||
|
||||
UiElement gameplayPage = layout.FindElement(0x10000212u)!;
|
||||
UiElement characterPage = layout.FindElement(0x10000211u)!;
|
||||
UiElement chatPage = layout.FindElement(0x1000050Cu)!;
|
||||
UiElement configPage = layout.FindElement(0x10000213u)!;
|
||||
|
||||
var visiblePages = new[] { gameplayPage, characterPage, chatPage, configPage }
|
||||
.Where(p => p.Visible)
|
||||
.ToList();
|
||||
Assert.Single(visiblePages);
|
||||
Assert.Same(chatPage, visiblePages[0]);
|
||||
Assert.Equal(0x1000050Cu, tabControl.ActivePageElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiTabControl_SwitchTo_UpdatesTabButtonOpenClosedState()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
|
||||
var gameplayButton = Assert.IsType<UiText>(layout.FindElement(0x1000020Du));
|
||||
var characterButton = Assert.IsType<UiText>(layout.FindElement(0x1000020Eu));
|
||||
|
||||
Assert.Equal(RetailUiStateIds.Open, gameplayButton.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Closed, characterButton.ActiveRetailStateId);
|
||||
|
||||
tabControl.SwitchTo(0x10000211u); // Character page slot
|
||||
|
||||
Assert.Equal(RetailUiStateIds.Closed, gameplayButton.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Open, characterButton.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiTabControl_ClickingTabButton_SwitchesActivePage()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
var characterButton = layout.FindElement(0x1000020Eu)!;
|
||||
|
||||
characterButton.OnEvent(new UiEvent(0, characterButton, UiEventType.Click));
|
||||
|
||||
Assert.Equal(0x10000211u, tabControl.ActivePageElementId);
|
||||
Assert.True(layout.FindElement(0x10000211u)!.Visible);
|
||||
Assert.False(layout.FindElement(0x10000212u)!.Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiTabControl_SwitchToSameActivePage_IsANoOp()
|
||||
{
|
||||
var layout = FixtureLoader.LoadOptionsPanel();
|
||||
var tabControl = Assert.IsType<UiTabControl>(layout.FindElement(0x10000208u));
|
||||
uint before = tabControl.ActivePageElementId;
|
||||
|
||||
tabControl.SwitchTo(before);
|
||||
|
||||
Assert.Equal(before, tabControl.ActivePageElementId);
|
||||
Assert.True(layout.FindElement(before)!.Visible);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,11 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
(0x21000072u, "powerbar_21000072.json"),
|
||||
(0x21000073u, "combat_21000073.json"),
|
||||
(0x21000074u, "radar_21000074.json"),
|
||||
(0x2100002Bu, "options_2100002B.json"),
|
||||
(0x2100002Au, "options_gameplay_2100002A.json"),
|
||||
(0x21000028u, "options_character_21000028.json"),
|
||||
(0x2100005Cu, "options_chat_2100005C.json"),
|
||||
(0x21000029u, "options_config_21000029.json"),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
30786
tests/AcDream.App.Tests/UI/Layout/fixtures/options_2100002B.json
Normal file
30786
tests/AcDream.App.Tests/UI/Layout/fixtures/options_2100002B.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue