using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; /// /// Unit tests for Fix C: per-element dat FontDid resolver in . /// /// /// cannot be constructed in pure unit tests (GL + dat required), /// so these tests verify the plumbing using null/non-null identity and sentinel resolvers: /// /// Null resolver → DatFont unchanged (backward-compat: same as before Fix C). /// Resolver present, FontDid == 0 → resolver NOT called; DatFont == global datFont. /// Resolver returns null for an id (font missing) → DatFont falls back to global datFont. /// Controller sets DatFont after build → controller value wins. /// threads fontResolve to the factory. /// /// /// public class DatWidgetFactoryFontResolveTests { private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: null fontResolve → DatFont == datFont (backward-compat) ───── /// /// When fontResolve is null (the live GameWindow path), BuildText must NOT /// touch DatFont beyond setting it to the global datFont. Null in → null out. /// [Fact] public void NullFontResolve_DatFont_EqualsGlobalFont() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = 0x40000001u }; // datFont=null, fontResolve=null → backward-compat: DatFont == null var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null, fontResolve: null)); Assert.Null(t.DatFont); } // ── Test 2: FontDid == 0 → resolver NOT called ──────────────────────────── /// /// When the element has FontDid == 0, the fontResolve delegate must NOT be /// invoked (the element has no dat font; fall through to global datFont). /// [Fact] public void FontDidZero_ResolverNotCalled() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = 0u }; bool called = false; UiDatFont? Resolver(uint id) { called = true; return null; } // FontDid=0 → resolver should never fire, even though one is provided. Assert.IsType(DatWidgetFactory.Create(info, NoTex, null, fontResolve: Resolver)); Assert.False(called, "fontResolve must NOT be called when FontDid == 0"); } // ── Test 3: resolver returns null (font missing) → fallback to datFont ─── /// /// When the element has a non-zero FontDid but the resolver returns null /// (font not found in dats), DatFont must fall back to the shared global datFont. /// Since datFont is null in unit tests, this verifies the fallback chain. /// [Fact] public void ResolverReturnsNull_FallsBackToGlobalFont() { const uint SomeFontDid = 0x40000005u; var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = SomeFontDid }; int callCount = 0; UiDatFont? Resolver(uint id) { callCount++; return null; } // always returns null (font missing) var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver)); // Resolver was called exactly once for this element. Assert.Equal(1, callCount); // Fallback: DatFont == global datFont (null in unit tests). Assert.Null(t.DatFont); } // ── Test 4: resolver called with correct FontDid ────────────────────────── /// /// When fontResolve is provided and FontDid is non-zero, the resolver is called /// with the element's exact FontDid value. /// [Fact] public void ResolverCalledWithElementFontDid() { const uint ExpectedFontDid = 0x40000002u; var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = ExpectedFontDid }; uint? capturedId = null; UiDatFont? Resolver(uint id) { capturedId = id; return null; } DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver); Assert.Equal(ExpectedFontDid, capturedId); } // ── Test 5: controller DatFont override wins over build-time value ─────── /// /// A controller that calls FindElement and sets DatFont afterward MUST override /// whatever the factory set at build time. This is the backward-compat guarantee. /// The DatFont property is settable — a controller can override it after build. /// [Fact] public void ControllerDatFontOverride_WinsOverBuildTimeFont() { const uint SomeFontDid = 0x40000003u; var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = SomeFontDid }; UiDatFont? Resolver(uint _) => null; // returns null → build-time DatFont == null var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver)); Assert.Null(t.DatFont); // build-time value set by factory // Controller simulated override: set DatFont to null explicitly. // The real guarantee is that DatFont is a settable property. // (We can't construct a real UiDatFont here — it requires GL + dats.) t.DatFont = null; // controller overrides after build Assert.Null(t.DatFont); // ✓ DatFont is settable — controller override mechanism is in place. } // ── Test 6: LayoutImporter.Build threads fontResolve to factory ────────── /// /// When is given a fontResolve, it must pass /// it to for EVERY element in the tree. /// Verified by checking that a Type-12 child with FontDid triggers the resolver. /// [Fact] public void LayoutImporter_Build_ThreadsFontResolve_ToFactory() { const uint FontDid = 0x40000004u; var root = new ElementInfo { Id = 1u, Type = 3, Width = 200, Height = 100 }; var child = new ElementInfo { Id = 2u, Type = 12, Width = 100, Height = 20, FontDid = FontDid }; root.Children.Add(child); int resolveCallCount = 0; UiDatFont? Resolver(uint id) { resolveCallCount++; return null; } var layout = LayoutImporter.Build(root, NoTex, datFont: null, fontResolve: Resolver); // The child element has a non-zero FontDid — resolver must have been called for it. Assert.True(resolveCallCount > 0, "fontResolve was not called for the child element"); // The child widget was built (findable in the layout). Assert.NotNull(layout.FindElement(2u)); } // ── Test 7: Meter element also receives element font from resolver ──────── /// /// Type-7 (UiMeter) elements with a FontDid must also go through the resolver. /// Verified by checking the resolver fires for a meter element with a non-zero FontDid. /// [Fact] public void FontResolve_CalledForMeter_WhenFontDidPresent() { const uint MeterFontDid = 0x40000006u; var meter = new ElementInfo { Type = 7, Width = 150, Height = 16, FontDid = MeterFontDid }; int callCount = 0; UiDatFont? Resolver(uint id) { callCount++; return null; } var w = DatWidgetFactory.Create(meter, NoTex, datFont: null, fontResolve: Resolver); var m = Assert.IsType(w); // Resolver was called for the meter element's FontDid. Assert.True(callCount > 0, "fontResolve was not called for meter element"); // Meter DatFont: resolver returned null, so DatFont falls back to global datFont (null). Assert.Null(m.DatFont); } // ── Test 8: LayoutImporter.BuildFromInfos signature is backward-compat ──── /// /// without fontResolve (default null) /// must still work for all callers that don't pass it — verifies the optional /// parameter default is correct and no existing callers broke. /// [Fact] public void BuildFromInfos_WithoutFontResolve_WorksAsBeforeFix() { var root = new ElementInfo { Id = 10u, Type = 3, Width = 200, Height = 100 }; var child = new ElementInfo { Id = 11u, Type = 12, Width = 100, Height = 20, FontDid = 0x40000001u }; // Call without fontResolve (the old signature shape). var layout = LayoutImporter.BuildFromInfos(root, new[] { child }, NoTex, datFont: null); var t = Assert.IsType(layout.FindElement(11u)); // No resolver → DatFont == global datFont (null in unit tests). Assert.Null(t.DatFont); } }