using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; /// /// TASK 38: the "Capture Window…" backdrop is session-scoped — a window pin /// survives while the window enumerates, but a persisted "window:" key from a /// previous session must heal away on reload (an HWND is recycled by the OS, so /// resurrecting it could capture an unrelated window). Real-App + temp-DB + /// fake-enumerator pattern: the seam is exercised end-to-end through the VM. /// [Collection("RealApp")] public sealed class WindowBackdropIntegrationTests { [Fact] public void WindowSelection_Seam_And_SessionScoped_Heal() { _app.Run(Run); } private readonly RealAppHost _app; public WindowBackdropIntegrationTests(RealAppHost app) => _app = app; private sealed class FakeWindowEnumerator : IWindowEnumerator { private readonly IReadOnlyList _windows; public FakeWindowEnumerator(params WindowInfo[] windows) => _windows = windows; public IReadOnlyList Enumerate() => _windows; } private void Run() { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-windowbackdrop-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { using (var schema = new LayoutStore(tempDb)) { } SqliteConnection.ClearAllPools(); SeedStaleWindowPin(tempDb); // The stale pin pointed at 0x4F00D (from a previous session); today // that exact window is gone (recycled), so the enumerator only sees // an unrelated window the creator might pin instead. var liveWindow = new WindowInfo("Notepad — oops.txt", "notepad", 0xDEADBEEF, 9999); MainViewModel.WindowEnumeratorOverride = new FakeWindowEnumerator(liveWindow); try { var window = new MainWindow(); try { var vm = (MainViewModel)window.DataContext; var live = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live)); var background = live.Elements.OfType().Single(s => s.IsBackground); // Seam wired: the submenu's item source reflects the enumerator. Assert.Contains(liveWindow, vm.OpenWindows); Assert.Equal(liveWindow, vm.OpenWindows[0]); // New session = new session: the persisted window: pin healed // away (to auto-detection or to nothing) — never resurrected // onto a recycled HWND. Assert.False(background.CaptureKey?.StartsWith("window:", StringComparison.OrdinalIgnoreCase) == true, "a previous session's window pin must not survive a reload"); // Pinning an un-capturable/just-died window must never leave a dead // "window:" pin behind — the transient heal falls back through // the auto chain (fullscreen game → desktop → static art), so // the key either becomes a live capture or null (static). vm.CaptureWindowCommand.Execute(liveWindow); Assert.False(background.CaptureKey?.StartsWith("window:", StringComparison.OrdinalIgnoreCase) == true, "a pin whose window cannot be captured must fall back to auto, not dead-end"); } finally { window.Close(); } } finally { MainViewModel.WindowEnumeratorOverride = null; } } finally { MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } } } /// Seeds a Live scene whose background persisted a stale /// "window:0x..."> capture key, as if a prior session pinned a window that /// is no longer around. private static void SeedStaleWindowPin(string path) { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); using (var scene = connection.CreateCommand()) { scene.CommandText = "INSERT INTO Scene (Id, Name, HasBackground, SortOrder) VALUES ('scene-live', 'Live', 1, 0);"; scene.ExecuteNonQuery(); } using (var bg = connection.CreateCommand()) { bg.CommandText = @"INSERT INTO Source (Id, SceneId, Type, Name, IsEnabled, X, Y, Width, Height, IsBackground, CaptureKey, SortOrder) VALUES ('bg-live', 'scene-live', 'DisplayCapture', 'Background', 1, 0, 0, 1920, 1080, 1, 'window:0x4F00D', 0);"; bg.ExecuteNonQuery(); } } }