Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,25 +220,22 @@
in addition to tab navigation, depending on the situation.<LineBreak />
</Paragraph>
<Paragraph>
Groups of controls that support arrow key navigation typically support Home/End and PgUp/PgDn, too.<LineBreak />
Groups of controls that support arrow key navigation typically support Home/End and PgUp/PgDn, too.
</Paragraph>
<Paragraph>
See <Hyperlink NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#navigation">
Keyboard interactions#Navigation
</Hyperlink>
, <Hyperlink NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#home-and-end-keys">
Keyboard interactions#Home and End keys
</Hyperlink>
, <Hyperlink NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#page-up-and-page-down-keys">
Keyboard interactions#Page up and Page down keys
</Hyperlink>
,
and <Hyperlink NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#control-group">
Keyboard interactions#Control group
</Hyperlink>
.</Paragraph>
</RichTextBlock>

<TextBlock Margin="0,8,0,0" Text="See also:" />
<ItemsControl AutomationProperties.Name="Arrow keys references">
<HyperlinkButton Content="Keyboard interactions: Navigation"
NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#navigation" />
<HyperlinkButton Content="Keyboard interactions: Home and End keys"
NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#home-and-end-keys" />
<HyperlinkButton Content="Keyboard interactions: Page up and Page down keys"
NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#page-up-and-page-down-keys" />
<HyperlinkButton Content="Keyboard interactions: Control group"
NavigateUri="https://learn.microsoft.com/windows/apps/design/input/keyboard-interactions#control-group" />
</ItemsControl>

<TextBlock
Margin="0,20,0,0"
AutomationProperties.HeadingLevel="Level3"
Expand Down
5 changes: 4 additions & 1 deletion WinUIGallery/Samples/ControlPages/MapControlPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<Image
Height="320"
HorizontalAlignment="Left"
AutomationProperties.AccessibilityView="Raw"
Source="/Assets/SampleMedia/MapExample.png" />
<controls:ControlExample
HorizontalAlignment="Stretch"
Expand All @@ -37,14 +38,16 @@
<PasswordBox
x:Name="MapToken"
MinWidth="200"
AutomationProperties.Name="Map service token"
KeyDown="MapToken_KeyDown"
PlaceholderText="Map service token" />
<Button Click="Button_Click" Content="Set token" />
</StackPanel>
<MapControl
x:Name="map1"
Height="400"
HorizontalAlignment="Stretch" />
HorizontalAlignment="Stretch"
AutomationProperties.Name="Map" />
</StackPanel>
</controls:ControlExample.Example>

Expand Down
24 changes: 24 additions & 0 deletions WinUIGallery/Samples/ControlPages/MapControlPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ 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)
Expand Down
2 changes: 1 addition & 1 deletion WinUIGallery/Samples/ControlPages/StoragePickersPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ private async void PickMultipleFilesButton_Click(object sender, RoutedEventArgs
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="SuggestedFolderTextBox" Header="Suggested folder " Width="148" PlaceholderText="Optional" IsReadOnly="True" Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
<Button x:Name="SelectSuggestedFolderButton" Grid.Column="1" Margin="8,0,0,0" VerticalAlignment="Bottom" Click="SelectSuggestedFolderButton_Click" ToolTipService.ToolTip="Select folder">
<Button x:Name="SelectSuggestedFolderButton" Grid.Column="1" Margin="8,0,0,0" VerticalAlignment="Bottom" AutomationProperties.Name="Select folder" Click="SelectSuggestedFolderButton_Click" ToolTipService.ToolTip="Select folder">
<FontIcon Glyph="&#xF89A;" />
</Button>
</Grid>
Expand Down
52 changes: 38 additions & 14 deletions tests/WinUIGallery.UITests/AxeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<RuleId> GloballyExcludedRules =
[
RuleId.NameIsInformative,
RuleId.NameExcludesControlType,
RuleId.NameExcludesLocalizedControlType,
RuleId.SiblingUniqueAndFocusable,
];

internal static void InitializeAxe()
{
var processes = Process.GetProcessesByName("WinUIGallery");
Expand All @@ -23,25 +34,38 @@ internal static void InitializeAxe()
AccessibilityScanner = ScannerFactory.CreateScanner(config);
}

public static void AssertNoAccessibilityErrors()
/// <summary>
/// Scans the current page for accessibility errors and asserts that none are found.
/// </summary>
/// <param name="pageRuleExclusions">
/// 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).
/// </param>
public static void AssertNoAccessibilityErrors(IEnumerable<RuleId> 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<RuleId> 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));
}
}
Expand Down
53 changes: 41 additions & 12 deletions tests/WinUIGallery.UITests/Tests/AxeScanAllTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,17 +19,44 @@ public class AxeScanAll : TestBase
public static readonly string jsonUri = "ControlInfoData.json";
public static new WindowsDriver<WindowsElement> 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<string, RuleId[]> 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<Group> Groups { get; set; }
Expand Down Expand Up @@ -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}\".");
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand Down
Loading