test(socials): a real preview test — and the Cancel bug it immediately found

The two regressions that reached the creator both lived in preview XAML that no test loads: the
appearance controls painted nothing, then the bar vanished when the plate was off. Every existing
test was green through both.

This test drives the real drawer on a real MainWindow (hermetic temp DB, per the layout-pollution
rule) and reads the real preview elements:
- the bar is VISIBLE, whatever the plate switch says
- a drawer text colour reaches the glyph
- the plate carries its colour and ~50% alpha, and its 18,6 padding only when enabled
- the shadow is an actual DropShadowEffect on the glyph, and off by default
- Cancel restores the pre-edit look

It paid for itself on the first run: Cancel did NOT revert. The snapshot held the same config
object the live preview mutates, so "restore" restored the edited object. The snapshot is now the
six values, written back on cancel — entries untouched.

Also fixes the harness details that made the first attempts fail: everything runs on the app's STA
thread (window creation AND close), and the exception is rethrown with its stack so a failure is
diagnosable next time.
This commit is contained in:
2026-10-30 10:02:06 -07:00
parent 0822552830
commit e89cc3682a
2 changed files with 140 additions and 5 deletions
+18 -5
View File
@@ -149,7 +149,11 @@ public partial class MainViewModel
_socials);
// Live preview: the drawer edits the REAL config, so colour/opacity/shadow land on the
// preview immediately instead of waiting for Save. Cancel restores the snapshot below.
_socialsBeforeEdit = _socials;
// Snapshot the VALUES, not the object: the drawer edits the live config in place for the
// preview, so holding the reference would "restore" the very object that was mutated.
_appearanceBeforeEdit = _socials is null ? null : (
_socials.BarEnabled, _socials.BarTextColor, _socials.BarBackgroundEnabled,
_socials.BarBackgroundColor, _socials.BarBackgroundOpacity, _socials.BarTextShadow);
dialog.PropertyChanged += (_, e) =>
{
if (e.PropertyName is null) return;
@@ -164,9 +168,17 @@ public partial class MainViewModel
};
dialog.CancelRequested += () =>
{
_socials = _socialsBeforeEdit;
NotifySocialsChanged();
OnPropertyChanged(nameof(SocialBarVisible));
if (_socials is { } live && _appearanceBeforeEdit is { } before)
{
live.BarEnabled = before.BarEnabled;
live.BarTextColor = before.TextColor;
live.BarBackgroundEnabled = before.BackgroundEnabled;
live.BarBackgroundColor = before.BackgroundColor;
live.BarBackgroundOpacity = before.BackgroundOpacity;
live.BarTextShadow = before.TextShadow;
NotifySocialsChanged();
OnPropertyChanged(nameof(SocialBarVisible));
}
CloseSocialsDrawer();
};
SocialsDrawer = dialog;
@@ -178,7 +190,8 @@ public partial class MainViewModel
OnPropertyChanged(nameof(SocialsDrawerOpen));
}
private SocialsConfig? _socialsBeforeEdit;
private (bool BarEnabled, string TextColor, bool BackgroundEnabled, string BackgroundColor,
double BackgroundOpacity, bool TextShadow)? _appearanceBeforeEdit;
private static bool IsAppearanceProperty(string name) => name is
nameof(SocialsDialogViewModel.BarEnabled) or nameof(SocialsDialogViewModel.BarTextColor)
+122
View File
@@ -0,0 +1,122 @@
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
using Microsoft.Data.Sqlite;
using Xunit;
using Xunit.Sdk;
using ytLive.Controls;
using ytLive.Models;
using ytLive.Services;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>The hole that let two regressions reach the creator on 2026-10-30: the Socials drawer's
/// appearance controls were tested against the RENDERER, while the preview is its own XAML tree —
/// so a control could paint nothing, or hide the bar entirely, with every test green.
///
/// <para>This drives the real drawer on a real MainWindow and reads the real preview elements:
/// the bar stays visible when the plate is off, the text colour reaches the glyphs, the plate
/// carries its colour and opacity, the shadow switches on, and Cancel restores the pre-edit
/// look.</para></summary>
[Collection("RealApp")]
public class SocialsPreviewTests
{
private readonly RealAppHost _app;
public SocialsPreviewTests(RealAppHost app) => _app = app;
[Fact]
public void The_preview_bar_follows_the_drawer_controls() => _app.Run(Run);
private static void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-socialspreview-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
MainWindow? window = null;
try
{
var seeded = new SocialsConfig { BarEnabled = true };
seeded.Entries.Add(new SocialEntry
{
Service = SocialService.YouTube, Handle = "creator",
ProfileUrl = "https://youtube.com/@creator",
});
using (var store = new LayoutStore(tempDb))
store.Save(Array.Empty<Scene>(), null, seeded);
SqliteConnection.ClearAllPools();
window = new MainWindow();
var vm = (MainViewModel)window.DataContext!;
window.Show();
window.UpdateLayout();
var preview = (PreviewPane)window.FindName("PreviewPane")!;
var bar = (FrameworkElement)preview.FindName("SocialBarElement")!;
var plate = FindChild<Border>(bar)!;
var label = FindChild<TextBlock>(bar)!;
Assert.Equal(Visibility.Visible, bar.Visibility);
// 1. Drawer edits land on the preview live — no Save needed.
vm.OpenSocialDialogCommand.Execute(null);
var drawer = vm.SocialsDrawer!;
drawer.BarTextColor = "#FF0000";
window.UpdateLayout();
Assert.Equal(Colors.Red, ((SolidColorBrush)label.Foreground).Color);
// 2. Plate off (the default) = transparent plate, NOT a hidden bar.
Assert.Equal(0, ((SolidColorBrush)plate.Background).Color.A);
Assert.Equal(Visibility.Visible, bar.Visibility);
drawer.BarBackgroundEnabled = true;
drawer.BarBackgroundColor = "#0000FF";
drawer.BarBackgroundOpacity = 0.5;
window.UpdateLayout();
var bg = ((SolidColorBrush)plate.Background).Color;
Assert.Equal(Colors.Blue, Color.FromRgb(bg.R, bg.G, bg.B));
Assert.InRange(bg.A, 120, 135); // 50% of 255
Assert.Equal(new Thickness(18, 6, 18, 6), plate.Padding);
// 3. The shadow is a real effect on the glyph, and off by default.
Assert.Null(label.Effect);
drawer.BarTextShadow = true;
window.UpdateLayout();
Assert.IsType<DropShadowEffect>(label.Effect);
// 4. Cancel restores the pre-edit look (white text, no plate).
drawer.CancelCommand.Execute(null);
window.UpdateLayout();
Assert.Equal(Colors.White, ((SolidColorBrush)label.Foreground).Color);
Assert.Equal(0, ((SolidColorBrush)plate.Background).Color.A);
}
catch (Exception ex)
{
throw new XunitException(ex.ToString()); // the host otherwise hides the real stack
}
finally
{
window?.Close();
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort */ }
}
}
private static T? FindChild<T>(DependencyObject root) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T match) return match;
var deeper = FindChild<T>(child);
if (deeper != null) return deeper;
}
return null;
}
}