using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Threading; using System.Xml.Linq; using Xunit; using ytLive.Controls; using ytLive.Helpers; using ytLive.Models; namespace ytLive.Tests; /// TASK 63 — the creator (2026-10-21): *"why does the drop down under the web cam /// properties not show the same style for the border color drop down list box?"* — and the /// reason behind it: *"one of the first things we did was define branding. We established /// look-and-feel standards. And yet you keep ignoring these and I'm wondering why."* /// /// The border colour was a hand-rolled ... button that built a /// ContextMenu in code — never a ComboBox, so the TASK 58 sweep (which only covered /// ComboBox/CheckBox/TextBox) never reached it. This guard holds the fix in place, in three /// parts: /// /// The model — the border colour is a palette entry the drop-down can select by /// VALUE (carrying the colour's name), and a custom hex the creator typed is still offered. /// /// The panel — the picker IS the app's own YtComboBox, the widget every /// other drop-down wears, and the hand-rolled menu is gone from both the XAML and the /// code-behind. The hex field stays, so a colour outside the palette can still be typed. /// /// The widget — with the app's real theme style and the panel's real templates, /// the CLOSED box shows the colour's name and the drop-down ROWS show the hex AND the name. /// (WPF copies ItemTemplate into the read-only SelectionBoxItemTemplate, so the /// two can only differ through .) [Collection("RealApp")] public sealed class BorderColorPickerTests { private readonly RealAppHost _app; public BorderColorPickerTests(RealAppHost app) => _app = app; [Fact] public void The_border_colour_picker_is_the_app_drop_down() { // ── 1. the model: the palette entry, and the custom hex ────────────────── var element = new WebcamSceneConfig { Name = "Webcam" }; var raised = new List(); element.PropertyChanged += (_, e) => raised.Add(e.PropertyName); element.BorderColor = "#e94560"; Assert.Equal("Crimson", element.BorderColorEntry.Name); Assert.Equal("#e94560", element.BorderColorEntry.Hex); Assert.Contains("BorderColorEntry", raised); Assert.Contains("BorderColorOptions", raised); element.BorderColorEntry = BorderColorPalette.All[0]; // picking a row Assert.Equal("#FFFFFF", element.BorderColor); element.BorderColor = "#123456"; // a typed hex Assert.Equal(string.Empty, element.BorderColorEntry.Name); // no name to show Assert.Equal("#123456", element.BorderColorEntry.Hex); Assert.Contains(element.BorderColorOptions, o => o.Hex == "#123456"); element.BorderColor = string.Empty; // no colour yet Assert.Equal(BorderColorPalette.All.Count, element.BorderColorOptions.Count); Assert.DoesNotContain(element.BorderColorOptions, o => string.IsNullOrEmpty(o.Hex)); // ── 2. the panel: the app's drop-down, no hand-rolled menu ─────────────── var xaml = XDocument.Load(SourceAssert.RepoPath("Controls", "LeftPanel.xaml")); var pickers = xaml.Descendants() .Where(e => e.Name.LocalName == "ComboBox") .Where(c => (string?)c.Attribute("ItemsSource") == "{Binding SelectedElement.BorderColorOptions}") .ToArray(); Assert.NotEmpty(pickers); foreach (var picker in pickers) { Assert.True(((string?)picker.Attribute("Style") ?? string.Empty) .Contains("YtComboBox", StringComparison.Ordinal), "a border-colour drop-down is not styled with YtComboBox"); Assert.Equal("{Binding SelectedElement.BorderColorEntry, Mode=TwoWay}", (string?)picker.Attribute("SelectedItem")); } var hexField = xaml.Descendants() .Where(e => e.Name.LocalName == "TextBox") .Any(t => (string?)t.Attribute("Text") == "{Binding SelectedElement.BorderColor, UpdateSourceTrigger=PropertyChanged}"); Assert.True(hexField, "the hex field is gone — a colour outside the palette can no longer be typed"); var panelSource = SourceAssert.Text("Controls", "LeftPanel.xaml"); var codeBehind = SourceAssert.Text("Controls", "LeftPanel.xaml.cs"); Assert.False(panelSource.Contains("WebcamBorderColorPicker_Click", StringComparison.Ordinal), "the hand-rolled colour menu is back in the panel"); Assert.False(codeBehind.Contains("WebcamBorderColorPicker_Click", StringComparison.Ordinal), "the hand-rolled colour menu is back in the code-behind"); Assert.False(codeBehind.Contains("ColorConverter.ConvertFromString", StringComparison.Ordinal), "a colour is hand-painted in code — the theme paints it"); // ── 3. the widget: the closed box names the colour, the rows show hex + name ── element.BorderColor = "#FFFFFF"; _app.Run(() => { var panel = new LeftPanel(); var selector = panel.Resources["BorderColorTemplates"] as ClosedBoxTemplateSelector; Assert.True(selector != null, "the panel carries no colour templates"); var combo = new ComboBox { Style = (Style)Application.Current.Resources["YtComboBox"], ItemTemplateSelector = selector, ItemsSource = element.BorderColorOptions, SelectedItem = element.BorderColorEntry, }; var window = new Window { Width = 420, Height = 240, Content = combo }; window.Show(); combo.UpdateLayout(); var closed = Texts(combo); Assert.True(closed.Contains("White", StringComparison.Ordinal), $"the closed box does not name the colour: '{closed}'"); Assert.False(closed.Contains("#FFFFFF", StringComparison.Ordinal), $"the closed box repeats the hex the field already shows: '{closed}'"); combo.IsDropDownOpen = true; combo.UpdateLayout(); Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); var popup = combo.Template.FindName("PART_Popup", combo) as Popup; var rows = popup?.Child == null ? string.Empty : Texts(popup.Child); Assert.True(rows.Contains("#FFFFFF", StringComparison.Ordinal), $"the drop-down rows do not show the hex: '{rows}'"); Assert.True(rows.Contains("White", StringComparison.Ordinal), $"the drop-down rows do not show the colour's name: '{rows}'"); window.Close(); // ── 4. clicking a row inside the open popup stays inside LeftPanel ─────── // MainWindow_PreviewMouseLeftButtonDown deselects SelectedElement (exiting // the properties listing) whenever LeftPanel.IsClickInside(e.OriginalSource) // returns false. A ComboBox's PART_Popup lives in its own PopupRoot visual // tree whose VisualTreeHelper.GetParent is null — the only link back to the // ComboBox and ElementsScrollViewer is PopupRoot's LOGICAL parent. panel.DataContext = new PanelContext { SelectedElement = element }; var hostWindow = new Window { Width = 320, Height = 720, Content = panel }; hostWindow.Show(); panel.UpdateLayout(); Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); var realCombo = FindVisualChildren(panel) .Single(c => c.IsVisible && c.ItemsSource is IEnumerable opts && opts.Any(o => o.Hex == "#e94560")); realCombo.IsDropDownOpen = true; realCombo.UpdateLayout(); Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); var crimsonEntry = BorderColorPalette.EntryFor("#e94560"); var crimsonItem = realCombo.ItemContainerGenerator.ContainerFromItem(crimsonEntry) as ComboBoxItem; Assert.NotNull(crimsonItem); // The click target inside the popup row (e.g. a TextBlock inside the item) // must be recognised as inside LeftPanel, or MainWindow clears SelectedElement. var clickTarget = FindVisualChildren(crimsonItem!).FirstOrDefault() ?? (DependencyObject)crimsonItem!; Assert.True(panel.IsClickInside(clickTarget), "clicking a colour row in the open ComboBox popup is treated as a click outside LeftPanel (exits properties)"); // Clicking the row in the open popup updates the webcam's BorderColor and closes the popup. crimsonItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) { RoutedEvent = UIElement.MouseLeftButtonDownEvent, Source = clickTarget, }); crimsonItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) { RoutedEvent = UIElement.MouseLeftButtonUpEvent, Source = clickTarget, }); realCombo.UpdateLayout(); Assert.Equal("#e94560", element.BorderColor); Assert.Equal(crimsonEntry, realCombo.SelectedItem); Assert.Contains("Crimson", Texts(realCombo)); Assert.False(realCombo.IsDropDownOpen); // And when starting from a typed custom hex (#123456), picking a palette // row in the open popup still lands cleanly on that palette colour. element.BorderColor = "#123456"; realCombo.IsDropDownOpen = true; realCombo.UpdateLayout(); Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded); var emeraldEntry = BorderColorPalette.EntryFor("#22c55e"); var emeraldItem = realCombo.ItemContainerGenerator.ContainerFromItem(emeraldEntry) as ComboBoxItem; Assert.NotNull(emeraldItem); var emeraldTarget = FindVisualChildren(emeraldItem!).FirstOrDefault() ?? (DependencyObject)emeraldItem!; Assert.True(panel.IsClickInside(emeraldTarget)); emeraldItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) { RoutedEvent = UIElement.MouseLeftButtonDownEvent, Source = emeraldTarget, }); emeraldItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs( System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left) { RoutedEvent = UIElement.MouseLeftButtonUpEvent, Source = emeraldTarget, }); realCombo.UpdateLayout(); Assert.Equal("#22c55e", element.BorderColor); Assert.Equal(emeraldEntry, realCombo.SelectedItem); Assert.Contains("Emerald", Texts(realCombo)); hostWindow.Close(); }); } private sealed class PanelContext { public SceneElement? SelectedElement { get; set; } public bool IsWebcamSelected => true; public bool CanToggleClipShape => true; } private static IEnumerable FindVisualChildren(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) yield return match; foreach (var nested in FindVisualChildren(child)) yield return nested; } } /// Every text in a visual tree, so a template's content can be read back. private static string Texts(DependencyObject root) { var texts = new List(); void Walk(DependencyObject node) { if (node is TextBlock block) texts.Add(block.Text); for (var i = 0; i < VisualTreeHelper.GetChildrenCount(node); i++) Walk(VisualTreeHelper.GetChild(node, i)); } Walk(root); return string.Join("|", texts); } }