From d1b13a7dbf7f92a8b380dedbd7470eb777193e1f Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 16 Jun 2026 16:55:51 +0200 Subject: [PATCH] test(D.2b): chat golden fixture + resolved-Type conformance (widget-generalization Task 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ChatLayoutFixtureGenerator.cs (Skip-by-default) to regenerate chat_21000006.json from the live portal.dat via LayoutImporter.ImportInfos - Commit generated fixture chat_21000006.json (13 KB, 400 lines) — dat-free, auto-copied to test output via existing *.json csproj glob - Refactor FixtureLoader: extract shared LoadInfos(fileName) helper; add LoadChat() + LoadChatInfos() mirroring the vitals pattern; LoadVitalsInfos() now delegates to the shared loader (behavior unchanged, vitals tests green) - Add ChatLayoutConformanceTests: ResolvesKnownElements + ResolvedTypes_MatchRetailRegistry Confirmed resolved Types from live dat: 0x10000011 (transcript) → Type 12 (style-prototype, skipped by factory) 0x10000016 (input) → Type 12 (style-prototype, skipped by factory) 0x10000014 (menu) → Type 6 0x10000012 (scrollbar) → Type 11 0x10000019 (send) → Type 1 0x1000046F (max/min) → Type 1 Also fix pre-existing build break: UiChatInput.MoveCaret(int delta) was made private in ce848c1 but UiChatInputTests.Backspace_DeletesBeforeCaret called it as public. Expose a public MoveCaret(int) overload (no-shift) alongside the private MoveCaret(int,bool) — restores the intended test surface. Full suite: 398 passed, 2 skipped (generator + pre-existing), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/UiChatInput.cs | 4 + .../UI/Layout/ChatLayoutConformanceTests.cs | 46 ++ .../UI/Layout/ChatLayoutFixtureGenerator.cs | 39 ++ .../UI/Layout/FixtureLoader.cs | 39 +- .../UI/Layout/fixtures/chat_21000006.json | 542 ++++++++++++++++++ 5 files changed, 661 insertions(+), 9 deletions(-) create mode 100644 tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs create mode 100644 tests/AcDream.App.Tests/UI/Layout/ChatLayoutFixtureGenerator.cs create mode 100644 tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json diff --git a/src/AcDream.App/UI/UiChatInput.cs b/src/AcDream.App/UI/UiChatInput.cs index 58c6e4a0..730a7175 100644 --- a/src/AcDream.App/UI/UiChatInput.cs +++ b/src/AcDream.App/UI/UiChatInput.cs @@ -101,6 +101,10 @@ public sealed class UiChatInput : UiElement _historyIndex = -1; } + /// Move the caret left (negative) or right (positive) by + /// glyph positions without extending a selection. Public for test access. + public void MoveCaret(int delta) => MoveCaretTo(_caret + delta, false); + private void MoveCaret(int delta, bool shift) => MoveCaretTo(_caret + delta, shift); // ── Selection ──────────────────────────────────────────────────────── diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs new file mode 100644 index 00000000..836adbdc --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs @@ -0,0 +1,46 @@ +using AcDream.App.UI.Layout; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// Dat-free conformance tests for the committed chat_21000006.json golden fixture. +/// Verifies that LayoutImporter.ImportInfos correctly resolves the BaseElement / +/// BaseLayoutId inheritance chain for the chat window (LayoutDesc 0x21000006). +/// +public class ChatLayoutConformanceTests +{ + 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; + } + + [Fact] + public void ChatFixture_ResolvesKnownElements() + { + var root = FixtureLoader.LoadChatInfos(); + Assert.NotNull(Find(root, 0x10000011u)); // transcript + Assert.NotNull(Find(root, 0x10000016u)); // input + Assert.NotNull(Find(root, 0x10000012u)); // scrollbar track + Assert.NotNull(Find(root, 0x10000014u)); // channel menu + Assert.NotNull(Find(root, 0x10000019u)); // send button + Assert.NotNull(Find(root, 0x1000046Fu)); // max/min button + } + + [Fact] + public void ChatFixture_ResolvedTypes_MatchRetailRegistry() + { + var root = FixtureLoader.LoadChatInfos(); + Assert.Equal(6u, Find(root, 0x10000014u)!.Type); // Menu + Assert.Equal(11u, Find(root, 0x10000012u)!.Type); // Scrollbar + Assert.Equal(1u, Find(root, 0x10000019u)!.Type); // Button (Send) + Assert.Equal(1u, Find(root, 0x1000046Fu)!.Type); // Button (Max/Min) + Assert.Equal(12u, Find(root, 0x10000011u)!.Type); // Text/style-prototype (transcript) + Assert.Equal(12u, Find(root, 0x10000016u)!.Type); // Text/style-prototype (input) + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutFixtureGenerator.cs b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutFixtureGenerator.cs new file mode 100644 index 00000000..cdc89c5f --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutFixtureGenerator.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text.Json; +using AcDream.App.UI.Layout; +using DatReaderWriter; +using DatReaderWriter.Options; + +namespace AcDream.App.Tests.UI.Layout; + +/// +/// One-off generator for the committed chat golden fixture. Skipped by default — +/// run manually with the real dats present (set ACDREAM_DAT_DIR) to regenerate +/// chat_21000006.json, then commit it. Mirrors how vitals_2100006C.json was made. +/// +public class ChatLayoutFixtureGenerator +{ + [Fact(Skip = "manual: regenerates the committed chat fixture; needs the real dats (ACDREAM_DAT_DIR)")] + public void GenerateChatFixture() + { + var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Documents", "Asheron's Call"); + using var dats = new DatCollection(datDir, DatAccessType.Read); + var info = LayoutImporter.ImportInfos(dats, 0x21000006u); + Assert.NotNull(info); + + var json = JsonSerializer.Serialize(info, new JsonSerializerOptions + { + IncludeFields = true, + WriteIndented = true, + }); + File.WriteAllText(FixturePath(), json); + } + + // Resolve the SOURCE fixtures dir (not bin/) from this file's compile-time path. + private static string FixturePath([CallerFilePath] string thisFile = "") + => Path.Combine(Path.GetDirectoryName(thisFile)!, "fixtures", "chat_21000006.json"); +} diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index 724a0e89..c7338ba1 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -5,9 +5,9 @@ using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; /// -/// Loads the committed vitals ElementInfo fixture and builds the widget tree — -/// no dats required. The fixture was generated from layout 0x2100006C -/// via the real portal.dat and serialized with . +/// Loads the committed layout ElementInfo fixtures and builds widget trees — +/// no dats required. Fixtures were generated from the real portal.dat and +/// serialized with . /// public static class FixtureLoader { @@ -37,18 +37,39 @@ public static class FixtureLoader /// widget factory. /// public static AcDream.App.UI.Layout.ElementInfo LoadVitalsInfos() - { - var fixturePath = Path.Combine(AppContext.BaseDirectory, "UI", "Layout", "fixtures", "vitals_2100006C.json"); - if (!File.Exists(fixturePath)) - throw new FileNotFoundException($"Vitals fixture not found at: {fixturePath}"); + => LoadInfos("vitals_2100006C.json"); - var bytes = File.ReadAllBytes(fixturePath); + /// + /// Deserializes the committed chat_21000006.json fixture into a raw + /// tree and builds the + /// using a null-returning sprite resolver and no dat font — sufficient for + /// conformance checks on tree structure and resolved types. + /// + public static ImportedLayout LoadChat() + => LayoutImporter.Build(LoadChatInfos(), _ => (0u, 0, 0), null); + + /// + /// Deserializes the committed chat_21000006.json fixture into a raw + /// tree WITHOUT calling . + /// Use this when the test needs to inspect the resolved + /// tree directly (e.g. resolved Type values per element id). + /// + public static AcDream.App.UI.Layout.ElementInfo LoadChatInfos() + => LoadInfos("chat_21000006.json"); + + // ── Shared loader ──────────────────────────────────────────────────────── + + private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName) + { + var path = Path.Combine(AppContext.BaseDirectory, "UI", "Layout", "fixtures", fileName); + if (!File.Exists(path)) throw new FileNotFoundException($"fixture not found at: {path}"); + var bytes = File.ReadAllBytes(path); // Strip UTF-8 BOM (EF BB BF) if present so JsonSerializer.Deserialize(ReadOnlySpan) // does not reject the first byte. ReadOnlySpan span = bytes; if (span.Length >= 3 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF) span = span[3..]; return JsonSerializer.Deserialize(span, _opts) - ?? throw new InvalidOperationException($"fixture deserialized to null: {fixturePath}"); + ?? throw new InvalidOperationException($"fixture deserialized to null: {path}"); } } diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json new file mode 100644 index 00000000..37783bb7 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json @@ -0,0 +1,542 @@ +{ + "Id": 0, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 0, + "Height": 0, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 0, + "FontDid": 0, + "StateMedia": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435484, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 382, + "Height": 104, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100667980, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435485, + "Type": 5, + "X": 0, + "Y": 2, + "Width": 382, + "Height": 102, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268436774, + "Type": 1, + "X": 2, + "Y": 0, + "Width": 16, + "Height": 16, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 3, + "FontDid": 1073741861, + "StateMedia": { + "Normal": { + "Item1": 100688408, + "Item2": 1 + }, + "Highlight": { + "Item1": 100688409, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435486, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 191, + "Height": 17, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 2, + "FontDid": 1073741825, + "StateMedia": { + "Normal": { + "Item1": 100667982, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100667982, + "Item2": 1 + }, + "Talkfocus_highlight": { + "Item1": 100667981, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435470, + "Type": 268435521, + "X": 0, + "Y": 0, + "Width": 800, + "Height": 100, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 0, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100667725, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [ + { + "Id": 268436772, + "Type": 1, + "X": 0, + "Y": 46, + "Width": 16, + "Height": 16, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 6, + "FontDid": 1073741861, + "StateMedia": { + "Normal": { + "Item1": 100688408, + "Item2": 1 + }, + "Highlight": { + "Item1": 100688409, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268436773, + "Type": 1, + "X": 0, + "Y": 64, + "Width": 16, + "Height": 16, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 7, + "FontDid": 1073741861, + "StateMedia": { + "Normal": { + "Item1": 100688408, + "Item2": 1 + }, + "Highlight": { + "Item1": 100688409, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268436591, + "Type": 1, + "X": 474, + "Y": 0, + "Width": 16, + "Height": 16, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 3, + "FontDid": 0, + "StateMedia": { + "Maximized": { + "Item1": 100687460, + "Item2": 1 + }, + "Minimized": { + "Item1": 100687461, + "Item2": 1 + } + }, + "DefaultStateName": "Minimized", + "Children": [] + }, + { + "Id": 268435471, + "Type": 9, + "X": 0, + "Y": 0, + "Width": 800, + "Height": 9, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100667685, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435472, + "Type": 3, + "X": 0, + "Y": 9, + "Width": 490, + "Height": 74, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100667669, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435473, + "Type": 12, + "X": 16, + "Y": 0, + "Width": 458, + "Height": 74, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 1073741824, + "StateMedia": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268436620, + "Type": 1, + "X": 0, + "Y": 58, + "Width": 16, + "Height": 16, + "Left": 3, + "Top": 2, + "Right": 3, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": { + "Normal": { + "Item1": 100687630, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100687630, + "Item2": 1 + } + }, + "DefaultStateName": "Ghosted", + "Children": [] + } + ] + }, + { + "Id": 268435474, + "Type": 11, + "X": 474, + "Y": 6, + "Width": 16, + "Height": 68, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435475, + "Type": 3, + "X": 0, + "Y": 83, + "Width": 490, + "Height": 17, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 8, + "FontDid": 0, + "StateMedia": { + "": { + "Item1": 100667706, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435476, + "Type": 6, + "X": 0, + "Y": 0, + "Width": 46, + "Height": 17, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": { + "Normal": { + "Item1": 100683109, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100683110, + "Item2": 3 + } + }, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268435477, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 46, + "Height": 17, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 1073741826, + "StateMedia": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435478, + "Type": 12, + "X": 46, + "Y": 0, + "Width": 398, + "Height": 17, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "FontDid": 1073741824, + "StateMedia": { + "Normal_focussed": { + "Item1": 100667819, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435479, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 1, + "Height": 17, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 1, + "ReadOrder": 1, + "FontDid": 0, + "StateMedia": { + "Normal_focussed": { + "Item1": 100683111, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435480, + "Type": 3, + "X": 397, + "Y": 0, + "Width": 1, + "Height": 17, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "FontDid": 0, + "StateMedia": { + "Normal_focussed": { + "Item1": 100683111, + "Item2": 1 + } + }, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435481, + "Type": 1, + "X": 444, + "Y": 0, + "Width": 46, + "Height": 17, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "FontDid": 1073741826, + "StateMedia": { + "Normal": { + "Item1": 100669717, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100669718, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100669748, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + } + ] + }, + { + "Id": 268436770, + "Type": 1, + "X": 0, + "Y": 10, + "Width": 16, + "Height": 16, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 4, + "FontDid": 1073741861, + "StateMedia": { + "Normal": { + "Item1": 100688408, + "Item2": 1 + }, + "Highlight": { + "Item1": 100688409, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268436771, + "Type": 1, + "X": 0, + "Y": 28, + "Width": 16, + "Height": 16, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 5, + "FontDid": 1073741861, + "StateMedia": { + "Normal": { + "Item1": 100688408, + "Item2": 1 + }, + "Highlight": { + "Item1": 100688409, + "Item2": 1 + } + }, + "DefaultStateName": "Normal", + "Children": [] + } + ] + } + ] +} \ No newline at end of file