fix(reorder): save layer list AND preview layout immediately at drop; recording save dialog +10% (276→304)
Reorder: OnSceneElementsReordered now calls SaveLayoutNow() (was debounced ScheduleSave) so a drag-drop persists instantly — one write stores the new layer list (Source.SortOrder) and each layer's preview geometry (X/Y/W/H) in the same rows. Reference: WPF ListBox drag-reorder requires an explicit persist at drop; a debounce window lets a crash lose the drop. Recording save dialog (RenameRecordingDialog.xaml): height 276→304 so the Cancel/Save button row is no longer obscured; Save button named SaveRecordingButton so the sizing test can assert it sits inside the client area. Tests: deterministic seam test now asserts the dragged layer's geometry (preview layout) is persisted with the new order; sizing test asserts 304 + textbox and button-row bottoms within client area. 308/308 green.
This commit is contained in:
@@ -21,7 +21,8 @@ namespace ytLive.Tests;
|
||||
/// directly (RemoveAt/Insert), bypassing SceneGraph's mutation surface, so the
|
||||
/// drop must be trapped as a data change and persisted. Real-App + temp-DB
|
||||
/// pattern (see WebcamOutputKeyTests); reuses the exact code-behind mutation and
|
||||
/// asserts the new order is written to the DB after the debounced background save.
|
||||
/// asserts the new order AND each layer's geometry (preview layout) are written
|
||||
/// to the DB — immediately at drop, no debounce window.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class LayerReorderPersistenceTests
|
||||
@@ -97,8 +98,14 @@ public sealed class LayerReorderPersistenceTests
|
||||
|
||||
Assert.True(PumpUntil(() => SourceNamesInDb(tempDb).SequenceEqual(expected),
|
||||
TimeSpan.FromSeconds(10)),
|
||||
"a real mouse drag-and-drop must be persisted by the debounced background save");
|
||||
"a real mouse drag-and-drop must be persisted by the background save");
|
||||
Assert.Equal(expected, SourceNamesInDb(tempDb));
|
||||
|
||||
// The same drop must persist the PREVIEW LAYOUT too — each layer's
|
||||
// geometry (X/Y/W/H) rides in the same Source rows as the SortOrder.
|
||||
var dragged = vm.StagedScene.Elements.OfType<Source>().Single(s => s.Name == "ImgC");
|
||||
Assert.Equal((dragged.X, dragged.Y, dragged.Width, dragged.Height),
|
||||
GeometryInDb(tempDb, "ImgC"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -173,8 +180,15 @@ public sealed class LayerReorderPersistenceTests
|
||||
|
||||
Assert.True(PumpUntil(() => SourceNamesInDb(tempDb).SequenceEqual(expected),
|
||||
TimeSpan.FromSeconds(10)),
|
||||
"the drag reorder must be persisted by the debounced background save");
|
||||
"the drag reorder must be persisted at drop (immediate save)");
|
||||
Assert.Equal(expected, SourceNamesInDb(tempDb));
|
||||
|
||||
// The drop must persist the dragged layer's PREVIEW LAYOUT too — its
|
||||
// X/Y/W/H ride in the same Source row, so one immediate save covers
|
||||
// both the new layer list and the preview geometry.
|
||||
var draggedSource = vm.StagedScene.Elements.OfType<Source>().Single(s => s.Name == dragged.Name);
|
||||
Assert.Equal((draggedSource.X, draggedSource.Y, draggedSource.Width, draggedSource.Height),
|
||||
GeometryInDb(tempDb, dragged.Name));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -221,11 +235,31 @@ public sealed class LayerReorderPersistenceTests
|
||||
}
|
||||
catch (SqliteException)
|
||||
{
|
||||
// The debounced save may hold the write lock mid-transaction — not yet.
|
||||
// The save may hold the write lock mid-transaction — not yet.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Width, double Height) GeometryInDb(string path, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path}");
|
||||
connection.Open();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT X, Y, Width, Height FROM Source WHERE SceneId = 'scene-live' AND Name = $name;";
|
||||
cmd.Parameters.AddWithValue("$name", name);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
reader.Read();
|
||||
return (reader.GetDouble(0), reader.GetDouble(1), reader.GetDouble(2), reader.GetDouble(3));
|
||||
}
|
||||
catch (SqliteException)
|
||||
{
|
||||
// The save may hold the write lock mid-transaction — not yet.
|
||||
return (double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SeedThreeLayers(string path)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path}");
|
||||
|
||||
@@ -4,10 +4,12 @@ using Xunit;
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The Rename Recording modal (creator-reported, 2026-09-18) cut off the file-name
|
||||
/// textbox: the fixed window was too short for its stacked content. The height is
|
||||
/// bumped 20% (230 → 276); this test renders the real dialog and asserts the textbox
|
||||
/// bottom edge lands inside the client area instead of clipping.
|
||||
/// The Rename Recording modal (the creator-reported dialog shown when a recording
|
||||
/// is ended, so it can be saved under a readable name) cut off its file-name textbox
|
||||
/// — and once the height was bumped 20% the Cancel/Save button row was revealed as
|
||||
/// previously obscured too. Height raised 230 → 304 (+20% then another +10%); this
|
||||
/// test renders the real dialog and asserts the textbox bottom edge AND the button
|
||||
/// row sit inside the client area instead of clipping.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class RenameRecordingDialogSizingTests
|
||||
@@ -17,7 +19,7 @@ public sealed class RenameRecordingDialogSizingTests
|
||||
public RenameRecordingDialogSizingTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void DialogHeight_Bumps20Percent_AndNameBoxIsNotClipped()
|
||||
public void DialogHeight_CoversTextboxAndButtonRow_WithoutClipping()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
@@ -27,7 +29,7 @@ public sealed class RenameRecordingDialogSizingTests
|
||||
var dialog = new RenameRecordingDialog("rec-2026-09-18");
|
||||
try
|
||||
{
|
||||
Assert.Equal(276, dialog.Height);
|
||||
Assert.Equal(304, dialog.Height);
|
||||
|
||||
dialog.Show();
|
||||
dialog.UpdateLayout();
|
||||
@@ -35,11 +37,18 @@ public sealed class RenameRecordingDialogSizingTests
|
||||
var box = dialog.NameBox;
|
||||
Assert.True(box.ActualWidth > 0, "file-name textbox must have a real layout");
|
||||
var client = (FrameworkElement)dialog.Content;
|
||||
var bottom = box.TransformToAncestor(client)
|
||||
var boxBottom = box.TransformToAncestor(client)
|
||||
.TransformBounds(new Rect(box.RenderSize)).Bottom;
|
||||
Assert.True(bottom <= client.ActualHeight,
|
||||
$"textbox bottom ({bottom:0.##}) must sit inside the client area " +
|
||||
Assert.True(boxBottom <= client.ActualHeight,
|
||||
$"textbox bottom ({boxBottom:0.##}) must sit inside the client area " +
|
||||
$"({client.ActualHeight:0.##}) — the dialog is clipping the name box");
|
||||
|
||||
var save = (FrameworkElement)client.FindName("SaveRecordingButton")!;
|
||||
var saveBottom = save.TransformToAncestor(client)
|
||||
.TransformBounds(new Rect(save.RenderSize)).Bottom;
|
||||
Assert.True(saveBottom <= client.ActualHeight,
|
||||
$"Save button bottom ({saveBottom:0.##}) must sit inside the client area " +
|
||||
$"({client.ActualHeight:0.##}) — the button row is obscured");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user