Files
LlamaCasty/ytLive.Tests/BorderColorPickerTests.cs
T
gramps 987fddac24 TASK 63: the border-colour picker IS the app's own drop-down
The creator (2026-10-21), on Live -> Layers -> webcam -> Properties:

  "why does the drop down under the web cam properties not show the same style for
   the border color drop down list box?"

  "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"

WHY IT WAS MISSED - the answer the creator is owed.
The TASK 58 sweep had already walked the whole app for un-themed controls and the
border picker STILL was not themed, because the sweep looked for control TYPES
(ComboBox / CheckBox / TextBox) carrying no theme style - and the border picker was
NONE of those. It was a "..." Button whose Click handler built a ContextMenu of
swatches in code. It was never in the sweep's scope, and PropertiesStyleTests.cs
(a type-based guard) could not see it either. A picker that LOOKS like a drop-down
is invisible to a type-based audit; the guard has to ask "is this the app's widget?".

WHAT CHANGED
- The picker is now the app's YtComboBox - the widget the Display and Font pickers
  wear - bound to the palette (SelectedElement.BorderColorOptions) with its selection
  bound by VALUE (SelectedItem -> BorderColorEntry), because WPF's SelectedValue
  matching is ordinal and the palette mixes #FFFFFF and #e94560.
- The closed box shows the swatch + the colour's NAME; the rows show swatch + hex +
  name (the rows the creator approved in TASK 60). A ComboBox cannot do that with one
  ItemTemplate - WPF copies it into the read-only SelectionBoxItemTemplate - so
  Helpers/ClosedBoxTemplateSelector.cs picks by the ContentPresenter's templated
  parent: the ComboBox for the closed box, the ComboBoxItem for a row. Verified
  against WPF with a throwaway probe, not assumed.
- The swatch is painted by the THEME (Helpers/HexToBrushConverter.cs, registered as
  HexToBrush) - the hand-painted SolidColorBrush(Color.FromRgb(...)) in the old
  click handler was the exact anti-pattern the theme forbids. The swatch also carries
  a 1px #2a3a5e border now, so black and dark swatches are visible on the dark menu
  (the follow-up TASK 60 left open).
- The hex field stays (60px, editable): a colour outside the palette can still be
  typed, and the drop-down then carries that hex as a NAMELESS entry ("no name" is the
  honest answer). The palette's name moved out of TASK 62's TextBlock into the
  drop-down's closed box.
- The hand-built menu, its ContextMenu/MenuItem/SolidColorBrush code and
  WebcamBorderColorPicker_Click are gone.

MODEL: SceneElement.BorderColorName -> BorderColorEntry (the palette entry for the
current hex, with its name; setting it writes the hex back) + BorderColorOptions (the
palette plus the custom entry), both raised when BorderColor changes.
BorderColorPalette gains EntryFor(hex) and OptionsFor(hex); NameOf delegates.

Guards: ytLive.Tests/BorderColorPickerTests.cs (the ONE test for this change - the
model, the panel, and the REAL widget: with the app's theme style and the panel's own
templates the closed box names the colour and the rows show the hex AND the name).
The TASK 60 guard (BorderColorPaletteTests) was updated because the mechanism it
asserted - a code-built menu - no longer exists; it now guards the new wiring (the
drop-down is bound to the palette and neither the panel nor the code-behind carries a
second list of colours).

Full suite: 435/435 passed, 0 warnings.
2026-10-21 23:09:11 -07:00

152 lines
7.6 KiB
C#

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;
/// <summary>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."*
///
/// <para>The border colour was a hand-rolled <c>...</c> button that built a
/// <c>ContextMenu</c> 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:</para>
///
/// <para><b>The model</b> — 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.</para>
///
/// <para><b>The panel</b> — the picker IS the app's own <c>YtComboBox</c>, 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.</para>
///
/// <para><b>The widget</b> — 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 <c>ItemTemplate</c> into the read-only <c>SelectionBoxItemTemplate</c>, so the
/// two can only differ through <see cref="ClosedBoxTemplateSelector"/>.)</para></summary>
[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<string?>();
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 picker = xaml.Descendants()
.Where(e => e.Name.LocalName == "ComboBox")
.SingleOrDefault(c => (string?)c.Attribute("ItemsSource")
== "{Binding SelectedElement.BorderColorOptions}");
Assert.True(picker != null, "the border colour is not a drop-down over the palette");
Assert.True(((string?)picker!.Attribute("Style") ?? string.Empty)
.Contains("YtComboBox", StringComparison.Ordinal),
"the 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();
});
}
/// <summary>Every text in a visual tree, so a template's content can be read back.</summary>
private static string Texts(DependencyObject root)
{
var texts = new List<string>();
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);
}
}