test(ui): cover resizable plugin windows through RetailWindowManager + persistence

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 15:40:31 +02:00
parent 3a3b40fab5
commit cd06f89e59
2 changed files with 116 additions and 0 deletions

View file

@ -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 =
"<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\" minw=\"200\" minh=\"150\"></panel>";
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<UiWindowLayout>(
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 =
"<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\" minw=\"200\" minh=\"150\"></panel>";
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,

View file

@ -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 =
"<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\" resizable=\"true\" minw=\"250\" minh=\"150\"></panel>";
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 = "<panel x=\"0\" y=\"0\" w=\"300\" h=\"200\"></panel>";
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; }