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 - - . + + + + + + + + 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..94588cf4a 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,44 @@ 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" + "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() + { + // 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 +117,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 +128,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 +149,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) {