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:
2026-09-18 10:20:25 -07:00
parent 2ac00db0da
commit f91bf8715d
6 changed files with 107 additions and 30 deletions
+38 -4
View File
@@ -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}");