From cd06f89e591cdcf3fc39028ce022b8cecdc9a2ce Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 15:40:31 +0200 Subject: [PATCH] test(ui): cover resizable plugin windows through RetailWindowManager + persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the resizable="true"/minw/minh markup grammar's coverage past MarkupDocument's own parse tests to the two host seams a resizable plugin panel actually flows through, proving no host-side wiring beyond what MarkupDocument.Build already sets on the panel was needed: - RetailWindowManagerTests: a resizable="true" markup panel accepts RetailWindowManager.ResizeTo within its minw/minh floor (and clamps to it below the floor); a plain (non-resizable) markup panel refuses — Width/Height unchanged and no Resized event, exactly today's fixed-size behavior. - RetailWindowLayoutPersistenceTests: a resizable panel's dragged size round-trips through save/restore into a fresh session, and a saved size below the panel's CURRENT minw/minh floor (a legacy save, or a plugin update that raised its floor) clamps UP to the floor on restore rather than restoring the too-small legacy value. Mutation proof: reverted MarkupDocument.cs to its pre-feature state and reran the new tests — the two RetailWindowManagerTests cases failed (the old UiNineSlicePanel ctor default of Resizable=true/MinWidth=40 let the "fixed" window resize and let the "resizable" window shrink below the new floor), and the persistence floor-clamp case failed (80x60 came back instead of clamping to 200x150). The plain save/restore round-trip case passed either way — the old ctor default was already resizable, so it exercises a real but coincidentally already-covered path; kept for its own documentation value. Restoring the implementation returns 256/257 (1 pre-existing unrelated skip) on the full Markup/PluginSidePanel/RetailWindow/Anchor-filtered App suite and 9/9 on the MossTank markup-filtered suite, both green. Co-Authored-By: Claude Fable 5.1 --- .../UI/RetailWindowLayoutPersistenceTests.cs | 75 +++++++++++++++++++ .../UI/RetailWindowManagerTests.cs | 41 ++++++++++ 2 files changed, 116 insertions(+) diff --git a/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs b/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs index 848f12eba..67c0abe1f 100644 --- a/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailWindowLayoutPersistenceTests.cs @@ -424,6 +424,81 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable Assert.Equal((77f, 88f), (saved.X, saved.Y)); } + // ── 2026-09-07: a resizable plugin markup panel's resized geometry + // persists and restores exactly like any other registered window — no + // persistence-layer change was needed for this, since RetailWindowLayoutPersistence + // already clamps against the frame's own MinWidth/MinHeight/MaxWidth/ + // MaxHeight (Apply's ClampDimension) for every registered handle. ──────── + + [Fact] + public void ResizableMarkupPluginPanel_ResizedSize_RoundTripsThroughPersistence() + { + const string xml = + ""; + var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)); + var store = new SettingsStore(PathName); + var root = new UiRoot { Width = 800, Height = 600 }; + root.AddChild(panel); + RetailWindowHandle handle = root.RegisterWindow("plugin-resizable-panel", panel); + using var persistence = new RetailWindowLayoutPersistence( + root.WindowManager, store, () => "Alice", () => (800, 600)); + + handle.MoveTo(60f, 70f); + handle.ResizeTo(420f, 260f); + + UiWindowLayout saved = Assert.IsType( + store.LoadWindowLayout("Alice", "800x600", "plugin-resizable-panel", default)); + Assert.Equal((60f, 70f, 420f, 260f), (saved.X, saved.Y, saved.Width, saved.Height)); + + // Simulate a fresh session: rebuild the same markup (a fresh, un-resized + // panel) and restore — the saved 420x260 must come back, not the + // authored 300x200. + var freshPanel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)); + var freshRoot = new UiRoot { Width = 800, Height = 600 }; + freshRoot.AddChild(freshPanel); + RetailWindowHandle freshHandle = freshRoot.RegisterWindow( + "plugin-resizable-panel", freshPanel); + using var freshPersistence = new RetailWindowLayoutPersistence( + freshRoot.WindowManager, store, () => "Alice", () => (800, 600)); + + freshPersistence.RestoreAll(); + + Assert.Equal((420f, 260f), (freshPanel.Width, freshPanel.Height)); + Assert.Equal((60f, 70f), (freshPanel.Left, freshPanel.Top)); + } + + [Fact] + public void ResizableMarkupPluginPanel_RestoreClampsBelowFloorToAuthoredMin() + { + // A legacy save from before this feature's minw/minh existed (or one + // from a plugin update that raised its floor) can carry a size below + // the CURRENT authored minimum. Restore must clamp up to that floor — + // this is what RetailWindowLayoutPersistence.Apply's ClampDimension + // already does against frame.MinWidth/MinHeight for every registered + // window; it only bites here because MarkupDocument now sets those + // fields from minw/minh instead of leaving them at UiElement's + // generic 40x40 default. + const string xml = + ""; + var store = new SettingsStore(PathName); + store.SaveWindowLayout( + "Alice", + "800x600", + "plugin-resizable-floor", + new UiWindowLayout(60f, 70f, 80f, 60f, true, false, false)); + + var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)); + var root = new UiRoot { Width = 800, Height = 600 }; + root.AddChild(panel); + root.RegisterWindow("plugin-resizable-floor", panel); + using var persistence = new RetailWindowLayoutPersistence( + root.WindowManager, store, () => "Alice", () => (800, 600)); + + persistence.RestoreAll(); + + Assert.Equal((200f, 150f), (panel.Width, panel.Height)); + } + private static RetailWindowHandle Mount( UiRoot root, string name, diff --git a/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs b/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs index 9c538bc21..9a245023c 100644 --- a/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailWindowManagerTests.cs @@ -250,6 +250,47 @@ public sealed class RetailWindowManagerTests transitions); } + // ── 2026-09-07: plugin markup's resizable="true" panel through the real + // window-manager ResizeTo path (no host wiring beyond what MarkupDocument + // already sets on the panel — Resizable/ResizeX/ResizeY/MinWidth/MinHeight + // are ordinary UiElement properties RetailWindowManager.ResizeTo already + // respects for any registered window). ─────────────────────────────────── + + [Fact] + public void ResizableMarkupPluginWindow_AcceptsResizeWithinMinAndParentConstraints() + { + const string xml = + ""; + var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)); + var root = new UiRoot { Width = 800, Height = 600 }; + root.AddChild(panel); + RetailWindowHandle handle = root.RegisterWindow("plugin-resizable", panel); + + Assert.True(handle.ResizeTo(500f, 400f)); + Assert.Equal((500f, 400f), (panel.Width, panel.Height)); + + // Below the authored floor clamps to minw/minh rather than shrinking further. + Assert.True(handle.ResizeTo(50f, 50f)); + Assert.Equal((250f, 150f), (panel.Width, panel.Height)); + } + + [Fact] + public void NonResizableMarkupPluginWindow_RefusesResize() + { + const string xml = ""; + var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32)); + var root = new UiRoot { Width = 800, Height = 600 }; + root.AddChild(panel); + RetailWindowHandle handle = root.RegisterWindow("plugin-fixed", panel); + int resized = 0; + handle.Resized += _ => resized++; + + handle.ResizeTo(500f, 400f); + + Assert.Equal((300f, 200f), (panel.Width, panel.Height)); + Assert.Equal(0, resized); + } + private sealed class RecordingController : IRetainedPanelController { public int ShownCount { get; private set; }