Files
LlamaCasty/ytLive.Tests/PropertiesHierarchyTests.cs
T

138 lines
7.2 KiB
C#

using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
/// <summary>TASK 59 — the creator's properties-panel hierarchy (2026-10-21):
/// "Mirror Rect: needs a title 'Shape' above it and Rect should be selected as a default
/// if none exists … the 'drag to move' help text needs to be indented under 'Mirror Rect'
/// to show parentage … same with Geometry, Border Color, Border Thickness … Remove should
/// be indented under Background as should Blur … Strength should be indented under Blur."
///
/// <para>One test, the whole story: the shape is a CHOICE and Rect is the selected
/// default (the model), the options live under a "Shape" title with the drag hint
/// indented beneath them, every titled block owns its content one indent deeper, and the
/// strength row sits under Blur — and the TASK 57 rule still holds: the circle option is
/// not offered in the vertical Chat layout, and the model refuses it there.</para></summary>
public class PropertiesHierarchyTests
{
[Fact]
public void The_webcam_properties_show_a_Shape_title_with_Rect_selected_and_indent_their_content()
{
// ── The model: the shape is a choice, and Rect is the default ─────────
var camera = new WebcamSceneConfig { WebcamId = "webcam-1", Name = "Webcam" };
Assert.True(camera.IsRectShape);
Assert.False(camera.IsRoundShape);
camera.IsRoundShape = true;
Assert.Equal(ClipShape.Round, camera.ClipShape);
Assert.False(camera.IsRectShape);
Assert.True(camera.IsRoundShape);
camera.IsRectShape = true;
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
Assert.True(camera.IsRectShape);
Assert.False(camera.IsRoundShape);
// The vertical Chat layout locks the shape to a rect — the circle option is
// not offered there, and the model refuses it even if the UI asks.
camera.VerticalShapeLocked = true;
camera.IsRoundShape = true;
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
Assert.False(camera.IsRoundShape);
Assert.True(camera.IsRectShape);
// ── The panel: a Shape title owns the options, the hint is indented ───
var doc = XDocument.Load(SourceAssert.RepoPath("Controls", "LeftPanel.xaml"));
XNamespace xaml = "http://schemas.microsoft.com/winfx/2006/xaml";
var theme = XDocument.Load(SourceAssert.RepoPath("Themes", "Controls.xaml"));
var propertyHeaderStyle = theme.Descendants().Single(e => e.Name.LocalName == "Style"
&& (string?)e.Attribute(xaml + "Key") == "PropertyHeader");
var fontWeight = propertyHeaderStyle.Elements().Single(e => e.Name.LocalName == "Setter"
&& (string?)e.Attribute("Property") == "FontWeight");
Assert.Equal("SemiBold", (string?)fontWeight.Attribute("Value"));
var shapeTitle = doc.Descendants().Single(e => e.Name.LocalName == "TextBlock"
&& (string?)e.Attribute("Text") == "Shape");
Assert.Equal("{StaticResource PropertyHeader}", (string?)shapeTitle.Attribute("Style"));
var shapeBlock = shapeTitle.Parent!;
var rectOption = OptionBoundTo(shapeBlock, "IsRectShape");
var circleOption = OptionBoundTo(shapeBlock, "IsRoundShape");
Assert.Contains("TwoWay", (string?)rectOption.Attribute("IsChecked") ?? "");
// The CIRCLE is the option that is not offered in the vertical Chat layout.
Assert.Contains("CanToggleClipShape", (string?)circleOption.Attribute("Visibility") ?? "");
// Both options live under the Shape title.
Assert.Contains(rectOption, shapeBlock.Descendants());
Assert.Contains(circleOption, shapeBlock.Descendants());
// The drag hint is indented, and it comes after the shape row — it reads as
// part of the shape block.
var hint = doc.Descendants().Single(e => e.Name.LocalName == "TextBlock"
&& ((string?)e.Attribute("Text") ?? "").StartsWith("Drag to move"));
Assert.True(LeftIndent(hint) > 0, "the drag hint is not indented under the shape row");
var siblings = shapeBlock.Parent!.Elements().ToList();
Assert.True(siblings.IndexOf(shapeBlock) < siblings.IndexOf(hint),
"the drag hint comes before the shape row");
// ── Every titled block owns its content one indent deeper ────────────
var geometryTitle = doc.Descendants()
.Where(e => e.Name.LocalName == "TextBlock" && (string?)e.Attribute("Text") == "Geometry")
.Single(e => ((string?)e.Parent?.Attribute("Visibility") ?? "").Contains("SelectedElement.IsDraggable"));
Assert.Equal("{StaticResource PropertyHeader}", (string?)geometryTitle.Attribute("Style"));
foreach (var row in geometryTitle.Parent!.Elements().Where(e => e.Name.LocalName == "DockPanel"))
Assert.Equal(10, LeftIndent(row));
// The webcam block (the one the TASK 51 test finds by its binding).
var webcamPanel = doc.Descendants().Single(e => e.Name.LocalName == "StackPanel"
&& ((string?)e.Attribute("Visibility") ?? "").Contains("SelectedElement.IsWebcam"));
foreach (var title in new[] { "Border Color", "Border Thickness" })
{
var label = webcamPanel.Descendants().Single(e => e.Name.LocalName == "TextBlock"
&& (string?)e.Attribute("Text") == title);
Assert.Equal("{StaticResource PropertyHeader}", (string?)label.Attribute("Style"));
Assert.Equal(10, LeftIndent(RowAfter(label)));
}
// Remove and Blur are indented under Background; Strength one level deeper,
// under Blur.
Assert.Equal(10, LeftIndent(RowContaining(webcamPanel, "Remove")));
Assert.Equal(10, LeftIndent(RowContaining(webcamPanel, "Blur")));
Assert.Equal(20, LeftIndent(RowContaining(webcamPanel, "Strength")));
}
/// <summary>The shape option bound to the given model property.</summary>
private static XElement OptionBoundTo(XElement scope, string property) =>
scope.Descendants().Single(e => e.Name.LocalName == "ToggleButton"
&& ((string?)e.Attribute("IsChecked") ?? "").Contains(property));
/// <summary>The row (DockPanel) that holds the given label.</summary>
private static XElement RowContaining(XElement scope, string label) =>
scope.Descendants().Single(e => e.Name.LocalName == "TextBlock"
&& (string?)e.Attribute("Text") == label).Parent!;
/// <summary>The row that follows a title.</summary>
private static XElement RowAfter(XElement title)
{
var next = title.ElementsAfterSelf().First(e => e.Name.LocalName == "DockPanel");
return next;
}
/// <summary>The left inset of an element's Margin, in pixels.</summary>
private static int LeftIndent(XElement element)
{
var margin = (string?)element.Attribute("Margin");
if (string.IsNullOrWhiteSpace(margin)) return 0;
var first = margin.Split(',')[0].Trim();
return int.TryParse(first, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
? value
: 0;
}
}