test(D.2b): chat golden fixture + resolved-Type conformance (widget-generalization Task 1)

- 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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 16:55:51 +02:00
parent 34e79096f3
commit d1b13a7dbf
5 changed files with 661 additions and 9 deletions

View file

@ -101,6 +101,10 @@ public sealed class UiChatInput : UiElement
_historyIndex = -1;
}
/// <summary>Move the caret left (negative) or right (positive) by <paramref name="delta"/>
/// glyph positions without extending a selection. Public for test access.</summary>
public void MoveCaret(int delta) => MoveCaretTo(_caret + delta, false);
private void MoveCaret(int delta, bool shift) => MoveCaretTo(_caret + delta, shift);
// ── Selection ────────────────────────────────────────────────────────

View file

@ -0,0 +1,46 @@
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// 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).
/// </summary>
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)
}
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
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");
}

View file

@ -5,9 +5,9 @@ using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Loads the committed vitals ElementInfo fixture and builds the widget tree
/// no dats required. The fixture was generated from layout <c>0x2100006C</c>
/// via the real portal.dat and serialized with <see cref="System.Text.Json"/>.
/// Loads the committed layout ElementInfo fixtures and builds widget trees
/// no dats required. Fixtures were generated from the real portal.dat and
/// serialized with <see cref="System.Text.Json"/>.
/// </summary>
public static class FixtureLoader
{
@ -37,18 +37,39 @@ public static class FixtureLoader
/// widget factory.
/// </summary>
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);
/// <summary>
/// Deserializes the committed <c>chat_21000006.json</c> fixture into a raw
/// <see cref="ElementInfo"/> tree and builds the <see cref="ImportedLayout"/>
/// using a null-returning sprite resolver and no dat font — sufficient for
/// conformance checks on tree structure and resolved types.
/// </summary>
public static ImportedLayout LoadChat()
=> LayoutImporter.Build(LoadChatInfos(), _ => (0u, 0, 0), null);
/// <summary>
/// Deserializes the committed <c>chat_21000006.json</c> fixture into a raw
/// <see cref="ElementInfo"/> tree WITHOUT calling <see cref="LayoutImporter.Build"/>.
/// Use this when the test needs to inspect the resolved <see cref="ElementInfo"/>
/// tree directly (e.g. resolved Type values per element id).
/// </summary>
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<T>(ReadOnlySpan<byte>)
// does not reject the first byte.
ReadOnlySpan<byte> span = bytes;
if (span.Length >= 3 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF)
span = span[3..];
return JsonSerializer.Deserialize<AcDream.App.UI.Layout.ElementInfo>(span, _opts)
?? throw new InvalidOperationException($"fixture deserialized to null: {fixturePath}");
?? throw new InvalidOperationException($"fixture deserialized to null: {path}");
}
}

View file

@ -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": []
}
]
}
]
}