From fab10ecd0f49fb237798df11024467f8f9badfbf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 18:57:31 +0700 Subject: [PATCH 1/6] P1 UI: canonical textbox, dropdown, editable safety, empty state and scrollbar styles (#730) --- Resources/P2BlueSteelGreige.xaml | 182 +++++++++++++++++++++++++++---- 1 file changed, 162 insertions(+), 20 deletions(-) diff --git a/Resources/P2BlueSteelGreige.xaml b/Resources/P2BlueSteelGreige.xaml index 42564a713..6b4c7328a 100644 --- a/Resources/P2BlueSteelGreige.xaml +++ b/Resources/P2BlueSteelGreige.xaml @@ -184,8 +184,8 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - From 420ca77834b2bb0b5050481136bf10999bebfe57 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 18:59:49 +0700 Subject: [PATCH 5/6] test(P1 UX): lock shared input, editable command, dropdown, empty state and task dialog contracts (#730) --- .../P1SharedComponentVisualContractsTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/ARSAS.Tests/P1SharedComponentVisualContractsTests.cs diff --git a/tests/ARSAS.Tests/P1SharedComponentVisualContractsTests.cs b/tests/ARSAS.Tests/P1SharedComponentVisualContractsTests.cs new file mode 100644 index 000000000..9641c3b12 --- /dev/null +++ b/tests/ARSAS.Tests/P1SharedComponentVisualContractsTests.cs @@ -0,0 +1,93 @@ +using System.Xml.Linq; + +namespace ARSAS.Tests; + +/// +/// Protect visual-only P1 component convergence without changing IEC 61850 bindings, +/// command editing, model scopes or the SMV measurement instrument templates. +/// +public sealed class P1SharedComponentVisualContractsTests +{ + private static readonly XNamespace Wpf = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; + private static readonly XNamespace X = "http://schemas.microsoft.com/winfx/2006/xaml"; + + [Fact] + public void P1_InputsAndDropdownsExposeOneSharedNormalFocusAndErrorContract() + { + var doc = XDocument.Parse(Read("Resources/P2BlueSteelGreige.xaml")); + var styles = doc.Descendants(Wpf + "Style").ToArray(); + XElement Named(string key) => styles.Single(e => (string?)e.Attribute(X + "Key") == key); + + var text = Named("WorkstationTextBox"); + Assert.Contains(text.Descendants(Wpf + "ScrollViewer"), + e => (string?)e.Attribute(X + "Name") == "PART_ContentHost"); + Assert.Contains(text.Descendants(Wpf + "Trigger"), + e => (string?)e.Attribute("Property") == "IsKeyboardFocusWithin"); + Assert.Contains(text.Descendants(Wpf + "Trigger"), + e => (string?)e.Attribute("Property") == "Validation.HasError"); + Assert.Contains(text.Descendants(Wpf + "Trigger"), + e => (string?)e.Attribute("Property") == "IsReadOnly"); + + var combo = Named("WorkstationComboBox"); + Assert.Contains(combo.Descendants(Wpf + "Popup"), + e => (string?)e.Attribute(X + "Name") == "PART_Popup"); + Assert.Contains(combo.Descendants(Wpf + "ItemsPresenter"), _ => true); + Assert.Contains(combo.Descendants(Wpf + "ToggleButton"), + e => ((string?)e.Attribute("IsChecked"))?.Contains("IsDropDownOpen", StringComparison.Ordinal) == true); + Assert.Contains(combo.Descendants(Wpf + "Trigger"), + e => (string?)e.Attribute("Property") == "IsEnabled"); + + Assert.Equal("{StaticResource WorkstationComboBox}", + (string?)Named("ModernComboBox").Attribute("BasedOn")); + Assert.Equal(2, styles.Count(e => e.Attribute(X + "Key") == null && + ((string?)e.Attribute("TargetType") == "TextBox" || + (string?)e.Attribute("TargetType") == "ComboBox"))); + } + + [Fact] + public void P1_EditableCommandValuePreservesFreeTextEditor() + { + var command = XDocument.Parse(Read("ControlCommandWindow.xaml")); + var editor = command.Descendants(Wpf + "ComboBox").Single(e => + (string?)e.Attribute(X + "Name") == "CommandValueBox"); + Assert.Equal("{StaticResource WorkstationEditableComboBox}", (string?)editor.Attribute("Style")); + Assert.Equal("{Binding AllowCustomValue}", (string?)editor.Attribute("IsEditable")); + Assert.Contains("UpdateSourceTrigger=PropertyChanged", (string?)editor.Attribute("Text")); + Assert.Equal("{Binding ValueOptions}", (string?)editor.Attribute("ItemsSource")); + + var theme = XDocument.Parse(Read("Resources/P2BlueSteelGreige.xaml")); + var fallback = theme.Descendants(Wpf + "Style").Single(e => + (string?)e.Attribute(X + "Key") == "WorkstationEditableComboBox"); + Assert.DoesNotContain(fallback.Descendants(Wpf + "Setter"), + e => (string?)e.Attribute("Property") == "Template"); + } + + [Fact] + public void P1_EmptyStateAndTaskDialogsReuseCanonicalCoolBlueTokens() + { + var goose = XDocument.Parse(Read("Views/GooseSubscriberView.xaml")); + Assert.Equal(2, goose.Descendants(Wpf + "Border").Count(e => + (string?)e.Attribute("Style") == "{StaticResource WorkstationEmptyState}")); + var inherited = goose.Descendants(Wpf + "Style").Single(e => + (string?)e.Attribute("BasedOn") == "{StaticResource WorkstationEmptyState}"); + Assert.Contains(inherited.Descendants(Wpf + "DataTrigger"), e => + ((string?)e.Attribute("Binding"))?.Contains("SelectedGooseStream", StringComparison.Ordinal) == true); + + var dialogs = XDocument.Parse(Read("Styles/IedTaskDialogStyles.xaml")); + string Brush(string key) => (string?)dialogs.Descendants(Wpf + "SolidColorBrush") + .Single(e => (string?)e.Attribute(X + "Key") == key).Attribute("Color") ?? ""; + Assert.Equal("{DynamicResource CoolCanvasColor}", Brush("TaskDialogCanvas")); + Assert.Equal("{DynamicResource CoolBorderColor}", Brush("TaskDialogBorder")); + Assert.Equal("{DynamicResource InkColor}", Brush("TaskDialogInk")); + } + + private static string Read(string path) + { + for (DirectoryInfo? dir = new(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) + { + var file = Path.Combine(dir.FullName, path); + if (File.Exists(file)) return File.ReadAllText(file); + } + throw new FileNotFoundException(path); + } +} From c47a622037110a09ed1e16653aac0c44b5f40de2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 19:06:13 +0700 Subject: [PATCH 6/6] test(P1 UX): resolve canonical dynamic dialog colors before WCAG contrast calculation (#730) --- .../IedTaskDialogVisualRegressionTests.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/ARSAS.Tests/IedTaskDialogVisualRegressionTests.cs b/tests/ARSAS.Tests/IedTaskDialogVisualRegressionTests.cs index 24c9ad451..0092cf454 100644 --- a/tests/ARSAS.Tests/IedTaskDialogVisualRegressionTests.cs +++ b/tests/ARSAS.Tests/IedTaskDialogVisualRegressionTests.cs @@ -105,9 +105,21 @@ public void DialogStyles_AreSelfContained_AndBothWindowsInitializeOnSta() public void SharedDialogPalette_MeetsReadableForegroundContrast() { var document = XDocument.Load(Find("Styles/IedTaskDialogStyles.xaml")); - string Color(string key) => document.Descendants(Presentation + "SolidColorBrush") - .Single(element => element.Attribute(Xaml + "Key")?.Value == key) - .Attribute("Color")!.Value; + var tokens = XDocument.Load(Find("Resources/P2BlueSteelGreige.xaml")); + string Color(string key) + { + var declared = document.Descendants(Presentation + "SolidColorBrush") + .Single(element => element.Attribute(Xaml + "Key")?.Value == key) + .Attribute("Color")!.Value; + // P1 dialog brushes reference the canonical palette at runtime. + // Resolve the exact Color resource rather than treating its + // DynamicResource markup extension as a literal six-digit hex. + var token = ResourceRef(declared, "DynamicResource"); + if (token is null) + return declared; + return tokens.Descendants(Presentation + "Color") + .Single(element => element.Attribute(Xaml + "Key")?.Value == token).Value; + } var background = Color("TaskDialogCanvas"); var card = Color("TaskDialogCard"); var ink = Color("TaskDialogInk");