using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows.Threading; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; /// /// Layer-list drag-to-reorder: the code-behind reorders StagedScene.Elements /// 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. /// [Collection("RealApp")] public sealed class LayerReorderPersistenceTests { private readonly RealAppHost _app; public LayerReorderPersistenceTests(RealAppHost app) => _app = app; [Fact] public void DragReorder_SurvivesTheDebouncedBackgroundSave() { _app.Run(Run); } private void Run() { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-reorder-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; var window = default(MainWindow); try { using (var schema = new LayoutStore(tempDb)) { } SqliteConnection.ClearAllPools(); SeedThreeLayers(tempDb); window = new MainWindow(); var vm = (MainViewModel)window.DataContext; vm.StagedScene = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live)); Assert.True(vm.StagedScene!.Elements.OfType().Count() >= 4, "3 seeded layers + the healed Background must load"); // Mirrors Controls/LeftPanel.List_PreviewMouseMove: drag the last layer // to the front of the user layers (the healed Background stays pinned // at index 0, so index 1 is the first user layer). var elements = vm.StagedScene.Elements; var dragged = elements[^1]; elements.RemoveAt(elements.Count - 1); elements.Insert(1, dragged); var expected = vm.StagedScene.Elements.OfType().Select(s => s.Name).ToArray(); vm.OnSceneElementsReordered(); Assert.True(PumpUntil(() => SourceNamesInDb(tempDb).SequenceEqual(expected), TimeSpan.FromSeconds(10)), "the drag reorder must be persisted by the debounced background save"); Assert.Equal(expected, SourceNamesInDb(tempDb)); } finally { if (window != null) window.Close(); MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } } } /// Pumps the app dispatcher (so the VM's debounce timer fires) until /// or the deadline passes. True when the condition /// became true, false on deadline. private static bool PumpUntil(Func isDone, TimeSpan timeout) { var frame = new DispatcherFrame(); var poll = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; var deadline = DateTime.UtcNow + timeout; poll.Tick += (_, _) => { if (isDone() || DateTime.UtcNow >= deadline) { poll.Stop(); frame.Continue = false; } }; poll.Start(); Dispatcher.PushFrame(frame); return isDone(); } private static string[] SourceNamesInDb(string path) { try { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT Name FROM Source WHERE SceneId = 'scene-live' ORDER BY SortOrder;"; using var reader = cmd.ExecuteReader(); var names = new List(); while (reader.Read()) names.Add(reader.GetString(0)); return names.ToArray(); } catch (SqliteException) { // The debounced save may hold the write lock mid-transaction — not yet. return []; } } private static void SeedThreeLayers(string path) { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); var scenes = new (string Id, string Name)[] { ("scene-starting", "Starting"), ("scene-live", "Live"), ("scene-brb", "BRB"), ("scene-chat", "Chat"), ("scene-ending", "Ending"), }; foreach (var (id, name) in scenes) { using var cmd = connection.CreateCommand(); cmd.CommandText = "INSERT INTO Scene (Id, Name, HasBackground, SortOrder) VALUES ($id, $name, 1, $order);"; cmd.Parameters.AddWithValue("$id", id); cmd.Parameters.AddWithValue("$name", name); cmd.Parameters.AddWithValue("$order", Array.IndexOf(scenes, (id, name))); cmd.ExecuteNonQuery(); } var layers = new[] { "ImgA", "ImgB", "ImgC" }; using (var source = connection.CreateCommand()) { source.CommandText = @"INSERT INTO Source (Id, SceneId, Type, Name, IsEnabled, X, Y, Width, Height, IsBackground, SortOrder) VALUES ($id, 'scene-live', 'Image', $name, 1, 0, 0, 640, 480, 0, $sort);"; var idP = source.Parameters.Add("$id", SqliteType.Text); var nameP = source.Parameters.Add("$name", SqliteType.Text); var sortP = source.Parameters.Add("$sort", SqliteType.Integer); for (var i = 0; i < layers.Length; i++) { idP.Value = $"img-{i}"; nameP.Value = layers[i]; sortP.Value = i; source.ExecuteNonQuery(); } } } }