diff --git a/ViewModels/MainViewModel.Socials.cs b/ViewModels/MainViewModel.Socials.cs
index b713db3..5a9955c 100644
--- a/ViewModels/MainViewModel.Socials.cs
+++ b/ViewModels/MainViewModel.Socials.cs
@@ -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)
diff --git a/ytLive.Tests/SocialsPreviewTests.cs b/ytLive.Tests/SocialsPreviewTests.cs
new file mode 100644
index 0000000..2802293
--- /dev/null
+++ b/ytLive.Tests/SocialsPreviewTests.cs
@@ -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;
+
+/// 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.
+///
+/// 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.
+[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(), 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(bar)!;
+ var label = FindChild(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(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(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(child);
+ if (deeper != null) return deeper;
+ }
+ return null;
+ }
+}