using System.Globalization; using System.Linq; using System.Xml.Linq; using Xunit; using ytLive.Models; namespace ytLive.Tests; /// 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." /// /// 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. 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")); var shapeTitle = doc.Descendants().Single(e => e.Name.LocalName == "TextBlock" && (string?)e.Attribute("Text") == "Shape"); Assert.Equal("SemiBold", (string?)shapeTitle.Attribute("FontWeight")); var rectOption = OptionBoundTo(doc, "IsRectShape"); var circleOption = OptionBoundTo(doc, "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. var shapeBlock = shapeTitle.Parent!; 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("IsWebcamSelected")); Assert.Equal("SemiBold", (string?)geometryTitle.Attribute("FontWeight")); 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("SemiBold", (string?)label.Attribute("FontWeight")); 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"))); } /// The shape option bound to the given model property. private static XElement OptionBoundTo(XDocument doc, string property) => doc.Descendants().Single(e => e.Name.LocalName == "ToggleButton" && ((string?)e.Attribute("IsChecked") ?? "").Contains(property)); /// The row (DockPanel) that holds the given label. private static XElement RowContaining(XElement scope, string label) => scope.Descendants().Single(e => e.Name.LocalName == "TextBlock" && (string?)e.Attribute("Text") == label).Parent!; /// The row that follows a title. private static XElement RowAfter(XElement title) { var next = title.ElementsAfterSelf().First(e => e.Name.LocalName == "DockPanel"); return next; } /// The left inset of an element's Margin, in pixels. 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; } }