138 lines
7.2 KiB
C#
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;
|
|
}
|
|
}
|