325 строки
14 KiB
C#
325 строки
14 KiB
C#
using Interop.UIAutomationCore;
|
|
using OpenQA.Selenium;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Microsoft.Edge.A11y
|
|
{
|
|
/// <summary>
|
|
/// A set of helper methods which wrap accessibility APIs
|
|
/// </summary>
|
|
static class EdgeA11yTools
|
|
{
|
|
const int RETRIES = 5;
|
|
const int RECURSIONDEPTH = 5;
|
|
|
|
public const string ExceptionMessage = "Currently only Edge is supported";
|
|
|
|
/// <summary>
|
|
/// This is used to find the DOM of the browser, which is used as the starting
|
|
/// point when searching for elements.
|
|
/// </summary>
|
|
/// <param name="retries">How many times we have already retried</param>
|
|
/// <returns>The browser, or null if it cannot be found</returns>
|
|
public static IUIAutomationElement FindBrowserDocument(int retries)
|
|
{
|
|
var uia = new CUIAutomation8();
|
|
|
|
return FindBrowserDocumentRecurser(uia.GetRootElement(), uia) ?? (retries > RETRIES ? null : FindBrowserDocument(retries + 1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is used only internally to recurse through the elements in the UI tree
|
|
/// to find the DOM.
|
|
/// </summary>
|
|
/// <param name="parent">The parent element to search from</param>
|
|
/// <param name="uia">A UIAutomation8 element that serves as a walker factory</param>
|
|
/// <returns>The browser, or null if it cannot be found</returns>
|
|
static IUIAutomationElement FindBrowserDocumentRecurser(IUIAutomationElement parent, CUIAutomation8 uia)
|
|
{
|
|
try
|
|
{
|
|
var walker = uia.RawViewWalker;
|
|
var element = walker.GetFirstChildElement(parent);
|
|
|
|
for (element = walker.GetFirstChildElement(parent); element != null; element = walker.GetNextSiblingElement(element))
|
|
{
|
|
var className = element.CurrentClassName;
|
|
var name = element.CurrentName;
|
|
|
|
if (name != null && name.Contains("Microsoft Edge"))
|
|
{
|
|
var result = FindBrowserDocumentRecurser(element, uia);
|
|
if (result != null)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
if (className != null && className.Contains("Spartan"))
|
|
{
|
|
var result = FindBrowserDocumentRecurser(element, uia);
|
|
if (result != null)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
if (className != null && className == "TabWindowClass")
|
|
{
|
|
return walker.GetFirstChildElement(walker.GetFirstChildElement(element));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This searches the UI tree to find an element with the given tag.
|
|
/// </summary>
|
|
/// <param name="browserElement">The browser element to search. </param>
|
|
/// <param name="controlType">The tag to search for</param>
|
|
/// <param name="searchStrategy">An alternative search strategy for elements which are not found by their controltype</param>
|
|
/// <param name="foundControlTypes">A list of all control types found on the page, for error reporting</param>
|
|
/// <returns>The elements found which match the tag given</returns>
|
|
public static List<IUIAutomationElement> SearchDocumentChildren(
|
|
IUIAutomationElement browserElement,
|
|
string controlType,
|
|
Func<IUIAutomationElement, bool> searchStrategy,
|
|
out HashSet<string> foundControlTypes)
|
|
{
|
|
var uia = new CUIAutomation8();
|
|
|
|
var walker = uia.RawViewWalker;
|
|
var tosearch = new List<Tuple<IUIAutomationElement, int>>();
|
|
var toreturn = new List<IUIAutomationElement>();
|
|
foundControlTypes = new HashSet<string>();
|
|
|
|
//We use a 0 here to signify the depth in the BFS search tree. The root element will have depth of 0.
|
|
tosearch.Add(new Tuple<IUIAutomationElement, int>(browserElement, 0));
|
|
var automationElementConverter = new ElementConverter();
|
|
|
|
while (tosearch.Any(e => e.Item2 < RECURSIONDEPTH))
|
|
{
|
|
var current = tosearch.First().Item1;
|
|
var currentdepth = tosearch.First().Item2;
|
|
|
|
var convertedRole = automationElementConverter.GetElementNameFromCode(current.CurrentControlType);
|
|
foundControlTypes.Add(convertedRole);
|
|
|
|
if (searchStrategy == null ? convertedRole.Equals(controlType, StringComparison.OrdinalIgnoreCase) : searchStrategy(current))
|
|
{
|
|
toreturn.Add(current);
|
|
}
|
|
else
|
|
{
|
|
for (var child = walker.GetFirstChildElement(current); child != null; child = walker.GetNextSiblingElement(child))
|
|
{
|
|
tosearch.Add(new Tuple<IUIAutomationElement, int>(child, currentdepth + 1));
|
|
}
|
|
}
|
|
|
|
tosearch.RemoveAt(0);
|
|
}
|
|
return toreturn;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find a list of the elements on the page that can be found by
|
|
/// tabbing through the UI.
|
|
///
|
|
/// </summary>
|
|
/// <param name="driverManager">The WebDriver wrapper</param>
|
|
/// <returns>A list of the ids of all tabbable elements</returns>
|
|
public static List<string> TabbableIds(DriverManager driverManager)
|
|
{
|
|
const int timeout = 0;
|
|
//add a dummy input element at the top of the page so we can start tabbing from there
|
|
driverManager.ExecuteScript(
|
|
"document.body.innerHTML = \"<input id='tabroot' />\" + document.body.innerHTML",
|
|
timeout);
|
|
|
|
var toreturn = new List<string>();
|
|
var id = "";
|
|
|
|
for (var i = 1; id != "tabroot"; i++)
|
|
{
|
|
if (id != "" && !toreturn.Contains(id))
|
|
{
|
|
toreturn.Add(id);
|
|
}
|
|
|
|
driverManager.SendTabs("tabroot", i);
|
|
|
|
var activeElement = driverManager.ExecuteScript("return document.activeElement", timeout) as IWebElement;
|
|
if (activeElement != null) id = activeElement.GetAttribute("id");
|
|
}
|
|
|
|
//remove the dummy input so we don't influence any other tests
|
|
driverManager.ExecuteScript(
|
|
"document.body.removeChild(document.getElementById(\"tabroot\"))",
|
|
timeout);
|
|
return toreturn;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Some extension methods
|
|
/// </summary>
|
|
public static class Extensions
|
|
{
|
|
/// <summary>
|
|
/// Send to send tab keys to an element
|
|
/// </summary>
|
|
/// <param name="driver">The driver being extended</param>
|
|
/// <param name="element">The element to send the tabs to</param>
|
|
/// <param name="count">The number of times to send tab</param>
|
|
public static void SendTabs(this DriverManager driver, string element, int count)
|
|
{
|
|
driver.SendSpecialKeys(element, String.Concat(Enumerable.Repeat("Tab", count)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send an Enter key to an element for submitting a form
|
|
/// </summary>
|
|
/// <param name="driver">The driver being extended</param>
|
|
/// <param name="element">The element to submit</param>
|
|
public static void SendSubmit(this DriverManager driver, string element)
|
|
{
|
|
driver.SendSpecialKeys(element, "Enter");
|
|
}
|
|
|
|
/// <summary>
|
|
/// This allows the SendSpecialKeys function to take friendly names instead of
|
|
/// character codes
|
|
/// </summary>
|
|
public static Lazy<Dictionary<string, string>> specialKeys = new Lazy<Dictionary<string, string>>(() =>
|
|
{
|
|
var keys = new Dictionary<string, string>();
|
|
|
|
keys.Add("Null", '\uE000'.ToString());
|
|
keys.Add("Cancel", '\uE001'.ToString());
|
|
keys.Add("Help", '\uE002'.ToString());
|
|
keys.Add("Back_space", '\uE003'.ToString());
|
|
keys.Add("Tab", '\uE004'.ToString());
|
|
keys.Add("Clear", '\uE005'.ToString());
|
|
keys.Add("Return", '\uE006'.ToString());
|
|
keys.Add("Enter", '\uE007'.ToString());
|
|
keys.Add("Shift", '\uE008'.ToString());
|
|
keys.Add("Control", '\uE009'.ToString());
|
|
keys.Add("Alt", '\uE00A'.ToString());
|
|
keys.Add("Pause", '\uE00B'.ToString());
|
|
keys.Add("Escape", '\uE00C'.ToString());
|
|
keys.Add("Space", '\uE00D'.ToString());
|
|
keys.Add("Page_up", '\uE00E'.ToString());
|
|
keys.Add("Page_down", '\uE00F'.ToString());
|
|
keys.Add("End", '\uE010'.ToString());
|
|
keys.Add("Home", '\uE011'.ToString());
|
|
keys.Add("Arrow_left", '\uE012'.ToString());
|
|
keys.Add("Arrow_up", '\uE013'.ToString());
|
|
keys.Add("Arrow_right", '\uE014'.ToString());
|
|
keys.Add("Arrow_down", '\uE015'.ToString());
|
|
keys.Add("Insert", '\uE016'.ToString());
|
|
keys.Add("Delete", '\uE017'.ToString());
|
|
keys.Add("Semicolon", '\uE018'.ToString());
|
|
keys.Add("Equals", '\uE019'.ToString());
|
|
keys.Add("Numpad0", '\uE01A'.ToString());
|
|
keys.Add("Numpad1", '\uE01B'.ToString());
|
|
keys.Add("Numpad2", '\uE01C'.ToString());
|
|
keys.Add("Numpad3", '\uE01D'.ToString());
|
|
keys.Add("Numpad4", '\uE01E'.ToString());
|
|
keys.Add("Numpad5", '\uE01F'.ToString());
|
|
keys.Add("Numpad6", '\uE020'.ToString());
|
|
keys.Add("Numpad7", '\uE021'.ToString());
|
|
keys.Add("Numpad8", '\uE022'.ToString());
|
|
keys.Add("Numpad9", '\uE023'.ToString());
|
|
keys.Add("Multiply", '\uE024'.ToString());
|
|
keys.Add("Add", '\uE025'.ToString());
|
|
keys.Add("Separator", '\uE026'.ToString());
|
|
keys.Add("Subtract", '\uE027'.ToString());
|
|
keys.Add("Decimal", '\uE028'.ToString());
|
|
keys.Add("Divide", '\uE029'.ToString());
|
|
keys.Add("F1", '\uE031'.ToString());
|
|
keys.Add("F2", '\uE032'.ToString());
|
|
keys.Add("F3", '\uE033'.ToString());
|
|
keys.Add("F4", '\uE034'.ToString());
|
|
keys.Add("F5", '\uE035'.ToString());
|
|
keys.Add("F6", '\uE036'.ToString());
|
|
keys.Add("F7", '\uE037'.ToString());
|
|
keys.Add("F8", '\uE038'.ToString());
|
|
keys.Add("F9", '\uE039'.ToString());
|
|
keys.Add("F10", '\uE03A'.ToString());
|
|
keys.Add("F11", '\uE03B'.ToString());
|
|
keys.Add("F12", '\uE03C'.ToString());
|
|
keys.Add("Meta", '\uE03D'.ToString());
|
|
keys.Add("Command", '\uE03D'.ToString());
|
|
keys.Add("Zenkaku_hankaku", '\uE040'.ToString());
|
|
|
|
return keys;
|
|
});
|
|
|
|
/// <summary>
|
|
/// A wrapper which converts strings with friendly-named special keys to be
|
|
/// converted into the appropriate character codes.
|
|
///
|
|
/// E.G. "Arrow_left" becomes '\uE012'.toString()
|
|
/// </summary>
|
|
/// <param name="driver"></param>
|
|
/// <param name="elementId"></param>
|
|
/// <param name="keysToSend"></param>
|
|
public static void SendSpecialKeys(this DriverManager driver, string elementId, string keysToSend)
|
|
{
|
|
foreach (var key in specialKeys.Value)
|
|
{
|
|
keysToSend = keysToSend.Replace(key.Key, key.Value);
|
|
}
|
|
driver.SendKeys(elementId, keysToSend);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all of the Control Patterns supported by an element
|
|
/// </summary>
|
|
/// <param name="element">The element being extended</param>
|
|
/// <returns>A list of all the patterns supported</returns>
|
|
public static List<string> GetPatterns(this IUIAutomationElement element)
|
|
{
|
|
int[] ids;
|
|
string[] names;
|
|
new CUIAutomation8().PollForPotentialSupportedPatterns(element, out ids, out names);
|
|
return names.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all the properties supported by an element
|
|
/// </summary>
|
|
/// <param name="element">The element being extended</param>
|
|
/// <returns>A list of all the properties supported</returns>
|
|
public static List<string> GetProperties(this IUIAutomationElement element)
|
|
{
|
|
int[] ids;
|
|
string[] names;
|
|
new CUIAutomation8().PollForPotentialSupportedProperties(element, out ids, out names);
|
|
return names.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the names of all children (not all descendents)
|
|
/// </summary>
|
|
/// <param name="element">The element being extended</param>
|
|
/// <returns>A list of all the children's names</returns>
|
|
public static List<string> GetChildNames(this IUIAutomationElement element)
|
|
{
|
|
var toreturn = new List<string>();
|
|
var walker = new CUIAutomation8().RawViewWalker;
|
|
for (var child = walker.GetFirstChildElement(element); child != null; child = walker.GetNextSiblingElement(child))
|
|
{
|
|
toreturn.Add(child.CurrentName);
|
|
}
|
|
|
|
return toreturn;
|
|
}
|
|
}
|
|
} |