using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive.Tests;
///
/// Reproduces the real MainWindow preview to check the round-clip corner
/// handle: whether a click at the corner actually stays inside the preview
/// (doesn't fall through and deselect the source) and whether the round clip
/// renders as a circle rather than an oval.
///
public sealed class RoundClipInteractionTests
{
[Fact]
public void Round_Clip_Corner_Is_Grabbable_And_Shape_Is_Circle()
{
Exception? failure = null;
var thread = new Thread(() =>
{
try
{
Run();
}
catch (Exception ex)
{
failure = ex;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (failure != null)
throw new Xunit.Sdk.XunitException("Round-clip interaction failed: " + failure);
}
private void Run()
{
var app = new App();
app.InitializeComponent();
// Never let the real MainWindow read/write the user's actual layout DB —
// Shutdown() saves the layout, which would persist these test sources
// over the real ones. Point it at a throwaway temp DB instead.
var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
var window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
try
{
window.Show();
window.UpdateLayout();
var scene = vm.ActiveScene!;
var webcam = new WebcamSceneConfig
{
WebcamId = "test-camera",
Name = "Webcam",
X = 1408,
Y = 778,
Width = 480,
Height = 270,
};
scene.Elements.Add(webcam);
vm.SelectedElement = webcam;
window.UpdateLayout();
var previewGrid = (Grid)window.FindName("PreviewGrid")!;
var canvasGrid = (Grid)window.FindName("CanvasGrid")!;
var toWindow = canvasGrid.TransformToVisual(window);
var corner = new Point(webcam.X + webcam.Width, webcam.Y + webcam.Height);
foreach (var shape in new[] { ClipShape.Traditional, ClipShape.Round })
{
webcam.ClipShape = shape;
window.UpdateLayout();
var cornerInWindow = toWindow.Transform(corner);
// A click on the corner must land inside the preview (descendant
// of PreviewGrid); otherwise Window_PreviewMouseLeftButtonDown
// deselects the source and the handle can never be grabbed.
var hit = VisualTreeHelper.HitTest(window, cornerInWindow);
var grabbable = hit != null && IsDescendantOf(hit.VisualHit, previewGrid);
Assert.True(grabbable,
$"{shape}: corner click fell through to '{hit?.VisualHit.GetType().Name ?? "null"}' " +
"so the source gets deselected before the resize handler runs");
}
// The round clip must render as a circle (square bounding box), not an oval.
webcam.ClipShape = ClipShape.Round;
window.UpdateLayout();
var ellipse = FindRoundEllipse(window);
Assert.NotNull(ellipse);
Assert.Equal(ellipse!.RenderSize.Width, ellipse.RenderSize.Height, 1.0);
}
finally
{
window.Close();
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { System.IO.File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
private static Ellipse? FindRoundEllipse(Window window)
{
return Walk(window, element => element is Ellipse e && e.IsVisible) as Ellipse;
}
private static DependencyObject? Walk(DependencyObject parent, Func predicate)
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (predicate(child)) return child;
var found = Walk(child, predicate);
if (found != null) return found;
}
return null;
}
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
{
while (child != null && !ReferenceEquals(child, ancestor))
child = VisualTreeHelper.GetParent(child);
return child != null;
}
}