From 429bb5d4b7e2fed86ccadad66bb7e6e2d2180974 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 11 Apr 2026 22:54:58 +0200 Subject: [PATCH 1/5] Fix Axe tests: replace global rule disabling with per-page exclusions (#1474) Refactor the Axe accessibility test infrastructure to use targeted per-page rule exclusions instead of globally disabling 10 rules for all pages. Changes: - AxeHelper: Accept optional per-page rule exclusions parameter. Only 4 framework-level rules (NameIsInformative, NameExcludesControlType, NameExcludesLocalizedControlType, SiblingUniqueAndFocusable) remain globally excluded. Improve error messages with element name, automation ID, and rule ID for easier debugging. - AxeScanAllTests: Add PageRuleExclusions dictionary mapping specific pages (WebView2, Icons, MapControl) to their known rule exclusions. Move WebView2, Icons, MapControl from full exclusion list to per-page rule exclusions so they are now scanned with most rules enabled. - MapControlPage: Add AutomationProperties.Name to PasswordBox and MapControl. Mark decorative Image with AccessibilityView=Raw. - StoragePickersPage: Add AutomationProperties.Name to icon-only button. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Samples/ControlPages/MapControlPage.xaml | 5 +- .../ControlPages/StoragePickersPage.xaml | 2 +- tests/WinUIGallery.UITests/AxeHelper.cs | 52 +++++++++++++----- .../Tests/AxeScanAllTests.cs | 55 +++++++++++++++---- 4 files changed, 87 insertions(+), 27 deletions(-) diff --git a/WinUIGallery/Samples/ControlPages/MapControlPage.xaml b/WinUIGallery/Samples/ControlPages/MapControlPage.xaml index a6cdf5d85..757e1ec0b 100644 --- a/WinUIGallery/Samples/ControlPages/MapControlPage.xaml +++ b/WinUIGallery/Samples/ControlPages/MapControlPage.xaml @@ -25,6 +25,7 @@ diff --git a/tests/WinUIGallery.UITests/AxeHelper.cs b/tests/WinUIGallery.UITests/AxeHelper.cs index 6b30a43be..3de79d3ea 100644 --- a/tests/WinUIGallery.UITests/AxeHelper.cs +++ b/tests/WinUIGallery.UITests/AxeHelper.cs @@ -4,6 +4,7 @@ using Axe.Windows.Automation; using Axe.Windows.Core.Enums; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -13,6 +14,16 @@ public class AxeHelper { public static IScanner AccessibilityScanner; + // Rules excluded globally due to known WinUI framework issues that affect all pages. + // These are not fixable from app code and produce false positives. + private static readonly HashSet GloballyExcludedRules = + [ + RuleId.NameIsInformative, + RuleId.NameExcludesControlType, + RuleId.NameExcludesLocalizedControlType, + RuleId.SiblingUniqueAndFocusable, + ]; + internal static void InitializeAxe() { var processes = Process.GetProcessesByName("WinUIGallery"); @@ -23,25 +34,38 @@ internal static void InitializeAxe() AccessibilityScanner = ScannerFactory.CreateScanner(config); } - public static void AssertNoAccessibilityErrors() + /// + /// Scans the current page for accessibility errors and asserts that none are found. + /// + /// + /// Optional set of rule IDs to exclude for this specific page. Use this for known + /// framework-level issues that only affect certain pages (e.g., BoundingRectangle + /// rules for pages with off-screen or collapsed elements). + /// + public static void AssertNoAccessibilityErrors(IEnumerable pageRuleExclusions = null) { - // Bug 1474: Disabling Rules NameReasonableLength and BoundingRectangleNotNull temporarily - var testResult = AccessibilityScanner.Scan(null).WindowScanOutputs.SelectMany(output => output.Errors) - .Where(rule => rule.Rule.ID != RuleId.NameIsInformative) - .Where(rule => rule.Rule.ID != RuleId.NameExcludesControlType) - .Where(rule => rule.Rule.ID != RuleId.NameExcludesLocalizedControlType) - .Where(rule => rule.Rule.ID != RuleId.SiblingUniqueAndFocusable) - .Where(rule => rule.Rule.ID != RuleId.NameReasonableLength) - .Where(rule => rule.Rule.ID != RuleId.BoundingRectangleNotNull) - .Where(rule => rule.Rule.ID != RuleId.BoundingRectangleNotNullListViewXAML) - .Where(rule => rule.Rule.ID != RuleId.BoundingRectangleNotNullTextBlockXAML) - .Where(rule => rule.Rule.ID != RuleId.NameNotNull) - .Where(rule => rule.Rule.ID != RuleId.ChromiumComponentsShouldUseWebScanner); + HashSet excludedRules = new(GloballyExcludedRules); + + if (pageRuleExclusions != null) + { + excludedRules.UnionWith(pageRuleExclusions); + } + + var testResult = AccessibilityScanner.Scan(null).WindowScanOutputs + .SelectMany(output => output.Errors) + .Where(rule => !excludedRules.Contains(rule.Rule.ID)); if (testResult.Any()) { var mappedResult = testResult.Select(result => - "Element " + result.Element.Properties["ControlType"] + " violated rule '" + result.Rule.Description + "'."); + { + string controlType = result.Element.Properties.TryGetValue("ControlType", out string ct) ? ct : "Unknown"; + string name = result.Element.Properties.TryGetValue("Name", out string n) ? n : "(no name)"; + string automationId = result.Element.Properties.TryGetValue("AutomationId", out string aid) ? aid : "(no id)"; + return $"[{result.Rule.ID}] Element '{controlType}' (Name='{name}', AutomationId='{automationId}') " + + $"violated rule '{result.Rule.Description}'."; + }); + Assert.Fail("Failed with the following accessibility errors \r\n" + string.Join("\r\n", mappedResult)); } } diff --git a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs index 7bfb0436f..e874d93df 100644 --- a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs +++ b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Axe.Windows.Core.Enums; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; +using Newtonsoft.Json; using OpenQA.Selenium.Appium.Windows; -using System; -using System.Linq; -using System.Text.Json; using System.Collections.Generic; +using System.Linq; using System.Reflection; -using Newtonsoft.Json; -using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; using System.Threading; namespace WinUIGallery.UITests.Tests; @@ -20,17 +19,48 @@ public class AxeScanAll : TestBase public static readonly string jsonUri = "ControlInfoData.json"; public static new WindowsDriver Session => SessionManager.Session; + // Pages that are completely excluded from Axe scanning due to AxeWindowsAutomationException + // or other issues that prevent scanning entirely. public static string[] ExclusionList = [ - "WebView2", // 46668961: Web contents from WebView2 are throwing null BoundingRectangle errors. - "Icons", // https://github.com/CommunityToolkit/Windows/issues/240 External toolkit SettingsExpander does not pass Axe testing // https://github.com/microsoft/axe-windows/issues/662 - // AxeWindowsAutomationException: Failed to get the root element(s) of the specified process error for following pages: + // AxeWindowsAutomationException: Failed to get the root element(s) of the specified process "PersonPicture", - "MapControl", "TabView" ]; + // Per-page rule exclusions for known framework-level issues that cannot be fixed in app code. + // Prefer adding targeted exclusions here over globally disabling rules in AxeHelper. + private static readonly Dictionary PageRuleExclusions = new() + { + // WebView2 hosts Chromium content which triggers BoundingRectangle and Chromium scanner rules + ["WebView2"] = + [ + RuleId.BoundingRectangleNotNull, + RuleId.BoundingRectangleNotNullListViewXAML, + RuleId.BoundingRectangleNotNullTextBlockXAML, + RuleId.ChromiumComponentsShouldUseWebScanner, + RuleId.NameNotNull, + RuleId.NameReasonableLength, + ], + // External CommunityToolkit SettingsExpander does not pass Axe testing + // https://github.com/CommunityToolkit/Windows/issues/240 + ["Icons"] = + [ + RuleId.NameNotNull, + RuleId.NameReasonableLength, + ], + // MapControl hosts external map content that can trigger BoundingRectangle errors + ["MapControl"] = + [ + RuleId.BoundingRectangleNotNull, + RuleId.BoundingRectangleNotNullListViewXAML, + RuleId.BoundingRectangleNotNullTextBlockXAML, + RuleId.NameNotNull, + RuleId.NameReasonableLength, + ], + }; + public class ControlInfoData { public List Groups { get; set; } @@ -91,6 +121,9 @@ public static void ClassInitialize(TestContext context) [TestProperty("Description", "Scan pages in the WinUIGallery for accessibility issues.")] public void ValidatePageAccessibilityWithAxe(string sectionName, string pageName) { + // Look up per-page rule exclusions for this page + PageRuleExclusions.TryGetValue(pageName, out RuleId[] ruleExclusions); + try { Logger.LogMessage($"Opening page \"{pageName}\"."); @@ -99,7 +132,7 @@ public void ValidatePageAccessibilityWithAxe(string sectionName, string pageName var page = Session.FindElementByAccessibilityId(pageName); page.Click(); - AxeHelper.AssertNoAccessibilityErrors(); + AxeHelper.AssertNoAccessibilityErrors(ruleExclusions); } catch (OpenQA.Selenium.WebDriverException exc) { @@ -120,7 +153,7 @@ public void ValidatePageAccessibilityWithAxe(string sectionName, string pageName var page = Session.FindElementByAccessibilityId(pageName); page.Click(); - AxeHelper.AssertNoAccessibilityErrors(); + AxeHelper.AssertNoAccessibilityErrors(ruleExclusions); } catch (OpenQA.Selenium.WebDriverException exc2) { From 3f44938194c3698cc4a2eef5cf8b69087532851e Mon Sep 17 00:00:00 2001 From: Marcel Wagner Date: Sat, 18 Apr 2026 16:59:08 +0200 Subject: [PATCH 2/5] Experimenting --- .../Tests/AxeScanAllTests.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs index e874d93df..94588cf4a 100644 --- a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs +++ b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs @@ -26,23 +26,19 @@ public class AxeScanAll : TestBase // https://github.com/microsoft/axe-windows/issues/662 // AxeWindowsAutomationException: Failed to get the root element(s) of the specified process "PersonPicture", - "TabView" + "TabView", + "MediaPlayerElement", + // WebView2 hosts Chromium content. Axe.Windows throws a NullReferenceException + // inside DesktopElementExtensionMethods.AddLogicalSizePseudoProperty during the + // parallel tree walk, before any rule filtering can take effect, so per-rule + // exclusions are insufficient. + "WebView2" ]; // Per-page rule exclusions for known framework-level issues that cannot be fixed in app code. // Prefer adding targeted exclusions here over globally disabling rules in AxeHelper. private static readonly Dictionary PageRuleExclusions = new() { - // WebView2 hosts Chromium content which triggers BoundingRectangle and Chromium scanner rules - ["WebView2"] = - [ - RuleId.BoundingRectangleNotNull, - RuleId.BoundingRectangleNotNullListViewXAML, - RuleId.BoundingRectangleNotNullTextBlockXAML, - RuleId.ChromiumComponentsShouldUseWebScanner, - RuleId.NameNotNull, - RuleId.NameReasonableLength, - ], // External CommunityToolkit SettingsExpander does not pass Axe testing // https://github.com/CommunityToolkit/Windows/issues/240 ["Icons"] = From ce932bd6334226c7229e27a2ef633b8af83248b4 Mon Sep 17 00:00:00 2001 From: Marcel Wagner Date: Sat, 18 Apr 2026 17:54:55 +0200 Subject: [PATCH 3/5] Adjustment --- .../AccessibilityKeyboardPage.xaml | 29 +++++++++---------- .../ControlPages/MapControlPage.xaml.cs | 24 +++++++++++++++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/WinUIGallery/Samples/ControlPages/Accessibility/AccessibilityKeyboardPage.xaml b/WinUIGallery/Samples/ControlPages/Accessibility/AccessibilityKeyboardPage.xaml index 822bba263..725cb978e 100644 --- a/WinUIGallery/Samples/ControlPages/Accessibility/AccessibilityKeyboardPage.xaml +++ b/WinUIGallery/Samples/ControlPages/Accessibility/AccessibilityKeyboardPage.xaml @@ -220,25 +220,22 @@ in addition to tab navigation, depending on the situation. - Groups of controls that support arrow key navigation typically support Home/End and PgUp/PgDn, too. + Groups of controls that support arrow key navigation typically support Home/End and PgUp/PgDn, too. - - See - Keyboard interactions#Navigation - - , - Keyboard interactions#Home and End keys - - , - Keyboard interactions#Page up and Page down keys - - , - and - Keyboard interactions#Control group - - . + + + + + + + + Date: Mon, 20 Apr 2026 08:47:05 +0200 Subject: [PATCH 4/5] Changes --- .../ControlPages/MapControlPage.xaml.cs | 24 ------------------- .../Tests/AxeScanAllTests.cs | 18 +++++++------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/WinUIGallery/Samples/ControlPages/MapControlPage.xaml.cs b/WinUIGallery/Samples/ControlPages/MapControlPage.xaml.cs index 716e3afbc..b7cafd434 100644 --- a/WinUIGallery/Samples/ControlPages/MapControlPage.xaml.cs +++ b/WinUIGallery/Samples/ControlPages/MapControlPage.xaml.cs @@ -15,30 +15,6 @@ public MapControlPage() this.InitializeComponent(); this.Loaded += MapControlPage_Loaded; - this.Unloaded += MapControlPage_Unloaded; - } - - private void MapControlPage_Unloaded(object sender, RoutedEventArgs e) - { - // MapControl is internally backed by a WebView2 hosting Azure Maps. - // When the page is navigated away from, the WebView2's UIA tree - // (a Pane containing a Chromium RootWebArea with a long - // data:text/html;base64 Name) can outlive the page and contaminate - // subsequent Axe.Windows accessibility scans across the process. - // Explicitly tearing down the MapControl on Unloaded forces the - // framework to dispose the embedded WebView2 and remove its UIA - // subtree from the process tree. - this.Loaded -= MapControlPage_Loaded; - this.Unloaded -= MapControlPage_Unloaded; - - if (map1 is not null) - { - map1.Layers.Clear(); - if (map1.Parent is Panel parent) - { - parent.Children.Remove(map1); - } - } } private void MapControlPage_Loaded(object sender, RoutedEventArgs e) diff --git a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs index 94588cf4a..a256ee83b 100644 --- a/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs +++ b/tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs @@ -32,7 +32,14 @@ public class AxeScanAll : TestBase // inside DesktopElementExtensionMethods.AddLogicalSizePseudoProperty during the // parallel tree walk, before any rule filtering can take effect, so per-rule // exclusions are insufficient. - "WebView2" + "WebView2", + // MapControl is internally backed by a WebView2 hosting Azure Maps. The system + // control has no public Dispose path, so its embedded WebView2 (and its UIA + // Pane + Chromium RootWebArea) leaks into the process tree even after the page + // is unloaded, contaminating every subsequent Axe scan with a long + // data:text/html;base64 Name. Skipping the page entirely prevents the WebView2 + // from ever being instantiated during the test run. + "MapControl" ]; // Per-page rule exclusions for known framework-level issues that cannot be fixed in app code. @@ -46,15 +53,6 @@ public class AxeScanAll : TestBase RuleId.NameNotNull, RuleId.NameReasonableLength, ], - // MapControl hosts external map content that can trigger BoundingRectangle errors - ["MapControl"] = - [ - RuleId.BoundingRectangleNotNull, - RuleId.BoundingRectangleNotNullListViewXAML, - RuleId.BoundingRectangleNotNullTextBlockXAML, - RuleId.NameNotNull, - RuleId.NameReasonableLength, - ], }; public class ControlInfoData From 82f5c8a2541e44ef777d90c87b462725377f6a83 Mon Sep 17 00:00:00 2001 From: Marcel Wagner Date: Mon, 20 Apr 2026 11:25:57 +0200 Subject: [PATCH 5/5] Fixup --- WinUIGallery/Controls/ColorSelector.xaml | 3 ++- .../Samples/ControlPages/AppWindowTitleBarPage.xaml | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/WinUIGallery/Controls/ColorSelector.xaml b/WinUIGallery/Controls/ColorSelector.xaml index 17f9b3193..dfa0264b3 100644 --- a/WinUIGallery/Controls/ColorSelector.xaml +++ b/WinUIGallery/Controls/ColorSelector.xaml @@ -1,6 +1,7 @@ - + @@ -79,36 +85,42 @@