├── .gitattributes ├── .gitignore ├── A11y.csproj ├── A11y.sln ├── DriverManager.cs ├── DriversExecutables ├── AMD64 │ ├── MicrosoftWebDriver.exe │ └── chromedriver.exe └── X86 │ ├── MicrosoftWebDriver.exe │ └── chromedriver.exe ├── EdgeA11yTools.cs ├── EdgeStrategy.cs ├── ElementConverter.cs ├── Interop.UIAutomationCore.dll ├── Javascript.cs ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── TestCaseResult.cs ├── TestData.cs ├── TestStrategy.cs ├── diff_runs.ps1 ├── license.txt ├── packages.config └── run.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # The output of the test 5 | scores.csv 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # NuGet Packages 149 | *.nupkg 150 | # The packages folder can be ignored because of Package Restore 151 | **/packages/* 152 | # except build/, which is used as an MSBuild target. 153 | !**/packages/build/ 154 | # Uncomment if necessary however generally it will be regenerated when needed 155 | #!**/packages/repositories.config 156 | 157 | # Windows Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Windows Azure Emulator 162 | efc/ 163 | rfc/ 164 | 165 | # Windows Store app package directory 166 | AppPackages/ 167 | 168 | # Visual Studio cache files 169 | # files ending in .cache can be ignored 170 | *.[Cc]ache 171 | # but keep track of directories ending in .cache 172 | !*.[Cc]ache/ 173 | 174 | # Others 175 | ClientBin/ 176 | [Ss]tyle[Cc]op.* 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.pfx 182 | *.publishsettings 183 | node_modules/ 184 | orleans.codegen.cs 185 | 186 | # RIA/Silverlight projects 187 | Generated_Code/ 188 | 189 | # Backup & report files from converting an old project file 190 | # to a newer Visual Studio version. Backup files are not needed, 191 | # because we have git ;-) 192 | _UpgradeReport_Files/ 193 | Backup*/ 194 | UpgradeLog*.XML 195 | UpgradeLog*.htm 196 | 197 | # SQL Server files 198 | *.mdf 199 | *.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | 209 | # GhostDoc plugin setting file 210 | *.GhostDoc.xml 211 | 212 | # Node.js Tools for Visual Studio 213 | .ntvs_analysis.dat 214 | 215 | # Visual Studio 6 build log 216 | *.plg 217 | 218 | # Visual Studio 6 workspace options file 219 | *.opt 220 | 221 | # Visual Studio LightSwitch build output 222 | **/*.HTMLClient/GeneratedArtifacts 223 | **/*.DesktopClient/GeneratedArtifacts 224 | **/*.DesktopClient/ModelManifest.xml 225 | **/*.Server/GeneratedArtifacts 226 | **/*.Server/ModelManifest.xml 227 | _Pvt_Extensions 228 | 229 | # Paket dependency manager 230 | .paket/paket.exe 231 | 232 | # FAKE - F# Make 233 | .fake/ 234 | 235 | # Powershell scripts used internally 236 | *int.ps1 237 | 238 | # Nuget executable 239 | nuget.exe 240 | -------------------------------------------------------------------------------- /A11y.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954} 8 | Exe 9 | Properties 10 | Microsoft.Edge.A11y 11 | Microsoft.Edge.A11y 12 | v4.5 13 | 512 14 | ..\ 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | False 39 | True 40 | A11y\Interop.UIAutomationCore.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | packages\Selenium.WebDriver.2.52.0\lib\net40\WebDriver.dll 53 | True 54 | 55 | 56 | packages\Selenium.Support.2.52.0\lib\net40\WebDriver.Support.dll 57 | True 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Always 78 | 79 | 80 | 81 | 88 | 89 | -------------------------------------------------------------------------------- /A11y.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.40629.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A11y", "A11y.csproj", "{3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug64|Any CPU = Debug64|Any CPU 13 | Debug64|x64 = Debug64|x64 14 | Release|Any CPU = Release|Any CPU 15 | Release|x64 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug|x64.ActiveCfg = Debug|Any CPU 21 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug64|Any CPU.ActiveCfg = Debug|Any CPU 22 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug64|Any CPU.Build.0 = Debug|Any CPU 23 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Debug64|x64.ActiveCfg = Debug|Any CPU 24 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {3A2CB5B2-3FD1-4F87-8EBB-581562D1F954}.Release|x64.ActiveCfg = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(SolutionProperties) = preSolution 29 | HideSolutionNode = FALSE 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DriverManager.cs: -------------------------------------------------------------------------------- 1 | using OpenQA.Selenium; 2 | using OpenQA.Selenium.Edge; 3 | using OpenQA.Selenium.Remote; 4 | using OpenQA.Selenium.Support.UI; 5 | using System; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading; 9 | 10 | namespace Microsoft.Edge.A11y 11 | { 12 | /// 13 | /// This is another wrapper around WebDriver. The reason this is used is to maintain 14 | /// compatibility with Edge internal testing tools. 15 | /// 16 | public class DriverManager 17 | { 18 | private RemoteWebDriver _driver; 19 | private TimeSpan _searchTimeout; 20 | 21 | /// 22 | /// Only ctor 23 | /// 24 | /// How long to search for elements 25 | public DriverManager(TimeSpan searchTimeout) 26 | { 27 | try 28 | { 29 | _driver = new EdgeDriver(EdgeDriverService.CreateDefaultService(DriverExecutablePath, "MicrosoftWebDriver.exe", 17556)); 30 | } 31 | catch (InvalidOperationException) 32 | { 33 | Console.WriteLine("Unable to start a WebDriver session. Ensure that the previous server window is closed."); 34 | Environment.Exit(1); 35 | } 36 | _searchTimeout = searchTimeout; 37 | } 38 | 39 | /// 40 | /// The root directory of the A11y project 41 | /// 42 | public static string ProjectRootFolder 43 | { 44 | get 45 | { 46 | return new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).Parent.Parent.FullName; 47 | } 48 | } 49 | 50 | /// 51 | /// The path to the WebDriver executables for Edge 52 | /// 53 | private static string DriverExecutablePath 54 | { 55 | get 56 | { 57 | //TODO support for linux/mac paths 58 | return Path.Combine(ProjectRootFolder, Environment.Is64BitOperatingSystem 59 | ? "DriversExecutables\\AMD64" 60 | : "DriversExecutables\\X86"); 61 | } 62 | } 63 | 64 | /// 65 | /// Navigate to url 66 | /// 67 | /// 68 | public void NavigateToUrl(string url) 69 | { 70 | _driver.Navigate().GoToUrl(url); 71 | } 72 | 73 | /// 74 | /// Execute a script on the current page 75 | /// 76 | /// The script 77 | /// The timeout in seconds 78 | /// How long to wait before executing the script in milliseconds 79 | /// Parameters to pass to the script 80 | /// 81 | public object ExecuteScript(string script, int timeout, int additionalSleep = 0, params object[] args) 82 | { 83 | Thread.Sleep(additionalSleep); 84 | 85 | var wait = new WebDriverWait(_driver, new TimeSpan(0, 0, 0, timeout)); 86 | wait.Until(driver => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState").Equals("complete")); 87 | 88 | var js = (IJavaScriptExecutor)_driver; 89 | return js.ExecuteScript(script, args); 90 | } 91 | 92 | /// 93 | /// Send the given keys to the element with the given id 94 | /// 95 | /// The element's id 96 | /// The keys to send 97 | public void SendKeys(string elementId, string keys) 98 | { 99 | _driver.FindElement(By.Id(elementId)).SendKeys(keys); 100 | } 101 | 102 | public Screenshot GetScreenshot() 103 | { 104 | return _driver.GetScreenshot(); 105 | } 106 | 107 | /// 108 | /// Close the driver 109 | /// 110 | internal void Close() 111 | { 112 | if (null != _driver) 113 | { 114 | try 115 | { 116 | _driver.Quit(); 117 | _driver.Dispose(); 118 | } 119 | catch (Exception) 120 | { 121 | // Don't throw here 122 | } 123 | _driver = null; 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /DriversExecutables/AMD64/MicrosoftWebDriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/A11y/926dcaedcb5c9f45f0d4e10ae09942882483c74e/DriversExecutables/AMD64/MicrosoftWebDriver.exe -------------------------------------------------------------------------------- /DriversExecutables/AMD64/chromedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/A11y/926dcaedcb5c9f45f0d4e10ae09942882483c74e/DriversExecutables/AMD64/chromedriver.exe -------------------------------------------------------------------------------- /DriversExecutables/X86/MicrosoftWebDriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/A11y/926dcaedcb5c9f45f0d4e10ae09942882483c74e/DriversExecutables/X86/MicrosoftWebDriver.exe -------------------------------------------------------------------------------- /DriversExecutables/X86/chromedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/A11y/926dcaedcb5c9f45f0d4e10ae09942882483c74e/DriversExecutables/X86/chromedriver.exe -------------------------------------------------------------------------------- /EdgeA11yTools.cs: -------------------------------------------------------------------------------- 1 | using Interop.UIAutomationCore; 2 | using OpenQA.Selenium; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing.Imaging; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using static Microsoft.Edge.A11y.ElementConverter; 10 | 11 | namespace Microsoft.Edge.A11y 12 | { 13 | /// 14 | /// A set of helper methods which wrap accessibility APIs 15 | /// 16 | static class EdgeA11yTools 17 | { 18 | const int RETRIES = 5; 19 | const int RECURSIONDEPTH = 10; 20 | 21 | public const string ExceptionMessage = "Currently only Edge is supported"; 22 | 23 | /// 24 | /// This is used to find the DOM of the browser, which is used as the starting 25 | /// point when searching for elements. 26 | /// 27 | /// How many times we have already retried 28 | /// The browser, or null if it cannot be found 29 | public static IUIAutomationElement FindBrowserDocument(int retries) 30 | { 31 | var uia = new CUIAutomation8(); 32 | 33 | return FindBrowserDocumentRecurser(uia.GetRootElement(), uia) ?? (retries > RETRIES ? null : FindBrowserDocument(retries + 1)); 34 | } 35 | 36 | /// 37 | /// This is used only internally to recurse through the elements in the UI tree 38 | /// to find the DOM. 39 | /// 40 | /// The parent element to search from 41 | /// A UIAutomation8 element that serves as a walker factory 42 | /// The browser, or null if it cannot be found 43 | static IUIAutomationElement FindBrowserDocumentRecurser(IUIAutomationElement parent, CUIAutomation8 uia) 44 | { 45 | try 46 | { 47 | var walker = uia.RawViewWalker; 48 | var element = walker.GetFirstChildElement(parent); 49 | 50 | for (element = walker.GetFirstChildElement(parent); element != null; element = walker.GetNextSiblingElement(element)) 51 | { 52 | var className = element.CurrentClassName; 53 | var name = element.CurrentName; 54 | 55 | if (name != null && name.Contains("Microsoft Edge")) 56 | { 57 | var result = FindBrowserDocumentRecurser(element, uia); 58 | if (result != null) 59 | { 60 | return result; 61 | } 62 | } 63 | if (className != null && className.Contains("Spartan")) 64 | { 65 | var result = FindBrowserDocumentRecurser(element, uia); 66 | if (result != null) 67 | { 68 | return result; 69 | } 70 | } 71 | if (className != null && className == "TabWindowClass") 72 | { 73 | return element; 74 | } 75 | } 76 | return null; 77 | } 78 | catch 79 | { 80 | return null; 81 | } 82 | } 83 | 84 | /// 85 | /// This searches the UI tree to find an element with the given tag. 86 | /// 87 | /// The browser element to search. 88 | /// The tag to search for 89 | /// An alternative search strategy for elements which are not found by their controltype 90 | /// A list of all control types found on the page, for error reporting 91 | /// The elements found which match the tag given 92 | public static List SearchChildren( 93 | IUIAutomationElement browserElement, 94 | UIAControlType controlType, 95 | Func searchStrategy, 96 | out HashSet foundControlTypes) 97 | { 98 | var uia = new CUIAutomation8(); 99 | 100 | var walker = uia.RawViewWalker; 101 | var tosearch = new List>(); 102 | var toreturn = new List(); 103 | foundControlTypes = new HashSet(); 104 | 105 | //We use a 0 here to signify the depth in the BFS search tree. The root element will have depth of 0. 106 | tosearch.Add(new Tuple(browserElement, 0)); 107 | 108 | while (tosearch.Any(e => e.Item2 < RECURSIONDEPTH)) 109 | { 110 | var current = tosearch.First().Item1; 111 | var currentdepth = tosearch.First().Item2; 112 | 113 | var convertedRole = GetControlTypeFromCode(current.CurrentControlType); 114 | foundControlTypes.Add(convertedRole); 115 | 116 | if (searchStrategy == null ? convertedRole == controlType : searchStrategy(current)) 117 | { 118 | toreturn.Add(current); 119 | } 120 | else 121 | { 122 | for (var child = walker.GetFirstChildElement(current); child != null; child = walker.GetNextSiblingElement(child)) 123 | { 124 | tosearch.Add(new Tuple(child, currentdepth + 1)); 125 | } 126 | } 127 | 128 | tosearch.RemoveAt(0); 129 | } 130 | return toreturn; 131 | } 132 | 133 | /// 134 | /// Find a list of the elements on the page that can be found by 135 | /// tabbing through the UI. 136 | /// 137 | /// The WebDriver wrapper 138 | /// A list of the ids of all tabbable elements 139 | public static List TabbableIds(DriverManager driverManager) 140 | { 141 | const int timeout = 0; 142 | //add a dummy input element at the top of the page so we can start tabbing from there 143 | driverManager.ExecuteScript( 144 | "document.body.innerHTML = \"\" + document.body.innerHTML", 145 | timeout); 146 | 147 | var toreturn = new List(); 148 | var id = ""; 149 | 150 | for (var i = 1; id != "tabroot"; i++) 151 | { 152 | if (id != "" && !toreturn.Contains(id)) 153 | { 154 | toreturn.Add(id); 155 | } 156 | 157 | driverManager.SendTabs("tabroot", i); 158 | 159 | var activeElement = driverManager.ExecuteScript("return document.activeElement", timeout) as IWebElement; 160 | if (activeElement != null) id = activeElement.GetAttribute("id"); 161 | } 162 | 163 | //remove the dummy input so we don't influence any other tests 164 | driverManager.ExecuteScript( 165 | "document.body.removeChild(document.getElementById(\"tabroot\"))", 166 | timeout); 167 | return toreturn; 168 | } 169 | } 170 | 171 | /// 172 | /// Some extension methods 173 | /// 174 | public static class Extensions 175 | { 176 | /// 177 | /// Send to send tab keys to an element 178 | /// 179 | /// The driver being extended 180 | /// The element to send the tabs to 181 | /// The number of times to send tab 182 | public static void SendTabs(this DriverManager driver, string element, int count) 183 | { 184 | var tabs = new List(); 185 | for(var i = 0; i< count; i++) 186 | { 187 | tabs.Add(WebDriverKey.Tab); 188 | } 189 | driver.SendSpecialKeys(element, tabs); 190 | } 191 | 192 | 193 | /// 194 | /// A wrapper which converts strings with friendly-named special keys to be 195 | /// converted into the appropriate character codes. 196 | /// 197 | /// E.G. "Arrow_left" becomes '\uE012'.toString() 198 | /// 199 | /// 200 | /// 201 | /// 202 | public static void SendSpecialKeys(this DriverManager driver, string elementId, List keys) 203 | { 204 | var stringToSend = new StringBuilder(); 205 | foreach (var key in keys) 206 | { 207 | if(key == WebDriverKey.Wait) 208 | { 209 | System.Threading.Thread.Sleep(1000); 210 | driver.SendKeys(elementId, stringToSend.ToString()); 211 | stringToSend = new StringBuilder(); 212 | } 213 | else 214 | { 215 | stringToSend.Append(GetWebDriverKeyString(key)); 216 | } 217 | } 218 | driver.SendKeys(elementId, stringToSend.ToString()); 219 | } 220 | 221 | public static void SendSpecialKeys(this DriverManager driver, string elementId, WebDriverKey key) 222 | { 223 | driver.SendSpecialKeys(elementId, new List { key }); 224 | } 225 | 226 | /// 227 | /// Get all of the Control Patterns supported by an element 228 | /// 229 | /// The element being extended 230 | /// The ids of the patterns 231 | /// A list of all the patterns supported 232 | public static List GetPatterns(this IUIAutomationElement element, out List ids) 233 | { 234 | int[] inIds; 235 | string[] names; 236 | new CUIAutomation8().PollForPotentialSupportedPatterns(element, out inIds, out names); 237 | ids = inIds.ToList(); 238 | return names.ToList(); 239 | } 240 | 241 | /// 242 | /// Get all of the Control Patterns supported by an element 243 | /// 244 | /// The element being extended 245 | /// A list of all the patterns supported 246 | public static List GetPatterns(this IUIAutomationElement element) 247 | { 248 | var ids = new List(); 249 | return GetPatterns(element, out ids); 250 | } 251 | 252 | /// 253 | /// Get all the properties supported by an element 254 | /// 255 | /// The element being extended 256 | /// A list of all the properties supported 257 | public static List GetProperties(this IUIAutomationElement element) 258 | { 259 | int[] ids; 260 | string[] names; 261 | new CUIAutomation8().PollForPotentialSupportedProperties(element, out ids, out names); 262 | return names.ToList(); 263 | } 264 | 265 | /// 266 | /// Get the names of all children (not all descendents) 267 | /// 268 | /// The element being extended 269 | /// A list of all the children's names 270 | public static List GetChildNames(this IUIAutomationElement element, Func searchStrategy = null) 271 | { 272 | var toReturn = new List(); 273 | var walker = new CUIAutomation8().RawViewWalker; 274 | for (var child = walker.GetFirstChildElement(element); child != null; child = walker.GetNextSiblingElement(child)) 275 | { 276 | if (searchStrategy == null || searchStrategy(child)) 277 | { 278 | toReturn.Add(child.CurrentName); 279 | } 280 | } 281 | 282 | return toReturn; 283 | } 284 | 285 | /// 286 | /// Find all descendents of a given element, optionally searching with the given strategy 287 | /// 288 | /// The element whose descendents we need to find 289 | /// The strategy we should use to evaluate children 290 | /// All elements that pass the searchStrategy 291 | public static List GetAllDescendents(this IUIAutomationElement element, Func searchStrategy = null) 292 | { 293 | var toReturn = new List(); 294 | var walker = new CUIAutomation8().RawViewWalker; 295 | 296 | var toSearch = new Queue();//BFS 297 | toSearch.Enqueue(element); 298 | 299 | while (toSearch.Any())//beware infinite recursion. If this becomes a problem add a limit 300 | { 301 | var current = toSearch.Dequeue(); 302 | for (var child = walker.GetFirstChildElement(current); child != null; child = walker.GetNextSiblingElement(child)) 303 | { 304 | toSearch.Enqueue(child); 305 | } 306 | 307 | if (searchStrategy == null || searchStrategy(current)) 308 | { 309 | toReturn.Add(current); 310 | } 311 | } 312 | 313 | return toReturn; 314 | } 315 | 316 | /// 317 | /// Parse the mystery object that is returned from WebDriver ExecuteScript calls. 318 | /// 319 | public static double ParseMystery(this object o) 320 | { 321 | try 322 | { 323 | return (int)o; 324 | } 325 | catch { } 326 | 327 | try 328 | { 329 | return (Int64)o; 330 | } 331 | catch { } 332 | 333 | try 334 | { 335 | return (double)o; 336 | } 337 | catch { } 338 | 339 | try 340 | { 341 | return double.Parse((string)o); 342 | } 343 | catch { } 344 | 345 | throw new InvalidCastException("Cannot parse to int, double, or string"); 346 | } 347 | 348 | /// 349 | /// Takes a screenshot and saves in the current directory 350 | /// 351 | /// 352 | /// 353 | public static void Screenshot(this DriverManager driver, string name) 354 | { 355 | driver.GetScreenshot().SaveAsFile(Path.Combine(Directory.GetCurrentDirectory(), name + ".png"), ImageFormat.Png); 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /EdgeStrategy.cs: -------------------------------------------------------------------------------- 1 | using Interop.UIAutomationCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using static Microsoft.Edge.A11y.ElementConverter; 7 | 8 | namespace Microsoft.Edge.A11y 9 | { 10 | /// 11 | /// A strategy for testing Edge Accessibility as scored at 12 | /// http://html5accessibility.com/ 13 | /// 14 | internal class EdgeStrategy : TestStrategy 15 | { 16 | public EdgeStrategy(string repositoryPath = "https://cdn.rawgit.com/DHBrett/AT-browser-tests/gh-pages/test-files/", string fileSuffix = "") 17 | { 18 | _driverManager = new DriverManager(TimeSpan.FromSeconds(10)); 19 | System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));//Wait for the browser to load before we start searching 20 | _RepositoryPath = repositoryPath; 21 | _FileSuffix = fileSuffix; 22 | } 23 | 24 | /// 25 | /// This handles most of the work of the test cases. 26 | /// 27 | /// N.B. all the test case results are returned in pairs, since we need to be 28 | /// able to give half scores for certain results. 29 | /// 30 | /// An object which stores information about the 31 | /// expected results 32 | /// 33 | /// 34 | internal override IEnumerable TestElement(TestData testData) 35 | { 36 | //Find the browser 37 | var browserElement = EdgeA11yTools.FindBrowserDocument(0); 38 | if (browserElement == null) 39 | { 40 | return Fail(testData.TestName, "Unable to find the browser"); 41 | } 42 | 43 | //Find elements using ControlType or the alternate search strategy 44 | HashSet foundControlTypes; 45 | var testElements = EdgeA11yTools.SearchChildren(browserElement, testData.ControlType, testData.SearchStrategy, out foundControlTypes); 46 | if (testElements.Count == 0) 47 | { 48 | return Fail(testData.TestName, testData.SearchStrategy == null ? 49 | "Unable to find the element, found these instead: " + foundControlTypes.Select(ct => ct.ToString()).Aggregate((a, b) => a + ", " + b) : 50 | "Unable to find the element using the alternate search strategy"); 51 | } 52 | 53 | var moreInfo = new StringBuilder(); 54 | 55 | //If necessary, check localized control type 56 | if (testData.LocalizedControlType != null) 57 | { 58 | foreach (var element in testElements) 59 | { 60 | if (!element.CurrentLocalizedControlType.Equals(testData.LocalizedControlType, StringComparison.OrdinalIgnoreCase)) 61 | { 62 | var error = "\nElement did not have the correct localized control type. Expected:" + 63 | testData.LocalizedControlType + " Actual:" + element.CurrentLocalizedControlType; 64 | moreInfo.Append(error); 65 | } 66 | } 67 | } 68 | 69 | //If necessary, check landmark and localized landmark types 70 | if (testData.LandmarkType != UIALandmarkType.Unknown) 71 | { 72 | foreach (var element in testElements) 73 | { 74 | var five = element as IUIAutomationElement5; 75 | var convertedLandmark = GetLandmarkTypeFromCode(five.CurrentLandmarkType); 76 | var localizedLandmark = five.CurrentLocalizedLandmarkType; 77 | 78 | if (convertedLandmark != testData.LandmarkType) 79 | { 80 | var error = "\nElement did not have the correct landmark type. Expected:" + 81 | testData.LandmarkType + " Actual:" + convertedLandmark + "\n"; 82 | moreInfo.Append(error); 83 | } 84 | 85 | if (localizedLandmark != testData.LocalizedLandmarkType) 86 | { 87 | var error = "\nElement did not have the correct localized landmark type. Expected:" + 88 | testData.LocalizedLandmarkType + " Actual:" + localizedLandmark + "\n"; 89 | moreInfo.Append(error); 90 | } 91 | } 92 | } 93 | 94 | //If necessary, naming and descriptions 95 | //This is done "out of order" since the keyboard checks below invalidate the tree 96 | if (testData.RequiredNames != null || testData.RequiredDescriptions != null) 97 | { 98 | moreInfo.Append(CheckElementNames(testElements, 99 | testData.RequiredNames ?? new List(), 100 | testData.RequiredDescriptions ?? new List())); 101 | } 102 | 103 | //If necessary, check keboard accessibility 104 | var tabbable = EdgeA11yTools.TabbableIds(_driverManager); 105 | if (testData.KeyboardElements != null && testData.KeyboardElements.Count > 0) 106 | { 107 | foreach (var e in testData.KeyboardElements) 108 | { 109 | if (!tabbable.Contains(e)) 110 | { 111 | moreInfo.Append("\nCould not access element with id: '" + e + "' by tab"); 112 | } 113 | } 114 | } 115 | 116 | try 117 | { 118 | //If necessary, check any additional requirements 119 | if (testData.AdditionalRequirement != null) 120 | { 121 | testElements = EdgeA11yTools.SearchChildren(browserElement, testData.ControlType, testData.SearchStrategy, out foundControlTypes); 122 | var additionalRequirementResult = testData.AdditionalRequirement(testElements, _driverManager, tabbable); 123 | if (additionalRequirementResult.Result != ResultType.Pass) 124 | { 125 | moreInfo.AppendLine(additionalRequirementResult.MoreInfo); 126 | } 127 | } 128 | } 129 | catch (Exception ex) 130 | { 131 | moreInfo.Append("\nCaught exception during test execution, ERROR: " + ex.Message + "\nCallStack:\n" + ex.StackTrace); 132 | } 133 | 134 | var moreInfoString = moreInfo.ToString(); 135 | if (moreInfoString != "") 136 | { 137 | return Half(testData.TestName, moreInfoString.Trim()); 138 | } 139 | 140 | return Pass(testData.TestName); 141 | } 142 | 143 | /// 144 | /// Check all the elements for correct naming and descriptions 145 | /// 146 | /// 147 | /// 148 | /// 149 | /// 150 | public static string CheckElementNames(List elements, List requiredNames, List requiredDescriptions) 151 | { 152 | var names = elements.ConvertAll(element => element.CurrentName).Where(e => !string.IsNullOrEmpty(e)).ToList(); 153 | var descriptions = elements.ConvertAll(element => ((IUIAutomationElement6)element).CurrentFullDescription).Where(e => !string.IsNullOrEmpty(e)).ToList(); 154 | var result = ""; 155 | 156 | //Check names 157 | var expectedNotFound = requiredNames.Where(rn => !names.Contains(rn)).ToList();//get a list of all required names not found 158 | var foundNotExpected = names.Where(n => !requiredNames.Contains(n)).ToList();//get a list of all found names that weren't required 159 | result += 160 | expectedNotFound.Any() ? "\n" + 161 | expectedNotFound.Aggregate((a, b) => a + ", " + b) + 162 | (expectedNotFound.Count() > 1 ? 163 | " were expected as names but not found. " : 164 | " was expected as a name but not found. ") 165 | : ""; 166 | result += 167 | foundNotExpected.Any() ? "\n" + 168 | foundNotExpected.Aggregate((a, b) => a + ", " + b) + 169 | (foundNotExpected.Count() > 1 ? 170 | " were found as names but not expected. " : 171 | " was found as a name but not expected. ") 172 | : ""; 173 | 174 | //Check descriptions 175 | expectedNotFound = requiredDescriptions.Where(rd => !descriptions.Contains(rd)).ToList(); 176 | foundNotExpected = descriptions.Where(d => !requiredDescriptions.Contains(d)).ToList(); 177 | result += 178 | expectedNotFound.Any() ? "\n" + 179 | expectedNotFound.Aggregate((a, b) => a + ", " + b) + 180 | (expectedNotFound.Count() > 1 ? 181 | " were expected as descriptions but not found. " : 182 | " was expected as a description but not found. ") 183 | : ""; 184 | result += 185 | foundNotExpected.Any() ? "\n" + 186 | foundNotExpected.Aggregate((a, b) => a + ", " + b) + 187 | (foundNotExpected.Count() > 1 ? 188 | " were found as descriptions but not expected. " : 189 | " was found as a description but not expected. ") 190 | : ""; 191 | 192 | return result; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /ElementConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Microsoft.Edge.A11y 6 | { 7 | /// 8 | /// A tool to convert element names into codes and back again 9 | /// 10 | public static class ElementConverter 11 | { 12 | private static Dictionary _ControlTypeMapping; 13 | private static Dictionary _PropertyMapping; 14 | private static Dictionary _LandmarkTypeMapping; 15 | private static Dictionary _WebDriverKeyMapping; 16 | 17 | private static void InitControlTypeMapping() 18 | { 19 | var ControlTypeMapping = new Dictionary(); 20 | ControlTypeMapping.Add(-1, UIAControlType.Unknown); 21 | ControlTypeMapping.Add(50000, UIAControlType.Button); 22 | ControlTypeMapping.Add(50001, UIAControlType.Calendar); 23 | ControlTypeMapping.Add(50002, UIAControlType.Checkbox); 24 | ControlTypeMapping.Add(50003, UIAControlType.Combobox); 25 | ControlTypeMapping.Add(50004, UIAControlType.Edit); 26 | ControlTypeMapping.Add(50005, UIAControlType.Hyperlink); 27 | ControlTypeMapping.Add(50006, UIAControlType.Image); 28 | ControlTypeMapping.Add(50007, UIAControlType.Listitem); 29 | ControlTypeMapping.Add(50008, UIAControlType.List); 30 | ControlTypeMapping.Add(50009, UIAControlType.Menu); 31 | ControlTypeMapping.Add(50010, UIAControlType.Menubar); 32 | ControlTypeMapping.Add(50011, UIAControlType.Menuitem); 33 | ControlTypeMapping.Add(50012, UIAControlType.Progressbar); 34 | ControlTypeMapping.Add(50013, UIAControlType.Radiobutton); 35 | ControlTypeMapping.Add(50014, UIAControlType.Scrollbar); 36 | ControlTypeMapping.Add(50015, UIAControlType.Slider); 37 | ControlTypeMapping.Add(50016, UIAControlType.Spinner); 38 | ControlTypeMapping.Add(50017, UIAControlType.Statusbar); 39 | ControlTypeMapping.Add(50018, UIAControlType.Tab); 40 | ControlTypeMapping.Add(50019, UIAControlType.Tabitem); 41 | ControlTypeMapping.Add(50020, UIAControlType.Text); 42 | ControlTypeMapping.Add(50021, UIAControlType.Toolbar); 43 | ControlTypeMapping.Add(50022, UIAControlType.Tooltip); 44 | ControlTypeMapping.Add(50023, UIAControlType.Tree); 45 | ControlTypeMapping.Add(50024, UIAControlType.Treeitem); 46 | ControlTypeMapping.Add(50025, UIAControlType.Custom); 47 | ControlTypeMapping.Add(50026, UIAControlType.Group); 48 | ControlTypeMapping.Add(50027, UIAControlType.Thumb); 49 | ControlTypeMapping.Add(50028, UIAControlType.Datagrid); 50 | ControlTypeMapping.Add(50029, UIAControlType.Dataitem); 51 | ControlTypeMapping.Add(50030, UIAControlType.Document); 52 | ControlTypeMapping.Add(50031, UIAControlType.Splitbutton); 53 | ControlTypeMapping.Add(50032, UIAControlType.Window); 54 | ControlTypeMapping.Add(50033, UIAControlType.Pane); 55 | ControlTypeMapping.Add(50034, UIAControlType.Header); 56 | ControlTypeMapping.Add(50035, UIAControlType.Headeritem); 57 | ControlTypeMapping.Add(50036, UIAControlType.Table); 58 | ControlTypeMapping.Add(50037, UIAControlType.Titlebar); 59 | ControlTypeMapping.Add(50038, UIAControlType.Separator); 60 | ControlTypeMapping.Add(50039, UIAControlType.Semanticzoom); 61 | ControlTypeMapping.Add(50040, UIAControlType.Appbar); 62 | 63 | _ControlTypeMapping = ControlTypeMapping; 64 | } 65 | 66 | private static void InitPropertyMapping() 67 | { 68 | var PropertyMapping = new Dictionary(); 69 | PropertyMapping.Add(-1, UIAProperty.Unknown); 70 | PropertyMapping.Add(30000, UIAProperty.RuntimeId); 71 | PropertyMapping.Add(30001, UIAProperty.BoundingRectangle); 72 | PropertyMapping.Add(30002, UIAProperty.ProcessId); 73 | PropertyMapping.Add(30003, UIAProperty.ControlType); 74 | PropertyMapping.Add(30004, UIAProperty.LocalizedControlType); 75 | PropertyMapping.Add(30005, UIAProperty.Name); 76 | PropertyMapping.Add(30006, UIAProperty.AcceleratorKey); 77 | PropertyMapping.Add(30007, UIAProperty.AccessKey); 78 | PropertyMapping.Add(30008, UIAProperty.HasKeyboardFocus); 79 | PropertyMapping.Add(30009, UIAProperty.IsKeyboardFocusable); 80 | PropertyMapping.Add(30010, UIAProperty.IsEnabled); 81 | PropertyMapping.Add(30011, UIAProperty.AutomationId); 82 | PropertyMapping.Add(30012, UIAProperty.ClassName); 83 | PropertyMapping.Add(30013, UIAProperty.HelpText); 84 | PropertyMapping.Add(30014, UIAProperty.ClickablePoint); 85 | PropertyMapping.Add(30015, UIAProperty.Culture); 86 | PropertyMapping.Add(30016, UIAProperty.IsControlElement); 87 | PropertyMapping.Add(30017, UIAProperty.IsContentElement); 88 | PropertyMapping.Add(30018, UIAProperty.LabeledBy); 89 | PropertyMapping.Add(30019, UIAProperty.IsPassword); 90 | PropertyMapping.Add(30020, UIAProperty.NativeWindowHandle); 91 | PropertyMapping.Add(30021, UIAProperty.ItemType); 92 | PropertyMapping.Add(30022, UIAProperty.IsOffscreen); 93 | PropertyMapping.Add(30023, UIAProperty.Orientation); 94 | PropertyMapping.Add(30024, UIAProperty.FrameworkId); 95 | PropertyMapping.Add(30025, UIAProperty.IsRequiredForForm); 96 | PropertyMapping.Add(30026, UIAProperty.ItemStatus); 97 | PropertyMapping.Add(30027, UIAProperty.IsDockPatternAvailable); 98 | PropertyMapping.Add(30028, UIAProperty.IsExpandCollapsePatternAvailable); 99 | PropertyMapping.Add(30029, UIAProperty.IsGridItemPatternAvailable); 100 | PropertyMapping.Add(30030, UIAProperty.IsGridPatternAvailable); 101 | PropertyMapping.Add(30031, UIAProperty.IsInvokePatternAvailable); 102 | PropertyMapping.Add(30032, UIAProperty.IsMultipleViewPatternAvailable); 103 | PropertyMapping.Add(30033, UIAProperty.IsRangeValuePatternAvailable); 104 | PropertyMapping.Add(30034, UIAProperty.IsScrollPatternAvailable); 105 | PropertyMapping.Add(30035, UIAProperty.IsScrollItemPatternAvailable); 106 | PropertyMapping.Add(30036, UIAProperty.IsSelectionItemPatternAvailable); 107 | PropertyMapping.Add(30037, UIAProperty.IsSelectionPatternAvailable); 108 | PropertyMapping.Add(30038, UIAProperty.IsTablePatternAvailable); 109 | PropertyMapping.Add(30039, UIAProperty.IsTableItemPatternAvailable); 110 | PropertyMapping.Add(30040, UIAProperty.IsTextPatternAvailable); 111 | PropertyMapping.Add(30041, UIAProperty.IsTogglePatternAvailable); 112 | PropertyMapping.Add(30042, UIAProperty.IsTransformPatternAvailable); 113 | PropertyMapping.Add(30043, UIAProperty.IsValuePatternAvailable); 114 | PropertyMapping.Add(30044, UIAProperty.IsWindowPatternAvailable); 115 | PropertyMapping.Add(30045, UIAProperty.ValueValue); 116 | PropertyMapping.Add(30046, UIAProperty.ValueIsReadOnly); 117 | PropertyMapping.Add(30047, UIAProperty.RangeValueValue); 118 | PropertyMapping.Add(30048, UIAProperty.RangeValueIsReadOnly); 119 | PropertyMapping.Add(30049, UIAProperty.RangeValueMinimum); 120 | PropertyMapping.Add(30050, UIAProperty.RangeValueMaximum); 121 | PropertyMapping.Add(30051, UIAProperty.RangeValueLargeChange); 122 | PropertyMapping.Add(30052, UIAProperty.RangeValueSmallChange); 123 | PropertyMapping.Add(30053, UIAProperty.ScrollHorizontalScrollPercent); 124 | PropertyMapping.Add(30054, UIAProperty.ScrollHorizontalViewSize); 125 | PropertyMapping.Add(30055, UIAProperty.ScrollVerticalScrollPercent); 126 | PropertyMapping.Add(30056, UIAProperty.ScrollVerticalViewSize); 127 | PropertyMapping.Add(30057, UIAProperty.ScrollHorizontallyScrollable); 128 | PropertyMapping.Add(30058, UIAProperty.ScrollVerticallyScrollable); 129 | PropertyMapping.Add(30059, UIAProperty.SelectionSelection); 130 | PropertyMapping.Add(30060, UIAProperty.SelectionCanSelectMultiple); 131 | PropertyMapping.Add(30061, UIAProperty.SelectionIsSelectionRequired); 132 | PropertyMapping.Add(30062, UIAProperty.GridRowCount); 133 | PropertyMapping.Add(30063, UIAProperty.GridColumnCount); 134 | PropertyMapping.Add(30064, UIAProperty.GridItemRow); 135 | PropertyMapping.Add(30065, UIAProperty.GridItemColumn); 136 | PropertyMapping.Add(30066, UIAProperty.GridItemRowSpan); 137 | PropertyMapping.Add(30067, UIAProperty.GridItemColumnSpan); 138 | PropertyMapping.Add(30068, UIAProperty.GridItemContainingGrid); 139 | PropertyMapping.Add(30069, UIAProperty.DockDockPosition); 140 | PropertyMapping.Add(30070, UIAProperty.ExpandCollapseExpandCollapseState); 141 | PropertyMapping.Add(30071, UIAProperty.MultipleViewCurrentView); 142 | PropertyMapping.Add(30072, UIAProperty.MultipleViewSupportedViews); 143 | PropertyMapping.Add(30073, UIAProperty.WindowCanMaximize); 144 | PropertyMapping.Add(30074, UIAProperty.WindowCanMinimize); 145 | PropertyMapping.Add(30075, UIAProperty.WindowWindowVisualState); 146 | PropertyMapping.Add(30076, UIAProperty.WindowWindowInteractionState); 147 | PropertyMapping.Add(30077, UIAProperty.WindowIsModal); 148 | PropertyMapping.Add(30078, UIAProperty.WindowIsTopmost); 149 | PropertyMapping.Add(30079, UIAProperty.SelectionItemIsSelected); 150 | PropertyMapping.Add(30080, UIAProperty.SelectionItemSelectionContainer); 151 | PropertyMapping.Add(30081, UIAProperty.TableRowHeaders); 152 | PropertyMapping.Add(30082, UIAProperty.TableColumnHeaders); 153 | PropertyMapping.Add(30083, UIAProperty.TableRowOrColumnMajor); 154 | PropertyMapping.Add(30084, UIAProperty.TableItemRowHeaderItems); 155 | PropertyMapping.Add(30085, UIAProperty.TableItemColumnHeaderItems); 156 | PropertyMapping.Add(30086, UIAProperty.ToggleToggleState); 157 | PropertyMapping.Add(30087, UIAProperty.TransformCanMove); 158 | PropertyMapping.Add(30088, UIAProperty.TransformCanResize); 159 | PropertyMapping.Add(30089, UIAProperty.TransformCanRotate); 160 | PropertyMapping.Add(30090, UIAProperty.IsLegacyIAccessiblePatternAvailable); 161 | PropertyMapping.Add(30091, UIAProperty.LegacyIAccessibleChildId); 162 | PropertyMapping.Add(30092, UIAProperty.LegacyIAccessibleName); 163 | PropertyMapping.Add(30093, UIAProperty.LegacyIAccessibleValue); 164 | PropertyMapping.Add(30094, UIAProperty.LegacyIAccessibleDescription); 165 | PropertyMapping.Add(30095, UIAProperty.LegacyIAccessibleRole); 166 | PropertyMapping.Add(30096, UIAProperty.LegacyIAccessibleState); 167 | PropertyMapping.Add(30097, UIAProperty.LegacyIAccessibleHelp); 168 | PropertyMapping.Add(30098, UIAProperty.LegacyIAccessibleKeyboardShortcut); 169 | PropertyMapping.Add(30099, UIAProperty.LegacyIAccessibleSelection); 170 | PropertyMapping.Add(30100, UIAProperty.LegacyIAccessibleDefaultAction); 171 | PropertyMapping.Add(30101, UIAProperty.AriaRole); 172 | PropertyMapping.Add(30102, UIAProperty.AriaProperties); 173 | PropertyMapping.Add(30103, UIAProperty.IsDataValidForForm); 174 | PropertyMapping.Add(30104, UIAProperty.ControllerFor); 175 | PropertyMapping.Add(30105, UIAProperty.DescribedBy); 176 | PropertyMapping.Add(30106, UIAProperty.FlowsTo); 177 | PropertyMapping.Add(30107, UIAProperty.ProviderDescription); 178 | PropertyMapping.Add(30108, UIAProperty.IsItemContainerPatternAvailable); 179 | PropertyMapping.Add(30109, UIAProperty.IsVirtualizedItemPatternAvailable); 180 | PropertyMapping.Add(30110, UIAProperty.IsSynchronizedInputPatternAvailable); 181 | 182 | _PropertyMapping = PropertyMapping; 183 | } 184 | 185 | private static void InitLandmarkTypeMapping() 186 | { 187 | var LandmarkTypeMapping = new Dictionary(); 188 | LandmarkTypeMapping.Add(-1, UIALandmarkType.Unknown); 189 | LandmarkTypeMapping.Add(80000, UIALandmarkType.Custom); 190 | LandmarkTypeMapping.Add(80001, UIALandmarkType.Form); 191 | LandmarkTypeMapping.Add(80002, UIALandmarkType.Main); 192 | LandmarkTypeMapping.Add(80003, UIALandmarkType.Navigation); 193 | LandmarkTypeMapping.Add(80004, UIALandmarkType.Search); 194 | _LandmarkTypeMapping = LandmarkTypeMapping; 195 | } 196 | 197 | private static void InitWebDriverKeyMapping() 198 | { 199 | var WebDriverKeyMapping = new Dictionary(); 200 | 201 | WebDriverKeyMapping.Add(WebDriverKey.Wait, null); 202 | WebDriverKeyMapping.Add(WebDriverKey.Null, '\uE000'.ToString()); 203 | WebDriverKeyMapping.Add(WebDriverKey.Cancel, '\uE001'.ToString()); 204 | WebDriverKeyMapping.Add(WebDriverKey.Help, '\uE002'.ToString()); 205 | WebDriverKeyMapping.Add(WebDriverKey.Back_space, '\uE003'.ToString()); 206 | WebDriverKeyMapping.Add(WebDriverKey.Tab, '\uE004'.ToString()); 207 | WebDriverKeyMapping.Add(WebDriverKey.Clear, '\uE005'.ToString()); 208 | WebDriverKeyMapping.Add(WebDriverKey.Return, '\uE006'.ToString()); 209 | WebDriverKeyMapping.Add(WebDriverKey.Enter, '\uE007'.ToString()); 210 | WebDriverKeyMapping.Add(WebDriverKey.Shift, '\uE008'.ToString()); 211 | WebDriverKeyMapping.Add(WebDriverKey.Control, '\uE009'.ToString()); 212 | WebDriverKeyMapping.Add(WebDriverKey.Alt, '\uE00A'.ToString()); 213 | WebDriverKeyMapping.Add(WebDriverKey.Pause, '\uE00B'.ToString()); 214 | WebDriverKeyMapping.Add(WebDriverKey.Escape, '\uE00C'.ToString()); 215 | WebDriverKeyMapping.Add(WebDriverKey.Space, '\uE00D'.ToString()); 216 | WebDriverKeyMapping.Add(WebDriverKey.Page_up, '\uE00E'.ToString()); 217 | WebDriverKeyMapping.Add(WebDriverKey.Page_down, '\uE00F'.ToString()); 218 | WebDriverKeyMapping.Add(WebDriverKey.End, '\uE010'.ToString()); 219 | WebDriverKeyMapping.Add(WebDriverKey.Home, '\uE011'.ToString()); 220 | WebDriverKeyMapping.Add(WebDriverKey.Arrow_left, '\uE012'.ToString()); 221 | WebDriverKeyMapping.Add(WebDriverKey.Arrow_up, '\uE013'.ToString()); 222 | WebDriverKeyMapping.Add(WebDriverKey.Arrow_right, '\uE014'.ToString()); 223 | WebDriverKeyMapping.Add(WebDriverKey.Arrow_down, '\uE015'.ToString()); 224 | WebDriverKeyMapping.Add(WebDriverKey.Insert, '\uE016'.ToString()); 225 | WebDriverKeyMapping.Add(WebDriverKey.Delete, '\uE017'.ToString()); 226 | WebDriverKeyMapping.Add(WebDriverKey.Semicolon, '\uE018'.ToString()); 227 | WebDriverKeyMapping.Add(WebDriverKey.Equals, '\uE019'.ToString()); 228 | WebDriverKeyMapping.Add(WebDriverKey.Numpad0, '\uE01A'.ToString()); 229 | WebDriverKeyMapping.Add(WebDriverKey.Numpad1, '\uE01B'.ToString()); 230 | WebDriverKeyMapping.Add(WebDriverKey.Numpad2, '\uE01C'.ToString()); 231 | WebDriverKeyMapping.Add(WebDriverKey.Numpad3, '\uE01D'.ToString()); 232 | WebDriverKeyMapping.Add(WebDriverKey.Numpad4, '\uE01E'.ToString()); 233 | WebDriverKeyMapping.Add(WebDriverKey.Numpad5, '\uE01F'.ToString()); 234 | WebDriverKeyMapping.Add(WebDriverKey.Numpad6, '\uE020'.ToString()); 235 | WebDriverKeyMapping.Add(WebDriverKey.Numpad7, '\uE021'.ToString()); 236 | WebDriverKeyMapping.Add(WebDriverKey.Numpad8, '\uE022'.ToString()); 237 | WebDriverKeyMapping.Add(WebDriverKey.Numpad9, '\uE023'.ToString()); 238 | WebDriverKeyMapping.Add(WebDriverKey.Multiply, '\uE024'.ToString()); 239 | WebDriverKeyMapping.Add(WebDriverKey.Add, '\uE025'.ToString()); 240 | WebDriverKeyMapping.Add(WebDriverKey.Separator, '\uE026'.ToString()); 241 | WebDriverKeyMapping.Add(WebDriverKey.Subtract, '\uE027'.ToString()); 242 | WebDriverKeyMapping.Add(WebDriverKey.Decimal, '\uE028'.ToString()); 243 | WebDriverKeyMapping.Add(WebDriverKey.Divide, '\uE029'.ToString()); 244 | WebDriverKeyMapping.Add(WebDriverKey.F1, '\uE031'.ToString()); 245 | WebDriverKeyMapping.Add(WebDriverKey.F2, '\uE032'.ToString()); 246 | WebDriverKeyMapping.Add(WebDriverKey.F3, '\uE033'.ToString()); 247 | WebDriverKeyMapping.Add(WebDriverKey.F4, '\uE034'.ToString()); 248 | WebDriverKeyMapping.Add(WebDriverKey.F5, '\uE035'.ToString()); 249 | WebDriverKeyMapping.Add(WebDriverKey.F6, '\uE036'.ToString()); 250 | WebDriverKeyMapping.Add(WebDriverKey.F7, '\uE037'.ToString()); 251 | WebDriverKeyMapping.Add(WebDriverKey.F8, '\uE038'.ToString()); 252 | WebDriverKeyMapping.Add(WebDriverKey.F9, '\uE039'.ToString()); 253 | WebDriverKeyMapping.Add(WebDriverKey.F10, '\uE03A'.ToString()); 254 | WebDriverKeyMapping.Add(WebDriverKey.F11, '\uE03B'.ToString()); 255 | WebDriverKeyMapping.Add(WebDriverKey.F12, '\uE03C'.ToString()); 256 | WebDriverKeyMapping.Add(WebDriverKey.Meta, '\uE03D'.ToString()); 257 | WebDriverKeyMapping.Add(WebDriverKey.Command, '\uE03D'.ToString()); 258 | WebDriverKeyMapping.Add(WebDriverKey.Zenkaku_hankaku, '\uE040'.ToString()); 259 | 260 | _WebDriverKeyMapping = WebDriverKeyMapping; 261 | } 262 | 263 | /// 264 | /// Convert a code into an element name 265 | /// 266 | /// 267 | /// 268 | public static UIAControlType GetControlTypeFromCode(int code) 269 | { 270 | if (_ControlTypeMapping == null) 271 | { 272 | InitControlTypeMapping(); 273 | } 274 | return _ControlTypeMapping.ContainsKey(code) ? _ControlTypeMapping[code] : UIAControlType.Unknown; 275 | } 276 | 277 | /// 278 | /// Convert a code into a property 279 | /// 280 | /// 281 | /// 282 | public static UIAProperty GetPropertyFromCode(int code) 283 | { 284 | if (_PropertyMapping == null) 285 | { 286 | InitPropertyMapping(); 287 | } 288 | return _PropertyMapping.ContainsKey(code) ? _PropertyMapping[code] : UIAProperty.Unknown; 289 | } 290 | 291 | public static int GetPropertyCode(UIAProperty property) 292 | { 293 | if (_PropertyMapping == null) 294 | { 295 | InitPropertyMapping(); 296 | } 297 | //will throw if given an invalid code 298 | return _PropertyMapping.Keys.First(k => _PropertyMapping[k] == property); 299 | } 300 | 301 | /// 302 | /// Convert a code into a landmark 303 | /// 304 | /// 305 | /// 306 | public static UIALandmarkType GetLandmarkTypeFromCode(int code) 307 | { 308 | if (_LandmarkTypeMapping == null) 309 | { 310 | InitLandmarkTypeMapping(); 311 | } 312 | return _LandmarkTypeMapping.ContainsKey(code) ? _LandmarkTypeMapping[code] : UIALandmarkType.Unknown; 313 | } 314 | 315 | /// 316 | /// Convert a WebDriverKey into its string representation 317 | /// 318 | /// 319 | /// 320 | public static string GetWebDriverKeyString(WebDriverKey key) 321 | { 322 | if (_WebDriverKeyMapping == null) 323 | { 324 | InitWebDriverKeyMapping(); 325 | } 326 | return _WebDriverKeyMapping[key]; 327 | } 328 | 329 | public enum UIAControlType 330 | { 331 | Unknown, 332 | Button, 333 | Calendar, 334 | Checkbox, 335 | Combobox, 336 | Edit, 337 | Hyperlink, 338 | Image, 339 | Listitem, 340 | List, 341 | Menu, 342 | Menubar, 343 | Menuitem, 344 | Progressbar, 345 | Radiobutton, 346 | Scrollbar, 347 | Slider, 348 | Spinner, 349 | Statusbar, 350 | Tab, 351 | Tabitem, 352 | Text, 353 | Toolbar, 354 | Tooltip, 355 | Tree, 356 | Treeitem, 357 | Custom, 358 | Group, 359 | Thumb, 360 | Datagrid, 361 | Dataitem, 362 | Document, 363 | Splitbutton, 364 | Window, 365 | Pane, 366 | Header, 367 | Headeritem, 368 | Table, 369 | Titlebar, 370 | Separator, 371 | Semanticzoom, 372 | Appbar, 373 | } 374 | 375 | public enum UIAProperty 376 | { 377 | Unknown, 378 | RuntimeId, 379 | BoundingRectangle, 380 | ProcessId, 381 | ControlType, 382 | LocalizedControlType, 383 | Name, 384 | AcceleratorKey, 385 | AccessKey, 386 | HasKeyboardFocus, 387 | IsKeyboardFocusable, 388 | IsEnabled, 389 | AutomationId, 390 | ClassName, 391 | HelpText, 392 | ClickablePoint, 393 | Culture, 394 | IsControlElement, 395 | IsContentElement, 396 | LabeledBy, 397 | IsPassword, 398 | NativeWindowHandle, 399 | ItemType, 400 | IsOffscreen, 401 | Orientation, 402 | FrameworkId, 403 | IsRequiredForForm, 404 | ItemStatus, 405 | IsDockPatternAvailable, 406 | IsExpandCollapsePatternAvailable, 407 | IsGridItemPatternAvailable, 408 | IsGridPatternAvailable, 409 | IsInvokePatternAvailable, 410 | IsMultipleViewPatternAvailable, 411 | IsRangeValuePatternAvailable, 412 | IsScrollPatternAvailable, 413 | IsScrollItemPatternAvailable, 414 | IsSelectionItemPatternAvailable, 415 | IsSelectionPatternAvailable, 416 | IsTablePatternAvailable, 417 | IsTableItemPatternAvailable, 418 | IsTextPatternAvailable, 419 | IsTogglePatternAvailable, 420 | IsTransformPatternAvailable, 421 | IsValuePatternAvailable, 422 | IsWindowPatternAvailable, 423 | ValueValue, 424 | ValueIsReadOnly, 425 | RangeValueValue, 426 | RangeValueIsReadOnly, 427 | RangeValueMinimum, 428 | RangeValueMaximum, 429 | RangeValueLargeChange, 430 | RangeValueSmallChange, 431 | ScrollHorizontalScrollPercent, 432 | ScrollHorizontalViewSize, 433 | ScrollVerticalScrollPercent, 434 | ScrollVerticalViewSize, 435 | ScrollHorizontallyScrollable, 436 | ScrollVerticallyScrollable, 437 | SelectionSelection, 438 | SelectionCanSelectMultiple, 439 | SelectionIsSelectionRequired, 440 | GridRowCount, 441 | GridColumnCount, 442 | GridItemRow, 443 | GridItemColumn, 444 | GridItemRowSpan, 445 | GridItemColumnSpan, 446 | GridItemContainingGrid, 447 | DockDockPosition, 448 | ExpandCollapseExpandCollapseState, 449 | MultipleViewCurrentView, 450 | MultipleViewSupportedViews, 451 | WindowCanMaximize, 452 | WindowCanMinimize, 453 | WindowWindowVisualState, 454 | WindowWindowInteractionState, 455 | WindowIsModal, 456 | WindowIsTopmost, 457 | SelectionItemIsSelected, 458 | SelectionItemSelectionContainer, 459 | TableRowHeaders, 460 | TableColumnHeaders, 461 | TableRowOrColumnMajor, 462 | TableItemRowHeaderItems, 463 | TableItemColumnHeaderItems, 464 | ToggleToggleState, 465 | TransformCanMove, 466 | TransformCanResize, 467 | TransformCanRotate, 468 | IsLegacyIAccessiblePatternAvailable, 469 | LegacyIAccessibleChildId, 470 | LegacyIAccessibleName, 471 | LegacyIAccessibleValue, 472 | LegacyIAccessibleDescription, 473 | LegacyIAccessibleRole, 474 | LegacyIAccessibleState, 475 | LegacyIAccessibleHelp, 476 | LegacyIAccessibleKeyboardShortcut, 477 | LegacyIAccessibleSelection, 478 | LegacyIAccessibleDefaultAction, 479 | AriaRole, 480 | AriaProperties, 481 | IsDataValidForForm, 482 | ControllerFor, 483 | DescribedBy, 484 | FlowsTo, 485 | ProviderDescription, 486 | IsItemContainerPatternAvailable, 487 | IsVirtualizedItemPatternAvailable, 488 | IsSynchronizedInputPatternAvailable, 489 | } 490 | 491 | public enum UIALandmarkType 492 | { 493 | Unknown, 494 | Custom, 495 | Form, 496 | Main, 497 | Navigation, 498 | Search 499 | } 500 | 501 | public enum WebDriverKey 502 | { 503 | Null, 504 | Cancel, 505 | Help, 506 | Back_space, 507 | Tab, 508 | Clear, 509 | Return, 510 | Enter, 511 | Shift, 512 | Control, 513 | Alt, 514 | Pause, 515 | Escape, 516 | Space, 517 | Page_up, 518 | Page_down, 519 | End, 520 | Home, 521 | Arrow_left, 522 | Arrow_up, 523 | Arrow_right, 524 | Arrow_down, 525 | Insert, 526 | Delete, 527 | Semicolon, 528 | Equals, 529 | Numpad0, 530 | Numpad1, 531 | Numpad2, 532 | Numpad3, 533 | Numpad4, 534 | Numpad5, 535 | Numpad6, 536 | Numpad7, 537 | Numpad8, 538 | Numpad9, 539 | Multiply, 540 | Add, 541 | Separator, 542 | Subtract, 543 | Decimal, 544 | Divide, 545 | F1, 546 | F2, 547 | F3, 548 | F4, 549 | F5, 550 | F6, 551 | F7, 552 | F8, 553 | F9, 554 | F10, 555 | F11, 556 | F12, 557 | Meta, 558 | Command, 559 | Zenkaku_hankaku, 560 | Wait 561 | } 562 | } 563 | } 564 | -------------------------------------------------------------------------------- /Interop.UIAutomationCore.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/A11y/926dcaedcb5c9f45f0d4e10ae09942882483c74e/Interop.UIAutomationCore.dll -------------------------------------------------------------------------------- /Javascript.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Edge.A11y 4 | { 5 | /// 6 | /// Used to store JavaScript that is passed to the page for testing 7 | /// 8 | static class Javascript 9 | { 10 | /// 11 | /// Modernizr feature detection for the track element 12 | /// 13 | public static string Track = "/*! modernizr 3.3.1 (Custom Build) | MIT * * http://modernizr.com/download/?-texttrackapi_track-setclasses !*/ !function(e,n,t){function a(e,n){return typeof e===n}function s(){var e,n,t,s,o,i,c;for(var f in l)if(l.hasOwnProperty(f)){if(e=[],n=l[f],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t 16 | /// Clear focus, so that we can start over in the tab order 17 | /// 18 | public static Action ClearFocus = (driverManager, timeout) => driverManager.ExecuteScript("document.activeElement.blur()", timeout); 19 | 20 | /// 21 | /// Scroll the element into view (for screenshot) 22 | /// 23 | public static Action ScrollIntoView = (driverManager, timeout) => driverManager.ExecuteScript("document.activeElement.scrollIntoView()", timeout); 24 | 25 | /// 26 | /// Remove hidden attribute from all buttons on the page 27 | /// 28 | public static string RemoveHidden = "var ps = document.getElementsByTagName('input'); for(i = 0; i < ps.length; i++){ ps[i].hidden = false}"; 29 | 30 | /// 31 | /// Change aria-hidden attribute to false for the second hidden element 32 | /// 33 | public static string RemoveAriaHidden = "document.getElementById('input2').setAttribute('aria-hidden', false)"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Microsoft.Win32; 6 | 7 | namespace Microsoft.Edge.A11y 8 | { 9 | class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | var testName = args.FirstOrDefault(); 14 | 15 | TestStrategy a11yStrategy = new EdgeStrategy(fileSuffix: ".html"); 16 | 17 | var results = TestData.alltests.Value.Where(td => 18 | (testName == null || td.TestName == testName)) //Either no test name was provided or the test names match 19 | .ToList().ConvertAll(td => a11yStrategy.Execute(td)) //Execute each of the tests 20 | .Where(r => r.Any()) //Only keep the ones that were executed 21 | .ToList().ConvertAll(r => //Convert results from internal form (Pass/Pass, Pass/Fail, Fail/Fail) to external (Pass, Half, Fail) 22 | { 23 | var first = r.ElementAt(0); 24 | var second = r.ElementAt(1); 25 | second.Result = second.Result == ResultType.Fail && first.Result == ResultType.Pass ? ResultType.Half : second.Result; 26 | second.Name = second.Name.Replace("-2", ""); 27 | return second; 28 | }); 29 | 30 | //output results to the console: failures, then halves, then passes 31 | results.OrderBy(r => r.Result == ResultType.Pass).ThenBy(r => r.Result == ResultType.Half).ToList().ForEach(r => Console.WriteLine(r)); 32 | 33 | if (results.Any()) 34 | { 35 | var score = results.ConvertAll(r => 36 | { 37 | switch (r.Result) 38 | { 39 | case ResultType.Fail: 40 | return 0; 41 | case ResultType.Half: 42 | return .5; 43 | case ResultType.Pass: 44 | return 1; 45 | default: 46 | throw new InvalidDataException(); 47 | } 48 | }).Average() * 100; 49 | 50 | Console.WriteLine("Edge Score: " + score); 51 | 52 | ResultsToCSV(results, score); 53 | } 54 | else 55 | { 56 | Console.WriteLine("No tests matched the name " + args[0]); 57 | } 58 | 59 | a11yStrategy.Close(); 60 | } 61 | 62 | public static void ResultsToCSV(List results, double score) 63 | { 64 | //Get the file 65 | var filePath = Path.Combine(DriverManager.ProjectRootFolder, "scores.csv"); 66 | 67 | //If this is the first time, write the header line with the test names 68 | if (!File.Exists(filePath)) 69 | { 70 | var headerLine = "buildNumber,buildIteration,buildArchitecture,buildBranch,buildDate,score,time," + 71 | results.Select(r => r.Name + "," + r.Name + "-details").Aggregate((s1, s2) => s1 + "," + s2) + "\n"; 72 | File.WriteAllText(filePath, headerLine); 73 | } 74 | 75 | var build = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "BuildLabEx", null); 76 | if (build == null || build as string == null) 77 | { 78 | throw new Exception("Unable to get build string"); 79 | } 80 | 81 | var time = DateTime.Now.ToString("yyyyMMdd-HHmm"); 82 | 83 | //Write the results 84 | var writer = File.AppendText(filePath); 85 | var resultline = (build as string).Replace('.', ',') + "," + score + "," + time + "," + 86 | results.Select(r => r.Result.ToString() + "," + (r.MoreInfo != null ? r.MoreInfo.Replace('\n', '\t') : "")) 87 | .Aggregate((s1, s2) => s1 + "," + s2); 88 | writer.WriteLine(resultline); 89 | 90 | writer.Flush(); 91 | writer.Close(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("A11y")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("A11y")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("18ca2a08-637b-48cd-81b6-6b7b08e5d441")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A11y Automation Test Suite 2 | An automated implementation of [html5accessibility.com](http://html5accessibility.com/) 3 | for Microsoft Edge. 4 | 5 | ## Running 6 | If you have Visual Studio, simply open A11y.sln and run. 7 | 8 | Otherwise you can build and run in one step by calling run.ps1 within PowerShell. 9 | 10 | ### Building and running manually 11 | It's also possible to run manually without Visual Studio or PowerShell. 12 | 13 | First navigate to the project's root directory and install nuget: 14 | ``` powershell "(new-object net.webclient).DownloadFile('https://nuget.org/nuget.exe', 'nuget.exe')" ``` 15 | 16 | Then restore the packages: 17 | ``` nuget.exe restore A11y.sln ``` 18 | 19 | Finally find MSBuild.exe, which comes pre-installed with the .NET framework. It will be 20 | in the C:\Windows\Microsoft.NET\Framework folder. 21 | For example: C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe. 22 | **Make sure to use the version installed on your computer.** 23 | 24 | Use MSBuild to compile the solution: ``` 25 | C:\Windows\Microsoft.NET\Framework\\MSBuild.exe A11y.sln ``` 26 | 27 | Then just run the compiled program: ``` bin\Debug\Microsoft.Edge.A11y.exe ``` 28 | 29 | ## Scores and reporting 30 | For a full explanation of how scores are calculated, see 31 | [html5accessibility.com](http://html5accessibility.com/). 32 | 33 | After the tests have run, the results are printed to the console and saved in the root 34 | directory of the project with the name "scores.csv". 35 | 36 | ## Testing your site 37 | It's possible to use A11y to automate testing of your site as well. A sample project is 38 | included on the site_testing branch. 39 | 40 | ### Test files 41 | The sample test page is included in this repo on the site_testing branch. It's possible 42 | (and better) to have your sites in another location. Just change the constructor call to 43 | to the TestStrategy class passing in the base URL where your test files are located. 44 | 45 | ### Pass and failure conditions 46 | The TestData.cs file contains the logic of the tests and an explanation of the built-in 47 | tests. To add your own tests, add a TestData object for each test you want to run. 48 | 49 | By default, elements are found by their control type, but you can pass in a custom method 50 | of searching by adding a searchStrategy parameter. 51 | 52 | In addition to the default tests, you can use the additionalRequirement paramter to 53 | specify any other requirement that you want to verify. If you find yourself using the 54 | same additionalRequirement for many of your tests, you may want to add another parameter 55 | to the TestData constructor to simplify testing that requirement. 56 | 57 | ## Contributing 58 | We want your feedback and your help! If you have any suggestions, file an issue and we 59 | can figure out how to get your needs met. 60 | 61 | If you'd like to submit code changes, the best thing to do is to file an issue first so 62 | we can talk about whether the change would fit with the direction and purpose of the 63 | project. Even if your changes don't fit with the general purpose of A11y, we'd love to 64 | see you fork the project to do new things with it. 65 | 66 | ## Legal 67 | 68 | You will need to complete a [Contributor License Agreement (CLA)](https://cla.microsoft.com/) before your pull request can be accepted. 69 | This agreement testifies that you are granting us permission to use the source code you are submitting, and that 70 | this work is being submitted under appropriate license that we can use it. 71 | The process is very simple as it just hooks into your Github account. Once we have received the signed CLA, we'll review the request. 72 | You will only need to do this once. 73 | 74 | ##Code of Conduct 75 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 76 | 77 | -------------------------------------------------------------------------------- /TestCaseResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Microsoft.Edge.A11y 4 | { 5 | public enum ResultType 6 | { 7 | Fail = 0, 8 | Pass = 1, 9 | Unknown = 2, 10 | Half = 3 11 | } 12 | 13 | public class TestCaseResult 14 | { 15 | public ResultType Result { get; set; } 16 | public string Name { get; set; } 17 | public string MoreInfo { get; set; } 18 | public string Browser { get; set; } 19 | 20 | public override string ToString() 21 | { 22 | return Name + ":\n\t" + Result + (MoreInfo != null ? " (" + MoreInfo + ")" : ""); 23 | } 24 | 25 | public string ToCSVString() 26 | { 27 | return Name + "," + Result + (MoreInfo != null ? "," + MoreInfo : ""); 28 | } 29 | } 30 | 31 | 32 | public class TestCaseResultExt : TestCaseResult 33 | { 34 | StringBuilder _MoreInfo = new StringBuilder(); 35 | 36 | new public string MoreInfo 37 | { 38 | get 39 | { 40 | return _MoreInfo.ToString(); 41 | } 42 | } 43 | 44 | public TestCaseResultExt() 45 | { 46 | Result = ResultType.Pass; 47 | } 48 | 49 | public void AddInfo(string info) 50 | { 51 | _MoreInfo.Append(info); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /TestData.cs: -------------------------------------------------------------------------------- 1 | using Interop.UIAutomationCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using static Microsoft.Edge.A11y.ElementConverter; 7 | 8 | namespace Microsoft.Edge.A11y 9 | { 10 | /// 11 | /// This is where the logic of the tests is stored 12 | /// 13 | class TestData 14 | { 15 | public const double epsilon = .001; 16 | 17 | /// 18 | /// The name of the test, which corresponds to the name of the html element 19 | /// 20 | private string _TestName; 21 | public string TestName 22 | { 23 | get 24 | { 25 | return _TestName; 26 | } 27 | } 28 | 29 | /// 30 | /// The name of the UIA control type we will use to search for the element 31 | /// 32 | private UIAControlType _ControlType; 33 | public UIAControlType ControlType 34 | { 35 | get 36 | { 37 | return _ControlType; 38 | } 39 | } 40 | 41 | /// 42 | /// The name of the UIA localized control type, which will be part of the test 43 | /// case if it is not null 44 | /// 45 | private string _LocalizedControlType; 46 | public string LocalizedControlType 47 | { 48 | get 49 | { 50 | return _LocalizedControlType; 51 | } 52 | } 53 | 54 | /// 55 | /// The name of the UIA landmark type, which will be part of the test 56 | /// case if it is not null 57 | /// 58 | private UIALandmarkType _LandmarkType; 59 | public UIALandmarkType LandmarkType 60 | { 61 | get 62 | { 63 | return _LandmarkType; 64 | } 65 | } 66 | 67 | /// 68 | /// The name of the UIA localized landmark type, which will be part of the test 69 | /// case if it is not null 70 | /// 71 | private string _LocalizedLandmarkType; 72 | public string LocalizedLandmarkType 73 | { 74 | get 75 | { 76 | return _LocalizedLandmarkType; 77 | } 78 | } 79 | 80 | /// 81 | /// A list of ids for all the elements that should be keyboard accessible (via tab) 82 | /// 83 | private List _KeyboardElements; 84 | public List KeyboardElements 85 | { 86 | get 87 | { 88 | return _KeyboardElements; 89 | } 90 | } 91 | 92 | /// 93 | /// If not null, this func will be used to test elements to see if they should be 94 | /// tested (instead of matching _ControlType). 95 | /// 96 | private Func _SearchStrategy; 97 | public Func SearchStrategy 98 | { 99 | get 100 | { 101 | return _SearchStrategy; 102 | } 103 | } 104 | 105 | /// 106 | /// A list of expected values which will be compared to the accessible names of 107 | /// the found elements. 108 | /// 109 | private List _requiredNames; 110 | public List RequiredNames 111 | { 112 | get 113 | { 114 | return _requiredNames; 115 | } 116 | } 117 | 118 | /// 119 | /// Same as above, but for accessible descriptions. 120 | /// 121 | private List _RequiredDescriptions; 122 | public List RequiredDescriptions 123 | { 124 | get 125 | { 126 | return _RequiredDescriptions; 127 | } 128 | } 129 | 130 | /// 131 | /// A func that allows extending the tests for specific elements. 132 | /// 133 | private Func, DriverManager, List, TestCaseResultExt> _AdditionalRequirement; 134 | public Func, DriverManager, List, TestCaseResultExt> AdditionalRequirement 135 | { 136 | get 137 | { 138 | return _AdditionalRequirement; 139 | } 140 | } 141 | 142 | /// 143 | /// Simple Ctor 144 | /// 145 | /// 146 | /// 147 | /// 148 | /// 149 | /// 150 | /// 151 | /// 152 | /// 153 | /// 154 | /// 155 | public TestData(string testName, 156 | UIAControlType controlType, 157 | string localizedControlType = null, 158 | UIALandmarkType landmarkType = UIALandmarkType.Unknown, 159 | string localizedLandmarkType = null, 160 | List keyboardElements = null, 161 | Func searchStrategy = null, 162 | List requiredNames = null, 163 | List requiredDescriptions = null, 164 | Func, DriverManager, List, TestCaseResultExt> additionalRequirement = null) 165 | { 166 | _TestName = testName; 167 | _ControlType = controlType; 168 | _LocalizedControlType = localizedControlType; 169 | _LandmarkType = landmarkType; 170 | _LocalizedLandmarkType = localizedLandmarkType; 171 | _KeyboardElements = keyboardElements; 172 | _SearchStrategy = searchStrategy; 173 | _requiredNames = requiredNames; 174 | _RequiredDescriptions = requiredDescriptions; 175 | _AdditionalRequirement = additionalRequirement; 176 | } 177 | 178 | //All the tests to run 179 | public static Lazy> alltests = new Lazy>(AllTests); 180 | 181 | /// 182 | /// Get the TestData for the given test page 183 | /// 184 | /// The name of the file being tested 185 | /// TestData for the given test page, or null if it couldn't be found 186 | public static TestData DataFromName(string testName) 187 | { 188 | return alltests.Value.FirstOrDefault(t => t._TestName == testName); 189 | } 190 | 191 | /// 192 | /// Singleton initializer 193 | /// 194 | /// 195 | static List AllTests() 196 | { 197 | const int timeout = 0; 198 | var uia = new CUIAutomation8(); 199 | var walker = uia.RawViewWalker; 200 | 201 | return new List{ 202 | new TestData("article", UIAControlType.Group, "article", 203 | requiredNames: 204 | new List{ 205 | "aria-label attribute 3", 206 | "h1 referenced by aria-labelledby4", 207 | "title attribute 5", 208 | "aria-label attribute 7"}, 209 | requiredDescriptions: 210 | new List{ 211 | "h1 referenced by aria-describedby6", 212 | "title attribute 7" 213 | }), 214 | new TestData("aside", UIAControlType.Group, "aside", UIALandmarkType.Custom, "complementary", 215 | requiredNames: 216 | new List{ 217 | "aria-label attribute 3", 218 | "h1 referenced by aria-labelledby4", 219 | "title attribute 5", 220 | "aria-label attribute 7"}, 221 | requiredDescriptions: 222 | new List{ 223 | "h1 referenced by aria-describedby6", 224 | "title attribute 7" 225 | }), 226 | new TestData("audio", UIAControlType.Group, "audio", 227 | additionalRequirement: ((elements, driver, ids) => { 228 | var result = new TestCaseResultExt(); 229 | 230 | CheckChildNames(new List { 231 | "Play", 232 | "Time elapsed", 233 | "Seek", 234 | "Time remaining", 235 | "Mute", 236 | "Volume"})(elements, driver, ids, result); 237 | 238 | CheckAudioKeyboardInteractions(elements, driver, ids, result); 239 | return result; 240 | } 241 | )), 242 | new TestData("canvas", UIAControlType.Image, 243 | additionalRequirement: ((elements, driver, ids) => { 244 | var result = new TestCaseResultExt(); 245 | var subdomElements = new List(); 246 | 247 | foreach (var e in elements) 248 | { 249 | subdomElements.AddRange(e.GetAllDescendents((d) => 250 | { 251 | var convertedRole = 252 | GetControlTypeFromCode(d.CurrentControlType); 253 | return (convertedRole == UIAControlType.Button || convertedRole == UIAControlType.Text); 254 | })); 255 | } 256 | if(subdomElements.Count() != 3) 257 | { 258 | result.Result = ResultType.Half; 259 | result.AddInfo("Unable to find subdom elements"); 260 | } 261 | 262 | var featureDetectionScript = @"canvas = document.getElementById('myCanvas'); 263 | isSupported = !!(canvas.getContext && canvas.getContext('2d')); 264 | isSupported = isSupported && !!(canvas.getContext('2d').drawFocusIfNeeded); 265 | return isSupported;"; 266 | 267 | if(!(bool)driver.ExecuteScript(featureDetectionScript, timeout)) 268 | { 269 | result.Result = ResultType.Half; 270 | result.AddInfo("\nFailed feature detection"); 271 | } 272 | 273 | return result; 274 | })), 275 | new TestData("datalist", UIAControlType.Combobox, keyboardElements: new List { "input1" }, 276 | additionalRequirement: ((elements, driver, ids) => { 277 | Func datalistValue = (id) => (string)driver.ExecuteScript("return document.getElementById('" + id + "').value", 0); 278 | var result = new TestCaseResultExt(); 279 | 280 | foreach(var element in elements) 281 | { 282 | var elementFive = (IUIAutomationElement5)element; 283 | List patternIds; 284 | var names = elementFive.GetPatterns(out patternIds); 285 | 286 | if (!names.Contains("SelectionPattern")) 287 | { 288 | result.Result = ResultType.Half; 289 | result.AddInfo("\nElement did not support SelectionPattern"); 290 | } 291 | else { 292 | var selectionPattern = (IUIAutomationSelectionPattern)elementFive.GetCurrentPattern( 293 | patternIds[names.IndexOf("SelectionPattern")]); 294 | 295 | if(selectionPattern.CurrentCanSelectMultiple == 1) 296 | { 297 | result.Result = ResultType.Half; 298 | result.AddInfo("\nCanSelectMultiple set to true"); 299 | } 300 | } 301 | } 302 | var previousControllerForElements = new HashSet(); 303 | 304 | //keyboard a11y 305 | foreach (var id in ids.Take(1)) 306 | { 307 | var initial = datalistValue(id); 308 | driver.SendSpecialKeys(id, WebDriverKey.Arrow_down); 309 | 310 | var controllerForElements = elements.Where(e => e.CurrentControllerFor != null && e.CurrentControllerFor.Length > 0).ToList().Select(element => elements.IndexOf(element)); 311 | if(controllerForElements.All(element => previousControllerForElements.Contains(element))){ 312 | result.Result = ResultType.Half; 313 | result.AddInfo("Element controller for not set for id: " + id); 314 | } 315 | 316 | previousControllerForElements.Add(controllerForElements.First(element => !previousControllerForElements.Contains(element))); 317 | 318 | driver.SendSpecialKeys(id, WebDriverKey.Enter); 319 | if (datalistValue(id) != "Item value 1") 320 | { 321 | result.Result = ResultType.Half; 322 | result.AddInfo("Unable to set the datalist with keyboard for element with id: " + id); 323 | return result; 324 | } 325 | } 326 | 327 | return result; 328 | })), 329 | new TestData("figure", UIAControlType.Group, "figure", 330 | requiredNames: 331 | new List{ 332 | "aria-label attribute 2", 333 | "p referenced by aria-labelledby3", 334 | "title attribute 4", 335 | "Figcaption element 5", 336 | "Figcaption element 7"}, 337 | requiredDescriptions: 338 | new List{ 339 | "p referenced by aria-describedby6", 340 | "title attribute 7" 341 | }), 342 | new TestData("figure-figcaption", UIAControlType.Text,//Control type is ignored 343 | searchStrategy: element => true, //Verify this element via text range 344 | additionalRequirement: ((elements, driver, ids) => 345 | { 346 | var result = new TestCaseResultExt(); 347 | var logoText = "HTML5 logo 1"; 348 | 349 | //there will be only one, since element is the pane in this case 350 | foreach(var element in elements) { 351 | var five = (IUIAutomationElement5) 352 | walker.GetFirstChildElement( 353 | walker.GetFirstChildElement(elements[0]));//only have the pane element, take its grandchild 354 | List patternIds; 355 | var names = five.GetPatterns(out patternIds); 356 | 357 | if (!names.Contains("TextPattern")) 358 | { 359 | result.Result = ResultType.Half; 360 | result.AddInfo("\nPane did not support TextPattern, unable to search"); 361 | return result; 362 | } 363 | 364 | var textPattern = (IUIAutomationTextPattern)five.GetCurrentPattern( 365 | patternIds[names.IndexOf("TextPattern")]); 366 | 367 | var documentRange = textPattern.DocumentRange; 368 | 369 | var foundText = documentRange.GetText(1000); 370 | if (!foundText.Contains(logoText)) 371 | { 372 | result.Result = ResultType.Half; 373 | result.AddInfo("\nText not found on page"); 374 | } 375 | 376 | var foundControlTypes = new HashSet(); 377 | var figure = EdgeA11yTools.SearchChildren(element, UIAControlType.Group, null, out foundControlTypes); 378 | 379 | var childRange = textPattern.RangeFromChild(figure[0]); 380 | 381 | var childRangeText = childRange.GetText(1000).Trim(); 382 | 383 | if(childRangeText != logoText) 384 | { 385 | result.Result = ResultType.Half; 386 | result.AddInfo(string.Format("\nUnable to find correct text range. Found '{0}' instead", childRangeText)); 387 | } 388 | } 389 | 390 | return result; 391 | })), 392 | new TestData("footer", UIAControlType.Group, 393 | searchStrategy: element => 394 | GetControlTypeFromCode(element.CurrentControlType) == UIAControlType.Group 395 | && element.CurrentLocalizedControlType != "article", 396 | requiredNames: 397 | new List{ 398 | "aria-label attribute 3", 399 | "small referenced by aria-labelledby4", 400 | "title attribute 5", 401 | "aria-label attribute 7"}, 402 | requiredDescriptions: 403 | new List{ 404 | "small referenced by aria-describedby6", 405 | "title attribute 7" 406 | }, 407 | additionalRequirement: (elements, driver, ids) => { 408 | var result = new TestCaseResultExt(); 409 | 410 | if (elements.Count() != 7) 411 | { 412 | result.Result = ResultType.Half; 413 | result.AddInfo("\nFound " + elements.Count() + " elements, expected 7."); 414 | } 415 | 416 | var convertedLandmarks = 0; 417 | var localizedLandmarks = 0; 418 | //same for landmark and localizedlandmark 419 | foreach (var element in elements) 420 | { 421 | var five = element as IUIAutomationElement5; 422 | var convertedLandmark = GetLandmarkTypeFromCode(five.CurrentLandmarkType); 423 | var localizedLandmark = five.CurrentLocalizedLandmarkType; 424 | if (convertedLandmark == UIALandmarkType.Custom) 425 | { 426 | convertedLandmarks++; 427 | } 428 | if (localizedLandmark == "content information") 429 | { 430 | localizedLandmarks++; 431 | } 432 | } 433 | if (convertedLandmarks != 1) 434 | { 435 | result.Result = ResultType.Half; 436 | result.AddInfo("\nFound " + convertedLandmarks + " elements with landmark type Custom, expected 1"); 437 | } 438 | 439 | if (localizedLandmarks != 1) 440 | { 441 | result.Result = ResultType.Half; 442 | result.AddInfo("\nFound " + localizedLandmarks + " elements with localized landmark type content information, expected 1"); 443 | } 444 | 445 | return result; 446 | }), 447 | new TestData("header", UIAControlType.Group, 448 | searchStrategy: element => 449 | GetControlTypeFromCode(element.CurrentControlType) == UIAControlType.Group 450 | && element.CurrentLocalizedControlType != "article", 451 | requiredNames: 452 | new List{ 453 | "aria-label attribute 3", 454 | "h1 referenced by aria-labelledby4", 455 | "title attribute 5", 456 | "aria-label attribute 7"}, 457 | requiredDescriptions: 458 | new List{ 459 | "h1 referenced by aria-describedby6", 460 | "title attribute 7"}, 461 | additionalRequirement: (elements, driver, ids) => { 462 | var result = new TestCaseResultExt(); 463 | 464 | if (elements.Count() != 7) 465 | { 466 | result.Result = ResultType.Half; 467 | result.AddInfo("\nFound " + elements.Count() + " elements, expected 7."); 468 | } 469 | 470 | var convertedLandmarks = 0; 471 | var localizedLandmarks = 0; 472 | var landmarkCode = 0; 473 | //same for landmark and localizedlandmark 474 | foreach (var element in elements) 475 | { 476 | var five = element as IUIAutomationElement5; 477 | var convertedLandmark = GetLandmarkTypeFromCode(five.CurrentLandmarkType); 478 | landmarkCode = five.CurrentLandmarkType; 479 | var localizedLandmark = five.CurrentLocalizedLandmarkType; 480 | if (convertedLandmark == UIALandmarkType.Custom) 481 | { 482 | convertedLandmarks++; 483 | } 484 | if (localizedLandmark == "banner") 485 | { 486 | localizedLandmarks++; 487 | } 488 | } 489 | if (convertedLandmarks != 1) 490 | { 491 | result.Result = ResultType.Half; 492 | result.AddInfo("\nFound " + convertedLandmarks + " elements with landmark type Custom, expected 1: " + landmarkCode); 493 | } 494 | 495 | if (localizedLandmarks != 1) 496 | { 497 | result.Result = ResultType.Half; 498 | result.AddInfo("\nFound " + localizedLandmarks + " elements with localized landmark type banner, expected 1"); 499 | } 500 | 501 | return result; 502 | }), 503 | new TestData("input-color", UIAControlType.Button, "color picker", 504 | additionalRequirement: (elements, driver, ids) => { 505 | var result = new TestCaseResultExt(); 506 | 507 | var previousControllerForElements = new HashSet(); 508 | foreach(var id in ids.Take(1)) 509 | { 510 | Func CheckColorValue = () => (string) driver.ExecuteScript("return document.getElementById('"+ id + "').value", timeout); 511 | Action ChangeColorValue = (value) => driver.ExecuteScript("document.getElementById('"+ id + "').value = '" + value + "'", timeout); 512 | Func ActiveElement = () => (string)driver.ExecuteScript("return document.activeElement.id", 0); 513 | 514 | var initial = CheckColorValue(); 515 | driver.SendSpecialKeys(id, new List { WebDriverKey.Enter, WebDriverKey.Escape, WebDriverKey.Enter, WebDriverKey.Enter }); 516 | 517 | //open dialog to check controllerfor 518 | driver.SendSpecialKeys(id, WebDriverKey.Enter); 519 | var controllerForElements = elements.Where(e => e.CurrentControllerFor != null && e.CurrentControllerFor.Length > 0).ToList().Select(element => elements.IndexOf(element)); 520 | if(controllerForElements.All(element => previousControllerForElements.Contains(element))){ 521 | result.Result = ResultType.Half; 522 | result.AddInfo("\nElement controller for not set for id: " + id); 523 | } 524 | else 525 | { 526 | //the element that corresponds to this id 527 | var thisElement = elements[controllerForElements.First(element => !previousControllerForElements.Contains(element))]; 528 | if(thisElement.CurrentControllerFor.Length > 1){ 529 | throw new Exception("\nMore than one ControllerFor present, test assumption failed"); 530 | } 531 | var thisDialog = thisElement.CurrentControllerFor.GetElement(0); 532 | var descendents = thisDialog.GetAllDescendents(); 533 | 534 | //sliders 535 | var sliders = descendents.Where(d => GetControlTypeFromCode(d.CurrentControlType) == UIAControlType.Slider); 536 | if(sliders.Count() != 3){ 537 | result.Result = ResultType.Half; 538 | result.AddInfo("\nDialog did not have three slider elements"); 539 | } 540 | else if (!sliders.All(s => s.GetPatterns().Contains("RangeValuePattern"))) 541 | { 542 | result.Result = ResultType.Half; 543 | result.AddInfo("\nDialog's sliders did not implement RangeValuePattern"); 544 | } 545 | 546 | //buttons 547 | if (descendents.Count(d => GetControlTypeFromCode(d.CurrentControlType) == UIAControlType.Button) != 2) 548 | { 549 | result.Result = ResultType.Half; 550 | result.AddInfo("\nDialog did not have two button elements"); 551 | } 552 | 553 | //color well 554 | var outputs = descendents.Where(d => GetControlTypeFromCode(d.CurrentControlType) == UIAControlType.Group && d.CurrentLocalizedControlType == "output"); 555 | if (outputs.Count() > 1) 556 | { 557 | throw new Exception("Test assumption failed: expected color dialog to have at most one output"); 558 | } 559 | else if (outputs.Count() == 0) 560 | { 561 | result.Result = ResultType.Half; 562 | result.AddInfo("\nCould not find output in color dialog"); 563 | } 564 | else if (outputs.Count() == 1) 565 | { 566 | var output = outputs.First(); 567 | if (output.CurrentName == null || output.CurrentName == "") 568 | { 569 | result.Result = ResultType.Half; 570 | result.AddInfo("\nColor dialog output did not have name set"); 571 | } 572 | else 573 | { 574 | var initialName = output.CurrentName; 575 | 576 | driver.SendSpecialKeys(id, new List { WebDriverKey.Tab, 577 | WebDriverKey.Tab, 578 | WebDriverKey.Arrow_right, 579 | WebDriverKey.Arrow_right }); 580 | 581 | if (output.CurrentName == initialName) 582 | { 583 | result.Result = ResultType.Half; 584 | result.AddInfo("\nColor dialog output did not change name when color changed: " + output.CurrentName); 585 | } 586 | 587 | ChangeColorValue("#000000");//Ensure that the color is clear before continuing 588 | } 589 | } 590 | } 591 | 592 | //open with enter, close with escape 593 | driver.SendSpecialKeys(id, new List { WebDriverKey.Escape, 594 | WebDriverKey.Enter, 595 | WebDriverKey.Tab, 596 | WebDriverKey.Arrow_right, 597 | WebDriverKey.Arrow_right, 598 | WebDriverKey.Escape }); 599 | if (CheckColorValue() != initial) 600 | { 601 | result.Result = ResultType.Half; 602 | result.AddInfo("\nUnable to cancel with escape"); 603 | } 604 | 605 | //open with enter, close with enter 606 | driver.SendSpecialKeys(id, new List { 607 | WebDriverKey.Escape, 608 | WebDriverKey.Enter, 609 | WebDriverKey.Tab, 610 | WebDriverKey.Tab, 611 | WebDriverKey.Arrow_right, 612 | WebDriverKey.Arrow_right, 613 | WebDriverKey.Enter }); 614 | if (CheckColorValue() == initial) 615 | { 616 | result.Result = ResultType.Half; 617 | result.AddInfo("\nUnable to change value with arrow keys and submit with enter"); 618 | } 619 | 620 | //open with space, close with enter 621 | initial = CheckColorValue(); 622 | driver.SendSpecialKeys(id, new List { 623 | WebDriverKey.Escape, 624 | WebDriverKey.Space, 625 | WebDriverKey.Tab, 626 | WebDriverKey.Tab, 627 | WebDriverKey.Arrow_right, 628 | WebDriverKey.Arrow_right, 629 | WebDriverKey.Enter }); 630 | if (initial == CheckColorValue()) 631 | { 632 | result.Result = ResultType.Half; 633 | result.AddInfo("\nUnable to open dialog with space"); 634 | } 635 | 636 | initial = CheckColorValue(); 637 | 638 | driver.SendSpecialKeys(id, new List { 639 | WebDriverKey.Enter, 640 | WebDriverKey.Tab, 641 | WebDriverKey.Tab, 642 | WebDriverKey.Tab }); 643 | if (ActiveElement() != id) 644 | { 645 | result.Result = ResultType.Half; 646 | result.AddInfo("\nUnable to reach accept/dismiss buttons via tab"); 647 | } 648 | else//only try to use the buttons if they're there 649 | { 650 | //**Dismiss button** 651 | //Open the dialog, change hue, tab to cancel button, activate it with space, 652 | //check that tabbing moves to the previous button (on the page not the dialog) 653 | driver.SendSpecialKeys(id, new List { WebDriverKey.Escape, 654 | WebDriverKey.Enter, 655 | WebDriverKey.Arrow_right, 656 | WebDriverKey.Arrow_right, 657 | WebDriverKey.Tab, 658 | WebDriverKey.Tab, 659 | WebDriverKey.Tab, 660 | WebDriverKey.Tab, 661 | WebDriverKey.Space, 662 | WebDriverKey.Shift, 663 | WebDriverKey.Tab, 664 | WebDriverKey.Shift }); 665 | if (initial != CheckColorValue() || ActiveElement() == id) 666 | { 667 | result.Result = ResultType.Half; 668 | result.AddInfo("\nUnable to cancel with dismiss button via space"); 669 | } 670 | 671 | //do the same as above, but activate the button with enter this time 672 | driver.SendSpecialKeys(id, new List { WebDriverKey.Escape, 673 | WebDriverKey.Enter, 674 | WebDriverKey.Arrow_right, 675 | WebDriverKey.Arrow_right, 676 | WebDriverKey.Tab, 677 | WebDriverKey.Tab, 678 | WebDriverKey.Tab, 679 | WebDriverKey.Tab, 680 | WebDriverKey.Enter, 681 | WebDriverKey.Shift, 682 | WebDriverKey.Tab, 683 | WebDriverKey.Shift }); 684 | if (initial != CheckColorValue() || ActiveElement() == id) 685 | { 686 | result.Result = ResultType.Half; 687 | result.AddInfo("\nUnable to cancel with dismiss button via enter"); 688 | } 689 | 690 | 691 | //**Accept button** 692 | initial = CheckColorValue(); 693 | 694 | //Open the dialog, tab to hue, change hue, tab to accept button, activate it with space, 695 | //send tab (since the dialog should be closed, this will transfer focus to the next 696 | //input-color button) 697 | driver.SendSpecialKeys(id, new List { WebDriverKey.Escape, 698 | WebDriverKey.Enter, 699 | WebDriverKey.Tab, 700 | WebDriverKey.Tab, 701 | WebDriverKey.Arrow_right, 702 | WebDriverKey.Arrow_right, 703 | WebDriverKey.Tab, 704 | WebDriverKey.Space, 705 | WebDriverKey.Tab }); 706 | if (initial == CheckColorValue() || ActiveElement() == id) 707 | { 708 | result.Result = ResultType.Half; 709 | result.AddInfo("\nUnable to accept with accept button via space"); 710 | } 711 | 712 | initial = CheckColorValue();//the value hopefully changed above, but just to be safe 713 | 714 | //Open the dialog, tab to hue, change hue, tab to accept button, activate it with enter 715 | //We don't have to worry about why the dialog closed here (button or global enter) 716 | driver.SendSpecialKeys(id, new List { WebDriverKey.Escape, 717 | WebDriverKey.Enter, 718 | WebDriverKey.Tab, 719 | WebDriverKey.Tab, 720 | WebDriverKey.Arrow_right, 721 | WebDriverKey.Arrow_right, 722 | WebDriverKey.Tab, 723 | WebDriverKey.Enter, 724 | WebDriverKey.Tab }); 725 | if (initial == CheckColorValue() || ActiveElement() == id) 726 | { 727 | result.Result = ResultType.Half; 728 | result.AddInfo("\nUnable to accept with accept button via enter"); 729 | } 730 | } 731 | } 732 | 733 | return result; 734 | }), 735 | new TestData("input-date", UIAControlType.Edit, 736 | keyboardElements: new List { "input1", "input2" }, 737 | requiredNames: 738 | new List{ 739 | "aria-label attribute2", 740 | "p referenced by aria-labelledby3", 741 | "label wrapping input 4", 742 | "title attribute 5", 743 | "label referenced by for/id attributes 7"}, 744 | requiredDescriptions: 745 | new List{ 746 | "p referenced by aria-describedby6", 747 | "title attribute 7" }, 748 | additionalRequirement: 749 | (element, driver, ids) => 750 | { 751 | var result = new TestCaseResultExt(); 752 | CheckCalendar(3)(element, driver, ids, result); 753 | return result; 754 | }), 755 | new TestData("input-datetime-local", UIAControlType.Edit, 756 | requiredNames: 757 | new List{ 758 | "aria-label attribute2", 759 | "p referenced by aria-labelledby3", 760 | "label wrapping input 4", 761 | "title attribute 5", 762 | "label referenced by for/id attributes 7"}, 763 | requiredDescriptions: 764 | new List{ 765 | "p referenced by aria-describedby6", 766 | "title attribute 7" }, 767 | additionalRequirement: 768 | (element, driver, ids) => 769 | { 770 | var result = new TestCaseResultExt(); 771 | CheckDatetimeLocal()(element, driver, ids, result); 772 | return result; 773 | }), 774 | new TestData("input-email", UIAControlType.Edit, "email", 775 | keyboardElements: new List { "input1", "input2" }, 776 | requiredNames: 777 | new List{ 778 | "aria-label attribute2", 779 | "p referenced by aria-labelledby3", 780 | "label wrapping input 4", 781 | "title attribute 5", 782 | "label referenced by for/id attributes 7"}, 783 | requiredDescriptions: 784 | new List{ 785 | "p referenced by aria-describedby6", 786 | "title attribute 7" }, 787 | additionalRequirement: (elements, driver, ids) => { 788 | var result = new TestCaseResultExt(); 789 | CheckValidation()(elements, driver, ids, result); 790 | CheckClearButton()(elements, driver, ids, result); 791 | return result; 792 | }), 793 | new TestData("input-month", UIAControlType.Edit, keyboardElements: new List { "input1", "input2" }, 794 | requiredNames: 795 | new List{ 796 | "aria-label attribute2", 797 | "p referenced by aria-labelledby3", 798 | "label wrapping input 4", 799 | "title attribute 5", 800 | "label referenced by for/id attributes 7"}, 801 | requiredDescriptions: 802 | new List{ 803 | "p referenced by aria-describedby6", 804 | "title attribute 7" }, 805 | additionalRequirement: 806 | (elements, driver, ids) => 807 | { 808 | var result = new TestCaseResultExt(); 809 | CheckCalendar(2)(elements, driver, ids, result); 810 | return result; 811 | }), 812 | new TestData("input-number", UIAControlType.Spinner, "number", 813 | keyboardElements: new List { "input1", "input2" }, 814 | requiredNames: 815 | new List{ 816 | "aria-label attribute2", 817 | "p referenced by aria-labelledby3", 818 | "label wrapping input 4", 819 | "title attribute 5", 820 | "label referenced by for/id attributes 7"}, 821 | requiredDescriptions: 822 | new List{ 823 | "p referenced by aria-describedby6", 824 | "title attribute 7" }, 825 | additionalRequirement: 826 | (elements, driver, ids) => 827 | { 828 | var result = new TestCaseResultExt(); 829 | CheckValidation()(elements, driver, ids, result); 830 | return result; 831 | }), 832 | new TestData("input-range", UIAControlType.Slider, keyboardElements: new List { "input1", "input2" }, 833 | requiredNames: 834 | new List{ 835 | "aria-label attribute 2", 836 | "p referenced by aria-labelledby3", 837 | "label wrapping input 4", 838 | "title attribute 5", 839 | "label referenced by for/id attributes 7"}, 840 | requiredDescriptions: 841 | new List{ 842 | "p referenced by aria-describedby6", 843 | "title attribute 7"}, 844 | additionalRequirement: (elements, driver, ids) => { 845 | var result = new TestCaseResultExt(); 846 | 847 | //keyboard interaction 848 | foreach(var id in ids.Take(1)){ 849 | Func RangeValue = () => (int) Int32.Parse((string) driver.ExecuteScript("return document.getElementById('" + id + "').value", 0)); 850 | 851 | var initial = RangeValue(); 852 | driver.SendSpecialKeys(id, WebDriverKey.Arrow_up); 853 | if (initial == RangeValue()) 854 | { 855 | result.Result = ResultType.Half; 856 | result.AddInfo("\nUnable to increase range with arrow up"); 857 | continue; 858 | } 859 | driver.SendSpecialKeys(id, WebDriverKey.Arrow_down); 860 | if (initial != RangeValue()) 861 | { 862 | result.Result = ResultType.Half; 863 | result.AddInfo("\nUnable to decrease range with arrow down"); 864 | continue; 865 | } 866 | 867 | driver.SendSpecialKeys(id, WebDriverKey.Arrow_right); 868 | if (initial >= RangeValue()) 869 | { 870 | result.Result = ResultType.Half; 871 | result.AddInfo("\nUnable to increase range with arrow right"); 872 | continue; 873 | } 874 | driver.SendSpecialKeys(id, WebDriverKey.Arrow_left); 875 | if (initial != RangeValue()) 876 | { 877 | result.Result = ResultType.Half; 878 | result.AddInfo("\nUnable to decrease range with arrow left"); 879 | continue; 880 | } 881 | } 882 | 883 | //rangevalue pattern 884 | foreach(var element in elements){ 885 | var rangeValuePattern = "RangeValuePattern"; 886 | if (!element.GetPatterns().Contains(rangeValuePattern)) { 887 | result.Result = ResultType.Half; 888 | result.AddInfo("\nElement did not implement the RangeValuePattern"); 889 | } 890 | } 891 | return result; 892 | }), 893 | new TestData("input-search", UIAControlType.Edit, "search", keyboardElements: new List { "input1", "input2" }, 894 | requiredNames: 895 | new List{ 896 | "aria-label attribute 2", 897 | "p referenced by aria-labelledby3", 898 | "label wrapping input 4", 899 | "title attribute 5", 900 | "label referenced by for/id attributes 7"}, 901 | requiredDescriptions: 902 | new List{ 903 | "p referenced by aria-describedby6", 904 | "title attribute 7"}, 905 | additionalRequirement: 906 | (elements, driver, ids) => { 907 | var result = new TestCaseResultExt(); 908 | CheckClearButton()(elements, driver, ids, result); 909 | return result; 910 | }), 911 | new TestData("input-tel", UIAControlType.Edit, "telephone", keyboardElements: new List { "input1", "input2" }, 912 | requiredNames: 913 | new List{ 914 | "aria-label attribute 2", 915 | "p referenced by aria-labelledby3", 916 | "label wrapping input 4", 917 | "title attribute 5", 918 | "label referenced by for/id attributes 7"}, 919 | requiredDescriptions: 920 | new List{ 921 | "p referenced by aria-describedby6", 922 | "title attribute 7"}, 923 | additionalRequirement: 924 | (elements, driver, ids) => { 925 | var result = new TestCaseResultExt(); 926 | CheckClearButton()(elements, driver, ids, result); 927 | return result; 928 | }), 929 | new TestData("input-time", UIAControlType.Edit, keyboardElements: new List { "input1", "input2" }, 930 | requiredNames: 931 | new List{ 932 | "aria-label attribute 2", 933 | "p referenced by aria-labelledby3", 934 | "label wrapping input 4", 935 | "title attribute 5", 936 | "label referenced by for/id attributes 7"}, 937 | requiredDescriptions: 938 | new List{ 939 | "p referenced by aria-describedby6", 940 | "title attribute 7" }, 941 | additionalRequirement: 942 | (elements, driver, ids) => { 943 | var result = new TestCaseResultExt(); 944 | CheckCalendar(3, 2)(elements, driver, ids, result); 945 | return result; 946 | }), 947 | new TestData("input-url", UIAControlType.Edit, "url", 948 | keyboardElements: new List { "input1", "input2" }, 949 | additionalRequirement: 950 | (elements, driver, ids) => { 951 | var result = new TestCaseResultExt(); 952 | CheckValidation()(elements, driver, ids, result); 953 | CheckClearButton()(elements, driver, ids, result); 954 | return result; 955 | }), 956 | new TestData("input-week", UIAControlType.Edit, keyboardElements: new List { "input1", "input2" }, 957 | requiredNames: 958 | new List{ 959 | "aria-label attribute 2", 960 | "p referenced by aria-labelledby3", 961 | "label wrapping input 4", 962 | "title attribute 5", 963 | "label referenced by for/id attributes 7"}, 964 | requiredDescriptions: 965 | new List{ 966 | "p referenced by aria-describedby6", 967 | "title attribute 7" }, 968 | additionalRequirement: 969 | (elements, driver, ids) => { 970 | var result = new TestCaseResultExt(); 971 | CheckCalendar(2)(elements, driver, ids, result); 972 | return result; 973 | }), 974 | new TestData("main", UIAControlType.Group, "main", UIALandmarkType.Main, "main", 975 | requiredNames: 976 | new List{ 977 | "title attribute 1", 978 | "aria-label attribute 2", 979 | "h1 referenced by aria-labelledby3", 980 | "title attribute 4", 981 | "aria-label attribute 6" 982 | }, 983 | requiredDescriptions: 984 | new List{ 985 | "h1 referenced by aria-describedby5", 986 | "title attribute 6"}), 987 | new TestData("mark", UIAControlType.Text, 988 | searchStrategy: element => 989 | GetControlTypeFromCode(element.CurrentControlType) == UIAControlType.Text 990 | && element.CurrentLocalizedControlType == "mark", 991 | requiredNames: 992 | new List{ 993 | "aria-label attribute2", 994 | "Element referenced by aria-labelledby attribute3", 995 | "title attribute 4", 996 | "aria-label attribute 6" 997 | }, 998 | requiredDescriptions: 999 | new List{ 1000 | "Element referenced by aria-describedby attribute5", 1001 | "title attribute 6"}), 1002 | new TestData("meter", UIAControlType.Progressbar, "meter", 1003 | requiredNames: 1004 | new List{ 1005 | "aria-label attribute 2", 1006 | "p referenced by aria-labelledby3", 1007 | "label wrapping meter 4", 1008 | "title attribute 5", 1009 | "label referenced by for/id attributes 7"}, 1010 | requiredDescriptions: 1011 | new List{ 1012 | "p referenced by aria-describedby6", 1013 | "title attribute 7" }, 1014 | additionalRequirement: 1015 | ((elements, driver, ids) => { 1016 | var result = new TestCaseResultExt(); 1017 | //readonly 1018 | if(!elements.All(element => element.GetProperties().Any(p => p.Contains("IsReadOnly")))){ 1019 | result.Result = ResultType.Half; 1020 | result.AddInfo("Not all elements were read only"); 1021 | } 1022 | 1023 | //rangevalue 1024 | foreach (var element in elements) 1025 | { 1026 | var patternName = "RangeValuePattern"; 1027 | 1028 | var patterned = GetPattern(patternName, element); 1029 | if(patterned == null) 1030 | { 1031 | result.Result = ResultType.Half; 1032 | result.AddInfo("\nElement did not support " + patternName); 1033 | } 1034 | else 1035 | { 1036 | if (patterned.CurrentMaximum - 100 > epsilon) 1037 | { 1038 | result.Result = ResultType.Half; 1039 | result.AddInfo("\nElement did not have the correct max"); 1040 | } 1041 | if (patterned.CurrentMinimum - 0 > epsilon) 1042 | { 1043 | result.Result = ResultType.Half; 1044 | result.AddInfo("\nElement did not have the correct min"); 1045 | } 1046 | var value = 83.5;//All the meters are set to this 1047 | if (patterned.CurrentValue - value > epsilon) 1048 | { 1049 | result.Result = ResultType.Half; 1050 | result.AddInfo("\nElement did not have the correct value"); 1051 | } 1052 | } 1053 | } 1054 | 1055 | //value 1056 | foreach (var element in elements) 1057 | { 1058 | var patterned = GetPattern("ValuePattern", element); 1059 | if (patterned == null) 1060 | { 1061 | result.Result = ResultType.Half; 1062 | result.AddInfo("\nElement did not support ValuePattern"); 1063 | } 1064 | else 1065 | { 1066 | if (patterned.CurrentValue == null || patterned.CurrentValue != "Good") 1067 | { 1068 | result.Result = ResultType.Half; 1069 | result.AddInfo("\nElement did not have value set"); 1070 | } 1071 | } 1072 | } 1073 | 1074 | return result; 1075 | }), 1076 | searchStrategy: (element => element.GetPatterns().Contains("RangeValuePattern"))),//NB the ControlType is not used for searching this element 1077 | new TestData("nav", UIAControlType.Group, "navigation", UIALandmarkType.Navigation, "navigation", 1078 | requiredNames: 1079 | new List{ 1080 | "aria-label attribute 2", 1081 | "h1 referenced by aria-labelledby3", 1082 | "title attribute 4", 1083 | "aria-label attribute 6"}, 1084 | requiredDescriptions: 1085 | new List{ 1086 | "h1 referenced by aria-describedby5", 1087 | "title attribute 6"}), 1088 | new TestData("output", UIAControlType.Group, "output", 1089 | requiredNames: 1090 | new List{ 1091 | "aria-label attribute 2", 1092 | "p referenced by aria-labelledby3", 1093 | "label wrapping output 410", 1094 | "title attribute 5", 1095 | "label referenced by for/id attributes 7" }, 1096 | requiredDescriptions: 1097 | new List{ 1098 | "p referenced by aria-describedby6", 1099 | "title attribute 7" }, 1100 | additionalRequirement: ((elements, driver, ids) => { 1101 | var result = new TestCaseResultExt(); 1102 | 1103 | if (!elements.All(element => ((IUIAutomationElement5)element).CurrentLiveSetting == LiveSetting.Polite)){ 1104 | result.Result = ResultType.Half; 1105 | result.AddInfo("\nElement did not have LiveSetting = Polite"); 1106 | } 1107 | var controllerForLengths = elements.Select(element => element.CurrentControllerFor != null ? element.CurrentControllerFor.Length : 0); 1108 | if (controllerForLengths.Count(cfl => cfl > 0) != 1) 1109 | { 1110 | result.Result = ResultType.Half; 1111 | result.AddInfo("\nExpected 1 element with ControllerFor set. Found " + controllerForLengths.Count(cfl => cfl > 0)); 1112 | } 1113 | return result; 1114 | })), 1115 | new TestData("progress", UIAControlType.Progressbar, 1116 | requiredNames: 1117 | new List{ 1118 | "aria-label attribute 2", 1119 | "p referenced by aria-labelledby3", 1120 | "label wrapping output 4", 1121 | "title attribute 5", 1122 | "label referenced by for/id attributes 7" }, 1123 | requiredDescriptions: 1124 | new List{ 1125 | "p referenced by aria-describedby6", 1126 | "title attribute 7" }, 1127 | additionalRequirement: (elements, driver, ids) => { 1128 | var result = new TestCaseResultExt(); 1129 | 1130 | //rangevalue 1131 | foreach (var element in elements) 1132 | { 1133 | var patternName = "RangeValuePattern"; 1134 | 1135 | var patterned = GetPattern(patternName, element); 1136 | if(patterned == null) 1137 | { 1138 | result.Result = ResultType.Half; 1139 | result.AddInfo("\nElement did not support " + patternName); 1140 | } 1141 | else 1142 | { 1143 | if (patterned.CurrentMaximum - 100 > epsilon) 1144 | { 1145 | result.Result = ResultType.Half; 1146 | result.AddInfo("\nElement did not have the correct max"); 1147 | } 1148 | if (patterned.CurrentMinimum - 0 > epsilon) 1149 | { 1150 | result.Result = ResultType.Half; 1151 | result.AddInfo("\nElement did not have the correct min"); 1152 | } 1153 | var value = 22;//All the progress bars are set to this 1154 | if (patterned.CurrentValue - value > epsilon) 1155 | { 1156 | result.Result = ResultType.Half; 1157 | result.AddInfo("\nElement did not have the correct value"); 1158 | } 1159 | } 1160 | } 1161 | 1162 | return result; 1163 | }), 1164 | new TestData("section", UIAControlType.Group, "section", UIALandmarkType.Custom, "region", 1165 | requiredNames: 1166 | new List{ 1167 | "aria-label attribute 3", 1168 | "h1 referenced by aria-labelledby4", 1169 | "title attribute 5", 1170 | "aria-label attribute 7"}, 1171 | requiredDescriptions: 1172 | new List{ 1173 | "h1 referenced by aria-describedby6", 1174 | "title attribute 7" }), 1175 | new TestData("time", UIAControlType.Text, 1176 | searchStrategy: element => 1177 | GetControlTypeFromCode(element.CurrentControlType) == UIAControlType.Text 1178 | && element.CurrentLocalizedControlType == "time", 1179 | requiredNames: 1180 | new List{ 1181 | "aria-label attribute2", 1182 | "Element referenced by aria-labelledby attribute 3", 1183 | "title attribute 4", 1184 | "aria-label attribute 6" 1185 | }, 1186 | requiredDescriptions: 1187 | new List{ 1188 | "2015-10-01", 1189 | "2015-10-02", 1190 | "2015-10-03", 1191 | "2015-10-04", 1192 | "Element referenced by aria-describedby attribute", 1193 | "title attribute 6", 1194 | }), 1195 | new TestData("track", UIAControlType.Unknown, 1196 | additionalRequirement: ((elements, driver, ids) => 1197 | { 1198 | var result = new TestCaseResultExt(); 1199 | driver.ExecuteScript(Javascript.Track, timeout); 1200 | 1201 | if(!(bool)driver.ExecuteScript("return Modernizr.track && Modernizr.texttrackapi", timeout)) { 1202 | result.Result = ResultType.Half; 1203 | result.AddInfo("Element was not found to be supported by Modernizr"); 1204 | } 1205 | return result; 1206 | }), 1207 | searchStrategy: (element => true)), 1208 | new TestData("video", UIAControlType.Group, null, keyboardElements: new List { "video1" }, 1209 | additionalRequirement: ((elements, driver, ids) => 1210 | { 1211 | var result = new TestCaseResultExt(); 1212 | CheckChildNames( 1213 | new List { 1214 | "Play", 1215 | "Time elapsed", 1216 | "Seek", 1217 | "Time remaining", 1218 | "Zoom in", 1219 | "Show audio", 1220 | "Show captioning", 1221 | "Mute", 1222 | "Volume", 1223 | "Full screen" })(elements, driver, ids, result); 1224 | CheckVideoKeyboardInteractions(elements, driver, ids, result); 1225 | return result; 1226 | })), 1227 | new TestData("hidden-att", UIAControlType.Button, null, 1228 | additionalRequirement: ((elements, driver, ids) => 1229 | { 1230 | var result = new TestCaseResultExt(); 1231 | 1232 | var browserElement = EdgeA11yTools.FindBrowserDocument(0); 1233 | 1234 | if (elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) != 0) 1235 | { 1236 | result.Result = ResultType.Half; 1237 | result.AddInfo("Found " + elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) + " elements. Expected 0"); 1238 | return result; 1239 | } 1240 | 1241 | //Make sure the text isn't showing up on the page 1242 | var five = (IUIAutomationElement5) 1243 | walker.GetFirstChildElement( 1244 | walker.GetFirstChildElement(elements[0]));//only have the pane element, take its grandchild 1245 | 1246 | List patternIds; 1247 | var names = five.GetPatterns(out patternIds); 1248 | var textPatternString = "TextPattern"; 1249 | if (!names.Contains(textPatternString)) 1250 | { 1251 | result.Result = ResultType.Half; 1252 | result.AddInfo("Pane did not support TextPattern, unable to search"); 1253 | return result; 1254 | } 1255 | 1256 | var textPattern = (IUIAutomationTextPattern)five.GetCurrentPattern( 1257 | patternIds[names.IndexOf(textPatternString)]); 1258 | 1259 | var documentRange = textPattern.DocumentRange; 1260 | 1261 | var foundText = documentRange.GetText(1000); 1262 | if(foundText.Contains("HiDdEn TeXt")) 1263 | { 1264 | result.Result = ResultType.Half; 1265 | result.AddInfo("\nFound text that should have been hidden"); 1266 | } 1267 | 1268 | //remove hidden attribute 1269 | driver.ExecuteScript(Javascript.RemoveHidden, timeout); 1270 | 1271 | //make sure the button show up now that their parents are not hidden 1272 | HashSet foundControlTypes; 1273 | elements = EdgeA11yTools.SearchChildren(browserElement, UIAControlType.Button, null, out foundControlTypes); 1274 | if (elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) != 1) 1275 | { 1276 | result.Result = ResultType.Half; 1277 | result.AddInfo("\nFound " + elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) + " elements. Expected 1"); 1278 | } 1279 | 1280 | //remove aria-hidden attribute 1281 | driver.ExecuteScript(Javascript.RemoveAriaHidden, timeout); 1282 | 1283 | //both buttons should now be visible, since both aria-hidden and hidden attribute are missing 1284 | elements = EdgeA11yTools.SearchChildren(browserElement, UIAControlType.Button, null, out foundControlTypes); 1285 | if (elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) != 2) 1286 | { 1287 | result.Result = ResultType.Half; 1288 | result.AddInfo("\nFound " + elements.Count(e => GetControlTypeFromCode(e.CurrentControlType) != UIAControlType.Pane) + " elements. Expected 2"); 1289 | } 1290 | 1291 | return result; 1292 | }), 1293 | searchStrategy: (element => true)),//take the pane 1294 | new TestData("required-att", UIAControlType.Edit, 1295 | additionalRequirement: (elements, driver, ids) => 1296 | { 1297 | var result = new TestCaseResultExt(); 1298 | driver.SendSpecialKeys("input1", WebDriverKey.Enter); 1299 | Thread.Sleep(TimeSpan.FromMilliseconds(500)); 1300 | foreach(var element in elements){//there can only be one 1301 | if(element.CurrentControllerFor == null || element.CurrentControllerFor.Length == 0){ 1302 | result.Result = ResultType.Half; 1303 | result.AddInfo("\nElement did not have controller for set"); 1304 | } 1305 | 1306 | if(element.CurrentIsRequiredForForm != 1){ 1307 | result.Result = ResultType.Half; 1308 | result.AddInfo("\nElement did not have IsRequiredForForm set to true"); 1309 | } 1310 | 1311 | if(element.CurrentHelpText == null || element.CurrentHelpText.Length == 0){ 1312 | result.Result = ResultType.Half; 1313 | result.AddInfo("\nElement did not have HelpText"); 1314 | } 1315 | } 1316 | 1317 | return result; 1318 | }), 1319 | new TestData("placeholder-att", UIAControlType.Edit, 1320 | requiredNames: 1321 | new List { 1322 | "placeholder text 1", 1323 | "Label text 2:", 1324 | "Label text 3:", 1325 | "placeholder text 4", 1326 | "placeholder text 5", 1327 | "aria-placeholder text 6" }, 1328 | requiredDescriptions: 1329 | new List { 1330 | "placeholder text 2", 1331 | "placeholder text 3", 1332 | "title text 4", 1333 | }) 1334 | }; 1335 | } 1336 | 1337 | /// 1338 | /// Convert an element into a pattern interface 1339 | /// 1340 | /// The pattern interface to implement 1341 | /// The friendly name of the pattern 1342 | /// The element to convert 1343 | /// The element as an implementation of the pattern 1344 | private static T GetPattern(string patternName, IUIAutomationElement element) 1345 | { 1346 | List patternNames; 1347 | List patternIds; 1348 | 1349 | patternNames = element.GetPatterns(out patternIds); 1350 | if (!patternNames.Contains(patternName)) 1351 | { 1352 | return default(T); 1353 | } 1354 | else 1355 | { 1356 | T pattern = (T)((IUIAutomationElement5)element).GetCurrentPattern(patternIds[patternNames.IndexOf(patternName)]); 1357 | return pattern; 1358 | } 1359 | } 1360 | 1361 | /// 1362 | /// Helper method to tab until an element whose name contains the given string 1363 | /// reports that it has focus 1364 | /// 1365 | /// The parent of the target element 1366 | /// A string which the target element's name will contain 1367 | /// The id to send tabs to 1368 | /// The WebDriver 1369 | /// true if the element was found, false otherwise 1370 | private static bool TabToElementByName(IUIAutomationElement parent, string name, string tabId, DriverManager driver) 1371 | { 1372 | var tabs = 0; 1373 | var resets = 0; 1374 | var element = parent.GetAllDescendents(e => e.CurrentName.Contains(name)).First(); 1375 | while (!(bool)element.GetCurrentPropertyValue(GetPropertyCode(UIAProperty.HasKeyboardFocus))) 1376 | { 1377 | driver.SendSpecialKeys(tabId, WebDriverKey.Tab); 1378 | if (++tabs > 20) 1379 | { 1380 | Javascript.ClearFocus(driver, 0); 1381 | tabs = 0; 1382 | resets++; 1383 | if (resets > 5) 1384 | { 1385 | return false; 1386 | } 1387 | } 1388 | } 1389 | return true; 1390 | } 1391 | 1392 | /// 1393 | /// Check basic keyboard interactions for the video control 1394 | /// 1395 | /// 1396 | /// 1397 | /// 1398 | /// An empty string if an element fails, otherwise an explanation 1399 | private static void CheckVideoKeyboardInteractions(List elements, DriverManager driver, List ids, TestCaseResultExt result) 1400 | { 1401 | string videoId = "video1"; 1402 | 1403 | Func VideoPlaying = () => (bool)driver.ExecuteScript("return !document.getElementById('" + videoId + "').paused", 0); 1404 | Func PauseVideo = () => driver.ExecuteScript("document.getElementById('" + videoId + "').pause()", 0); 1405 | Func PlayVideo = () => driver.ExecuteScript("document.getElementById('" + videoId + "').play()", 0); 1406 | Func GetVideoVolume = () => driver.ExecuteScript("return document.getElementById('" + videoId + "').volume", 0).ParseMystery(); 1407 | Func VideoVolume = expected => Math.Abs(GetVideoVolume() - expected) < epsilon; 1408 | Func VideoMuted = () => (bool)driver.ExecuteScript("return document.getElementById('" + videoId + "').muted", 0); 1409 | Func GetVideoElapsed = () => driver.ExecuteScript("return document.getElementById('" + videoId + "').currentTime", 0).ParseMystery(); 1410 | Func VideoElapsed = expected => Math.Abs(GetVideoElapsed() - expected) < epsilon; 1411 | Func IsVideoFullScreen = () => driver.ExecuteScript("return document.webkitFullscreenElement", 0) != null; 1412 | Func IsVideoLoaded = () => (bool)driver.ExecuteScript("return document.getElementById('" + videoId + "').readyState == 4", 0); 1413 | 1414 | if (!WaitForCondition(IsVideoLoaded, attempts: 40)) 1415 | { 1416 | result.Result = ResultType.Half; 1417 | result.AddInfo("\nVideo did not load after 20 seconds"); 1418 | return; 1419 | } 1420 | 1421 | //Case 1: tab to play button and play/pause 1422 | TabToElementByName(elements[0], "Play", videoId, driver); 1423 | driver.SendSpecialKeys(videoId, WebDriverKey.Space); 1424 | if (!WaitForCondition(VideoPlaying)) 1425 | { 1426 | result.Result = ResultType.Half; 1427 | result.AddInfo("\tVideo was not playing after spacebar on play button\n"); 1428 | PlayVideo(); 1429 | } 1430 | driver.SendSpecialKeys(videoId, WebDriverKey.Enter); 1431 | if (!WaitForCondition(VideoPlaying, reverse: true)) 1432 | { 1433 | result.Result = ResultType.Half; 1434 | result.AddInfo("\tVideo was not paused after enter on play button\n"); 1435 | PauseVideo(); 1436 | } 1437 | 1438 | //Case 2: Volume and mute 1439 | Javascript.ScrollIntoView(driver, 0); 1440 | Javascript.ClearFocus(driver, 0); 1441 | TabToElementByName(elements[0], "Mute", videoId, driver); 1442 | driver.Screenshot("before_enter_to_mute"); 1443 | driver.SendSpecialKeys(videoId, WebDriverKey.Enter);//mute 1444 | driver.Screenshot("after_enter_to_mute"); 1445 | if (!WaitForCondition(VideoMuted)) 1446 | { 1447 | result.Result = ResultType.Half; 1448 | result.AddInfo("\tEnter did not mute the video\n"); 1449 | } 1450 | 1451 | WaitForCondition(() => 1452 | elements[0].GetAllDescendents().Any(e => e.CurrentName == "Unmute") 1453 | ); 1454 | 1455 | driver.Screenshot("before_enter_to_unmute"); 1456 | driver.SendSpecialKeys(videoId, WebDriverKey.Enter);//unmute 1457 | driver.Screenshot("after_enter_to_unmute"); 1458 | if (!WaitForCondition(VideoMuted, reverse: true)) 1459 | { 1460 | result.Result = ResultType.Half; 1461 | result.AddInfo("\tEnter did not unmute the video\n"); 1462 | } 1463 | var initial = GetVideoVolume(); 1464 | driver.SendSpecialKeys(videoId, new List { WebDriverKey.Arrow_down, WebDriverKey.Arrow_down });//volume down 1465 | if (!WaitForCondition(VideoVolume, initial - 0.1)) 1466 | { 1467 | result.Result = ResultType.Half; 1468 | result.AddInfo("\tVolume did not decrease with arrow keys\n"); 1469 | } 1470 | driver.SendSpecialKeys(videoId, new List { WebDriverKey.Arrow_up, WebDriverKey.Arrow_up });//volume up 1471 | if (!WaitForCondition(VideoVolume, initial)) 1472 | { 1473 | result.Result = ResultType.Half; 1474 | result.AddInfo("\tVolume did not increase with arrow keys\n"); 1475 | } 1476 | 1477 | //Case 3: Audio selection 1478 | //TODO test manually 1479 | 1480 | //Case 4: Progress and seek 1481 | if (VideoPlaying()) 1482 | { //this should not be playing 1483 | result.Result = ResultType.Half; 1484 | result.AddInfo("\tVideo was playing when it shouldn't have been\n"); 1485 | } 1486 | Javascript.ClearFocus(driver, 0); 1487 | TabToElementByName(elements[0], "Seek", videoId, driver); 1488 | initial = GetVideoElapsed(); 1489 | driver.SendSpecialKeys(videoId, WebDriverKey.Arrow_right); //skip ahead 1490 | if (!WaitForCondition(VideoElapsed, initial + 10)) 1491 | { 1492 | result.Result = ResultType.Half; 1493 | result.AddInfo("\tVideo did not skip forward with arrow right\n"); 1494 | } 1495 | 1496 | driver.SendSpecialKeys(videoId, WebDriverKey.Arrow_left); //skip back 1497 | if (!WaitForCondition(VideoElapsed, initial)) 1498 | { 1499 | result.Result = ResultType.Half; 1500 | result.AddInfo("\tVideo did not skip back with arrow left\n"); 1501 | } 1502 | 1503 | //Case 5: Progress and seek on remaining time 1504 | if (VideoPlaying()) 1505 | { //this should not be playing 1506 | result.Result = ResultType.Half; 1507 | result.AddInfo("\tVideo was playing when it shouldn't have been\n"); 1508 | } 1509 | Javascript.ClearFocus(driver, 0); 1510 | TabToElementByName(elements[0], "Seek", videoId, driver); 1511 | initial = GetVideoElapsed(); 1512 | driver.SendSpecialKeys(videoId, WebDriverKey.Arrow_right); //skip ahead 1513 | if (!WaitForCondition(VideoElapsed, initial + 10)) 1514 | { 1515 | result.Result = ResultType.Half; 1516 | result.AddInfo("\tVideo did not skip forward with arrow right\n"); 1517 | } 1518 | 1519 | driver.SendSpecialKeys(videoId, WebDriverKey.Arrow_left); //skip back 1520 | if (!WaitForCondition(VideoElapsed, initial)) 1521 | { 1522 | result.Result = ResultType.Half; 1523 | result.AddInfo("\tVideo did not skip back with arrow left\n"); 1524 | driver.SendSpecialKeys(videoId, WebDriverKey.Arrow_left); //skip back 1525 | } 1526 | 1527 | //Case 6: Full screen 1528 | Javascript.ClearFocus(driver, 0); 1529 | TabToElementByName(elements[0], "Full screen", videoId, driver); 1530 | driver.Screenshot("before_enter_to_fullscreen"); 1531 | driver.SendSpecialKeys(videoId, WebDriverKey.Enter); //enter fullscreen mode 1532 | driver.Screenshot("after_enter_to_fullscreen"); 1533 | if (!WaitForCondition(IsVideoFullScreen)) 1534 | { 1535 | result.Result = ResultType.Half; 1536 | result.AddInfo("\tVideo did not enter FullScreen mode\n"); 1537 | } 1538 | driver.Screenshot("before_escape_from_fullscreen"); 1539 | driver.SendSpecialKeys(videoId, WebDriverKey.Escape); 1540 | driver.Screenshot("after_escape_from_fullscreen"); 1541 | if (!WaitForCondition(IsVideoFullScreen, reverse: true)) 1542 | { 1543 | result.Result = ResultType.Half; 1544 | result.AddInfo("\tVideo did not exit FullScreen mode\n"); 1545 | } 1546 | } 1547 | 1548 | /// 1549 | /// Check basic keyboard interactions for the audio control 1550 | /// 1551 | /// The elements found on the page 1552 | /// The driver 1553 | /// The html ids of the elements on the page 1554 | /// An empty string if an element fails, otherwise an explanation 1555 | private static void CheckAudioKeyboardInteractions(List elements, DriverManager driver, List ids, TestCaseResultExt result) 1556 | { 1557 | string audioId = "audio1"; 1558 | Func AudioPlaying = () => (bool)driver.ExecuteScript("return !document.getElementById('" + audioId + "').paused", 0); 1559 | Func PauseAudio = () => driver.ExecuteScript("!document.getElementById('" + audioId + "').pause()", 0); 1560 | Func PlayAudio = () => driver.ExecuteScript("!document.getElementById('" + audioId + "').play()", 0); 1561 | Func GetAudioVolume = () => driver.ExecuteScript("return document.getElementById('" + audioId + "').volume", 0).ParseMystery(); 1562 | Func AudioVolume = expected => Math.Abs(GetAudioVolume() - expected) < epsilon; 1563 | Func AudioMuted = () => (bool)driver.ExecuteScript("return document.getElementById('" + audioId + "').muted", 0); 1564 | Func GetAudioElapsed = () => driver.ExecuteScript("return document.getElementById('" + audioId + "').currentTime", 0).ParseMystery(); 1565 | Func AudioElapsed = expected => Math.Abs(GetAudioElapsed() - expected) < epsilon; 1566 | Func IsAudioLoaded = () => (bool)driver.ExecuteScript("return document.getElementById('" + audioId + "').readyState == 4", 0); 1567 | 1568 | if (!WaitForCondition(IsAudioLoaded, attempts: 40)) 1569 | { 1570 | result.Result = ResultType.Half; 1571 | result.AddInfo("\nAudio did not load after 20 seconds"); 1572 | return; 1573 | } 1574 | 1575 | //Case 1: Play/Pause 1576 | driver.SendTabs(audioId, 1); //Tab to play button 1577 | driver.SendSpecialKeys(audioId, WebDriverKey.Enter); 1578 | if (!WaitForCondition(AudioPlaying)) 1579 | { 1580 | result.Result = ResultType.Half; 1581 | result.AddInfo("\tAudio did not play with enter\n"); 1582 | PlayAudio(); 1583 | } 1584 | 1585 | driver.SendSpecialKeys(audioId, WebDriverKey.Space); 1586 | if (!WaitForCondition(AudioPlaying, reverse: true)) 1587 | { 1588 | result.Result = ResultType.Half; 1589 | result.AddInfo("\tAudio did not pause with space\n"); 1590 | PauseAudio(); 1591 | } 1592 | 1593 | //Case 2: Seek 1594 | if (AudioPlaying()) 1595 | { 1596 | result.Result = ResultType.Half; 1597 | result.AddInfo("\tAudio was playing when it shouldn't have been\n"); 1598 | } 1599 | driver.SendTabs(audioId, 3); 1600 | var initial = GetAudioElapsed(); 1601 | driver.SendSpecialKeys(audioId, WebDriverKey.Arrow_right); 1602 | if (!WaitForCondition(AudioElapsed, initial + 10)) 1603 | { 1604 | result.Result = ResultType.Half; 1605 | result.AddInfo("\tAudio did not skip forward with arrow right\n"); 1606 | } 1607 | driver.SendSpecialKeys(audioId, WebDriverKey.Arrow_left); 1608 | if (!WaitForCondition(AudioElapsed, initial)) 1609 | { 1610 | result.Result = ResultType.Half; 1611 | result.AddInfo("\tAudio did not skip back with arrow left\n"); 1612 | } 1613 | 1614 | //Case 3: Volume and mute 1615 | Javascript.ClearFocus(driver, 0); 1616 | driver.SendTabs(audioId, 5); 1617 | initial = GetAudioVolume(); 1618 | driver.SendSpecialKeys(audioId, WebDriverKey.Arrow_down); 1619 | if (!WaitForCondition(AudioVolume, initial - .05)) 1620 | { 1621 | result.Result = ResultType.Half; 1622 | result.AddInfo("\tVolume did not decrease with arrow down\n"); 1623 | } 1624 | 1625 | driver.SendSpecialKeys(audioId, WebDriverKey.Arrow_up); 1626 | if (!WaitForCondition(AudioVolume, initial)) 1627 | { 1628 | result.Result = ResultType.Half; 1629 | result.AddInfo("\tVolume did not increase with arrow up\n"); 1630 | } 1631 | 1632 | driver.SendSpecialKeys(audioId, WebDriverKey.Enter); 1633 | if (!WaitForCondition(AudioMuted)) 1634 | { 1635 | result.Result = ResultType.Half; 1636 | result.AddInfo("\tAudio was not muted by enter on the volume control\n"); 1637 | } 1638 | driver.SendSpecialKeys(audioId, WebDriverKey.Enter); 1639 | if (!WaitForCondition(AudioMuted, reverse: true)) 1640 | { 1641 | result.Result = ResultType.Half; 1642 | result.AddInfo("\tAudio was not unmuted by enter on the volume control\n"); 1643 | } 1644 | } 1645 | 1646 | /// 1647 | /// Test all date/time elements except for datetime-local, which is tested by the 1648 | /// amended method below. 1649 | /// 1650 | /// A count of the number of fields to test 1651 | /// A count of the number of output fields to test 1652 | /// 1653 | public static Action, DriverManager, List, TestCaseResultExt> CheckCalendar(int fields, int outputFields = -1) 1654 | { 1655 | return ((elements, driver, ids, result) => 1656 | { 1657 | //set to the number of fields by default 1658 | outputFields = outputFields == -1 ? fields : outputFields; 1659 | 1660 | var previousControllerForElements = new HashSet(); 1661 | foreach (var id in ids.Take(1)) 1662 | { 1663 | driver.SendSpecialKeys(id, new List { 1664 | WebDriverKey.Enter, 1665 | WebDriverKey.Escape, 1666 | WebDriverKey.Enter, 1667 | WebDriverKey.Enter });//Make sure that the element has focus (gets around weirdness in WebDriver) 1668 | 1669 | Func DateValue = () => (string)driver.ExecuteScript("return document.getElementById('" + id + "').value", 0); 1670 | Func ActiveElement = () => (string)driver.ExecuteScript("return document.activeElement.id", 0); 1671 | 1672 | var today = DateValue(); 1673 | 1674 | //Open the menu 1675 | driver.SendSpecialKeys(id, new List { 1676 | WebDriverKey.Escape, 1677 | WebDriverKey.Enter, 1678 | WebDriverKey.Wait }); 1679 | 1680 | //Check ControllerFor 1681 | var controllerForElements = elements.Where(e => e.CurrentControllerFor != null && e.CurrentControllerFor.Length > 0).ToList().Select(element => elements.IndexOf(element)); 1682 | if (controllerForElements.All(element => previousControllerForElements.Contains(element))) 1683 | { 1684 | result.Result = ResultType.Half; 1685 | result.AddInfo("\nElement controller for not set for id: " + id); 1686 | } 1687 | 1688 | //Change each field in the calendar 1689 | for (int i = 0; i < fields; i++) 1690 | { 1691 | driver.SendSpecialKeys(id, new List { 1692 | WebDriverKey.Arrow_down, 1693 | WebDriverKey.Tab }); 1694 | } 1695 | 1696 | 1697 | //Close the menu (only necessary for time) 1698 | driver.SendSpecialKeys(id, WebDriverKey.Enter); 1699 | 1700 | //Get the altered value, which should be one off the default 1701 | //for each field 1702 | var newdate = DateValue(); 1703 | var newdatesplit = newdate.Split('-', ':'); 1704 | var todaysplit = today.Split('-', ':'); 1705 | 1706 | //ensure that all fields have been changed 1707 | for (int i = 0; i < outputFields; i++) 1708 | { 1709 | if (newdatesplit[i] == todaysplit[i]) 1710 | { 1711 | result.Result = ResultType.Half; 1712 | result.AddInfo("\nNot all fields were changed by keyboard interaction."); 1713 | } 1714 | } 1715 | 1716 | var fieldTabs = new List(); 1717 | for (var i = 0; i < fields; i++) 1718 | { 1719 | fieldTabs.Add(WebDriverKey.Tab); 1720 | } 1721 | 1722 | var keys = new List { WebDriverKey.Enter, WebDriverKey.Wait }; 1723 | keys.AddRange(fieldTabs); 1724 | driver.SendSpecialKeys(id, keys); 1725 | 1726 | var initial = ""; 1727 | 1728 | //Check that the accept and cancel buttons are in the tab order 1729 | if (ActiveElement() != id) 1730 | { 1731 | result.Result = ResultType.Half; 1732 | result.AddInfo("\nUnable to get to accept/dismiss buttons by tab"); 1733 | } 1734 | else//only try to use the buttons if they're there 1735 | { 1736 | initial = DateValue(); 1737 | //**Dismiss button** 1738 | //Open the dialog, change a field, tab to cancel button, activate it with space, 1739 | //check that tabbing moves to the previous button (on the page not the dialog) 1740 | keys = new List { 1741 | WebDriverKey.Escape, 1742 | WebDriverKey.Enter, 1743 | WebDriverKey.Wait, 1744 | WebDriverKey.Arrow_down }; 1745 | keys.AddRange(fieldTabs); 1746 | keys.AddRange(new List { 1747 | WebDriverKey.Tab, 1748 | WebDriverKey.Space, 1749 | WebDriverKey.Shift, 1750 | WebDriverKey.Tab, 1751 | WebDriverKey.Shift}); 1752 | driver.SendSpecialKeys(id, keys); 1753 | if (initial != DateValue() || ActiveElement() == id) 1754 | { 1755 | result.Result = ResultType.Half; 1756 | result.AddInfo("\nUnable to cancel with dismiss button via space"); 1757 | } 1758 | 1759 | //do the same as above, but activate the button with enter this time 1760 | keys = new List { 1761 | WebDriverKey.Escape, 1762 | WebDriverKey.Enter, 1763 | WebDriverKey.Wait, 1764 | WebDriverKey.Arrow_down }; 1765 | keys.AddRange(fieldTabs); 1766 | keys.AddRange(new List { 1767 | WebDriverKey.Tab, 1768 | WebDriverKey.Enter, 1769 | WebDriverKey.Shift, 1770 | WebDriverKey.Tab, 1771 | WebDriverKey.Shift }); 1772 | driver.SendSpecialKeys(id, keys); 1773 | if (initial != DateValue() || ActiveElement() == id) 1774 | { 1775 | result.Result = ResultType.Half; 1776 | result.AddInfo("\nUnable to cancel with dismiss button via enter"); 1777 | } 1778 | 1779 | //**Accept button** 1780 | initial = DateValue(); 1781 | 1782 | //Open the dialog, change a field, tab to accept button, activate it with space, 1783 | //send tab (since the dialog should be closed, this will transfer focus to the next 1784 | //input-color button) 1785 | keys = new List { 1786 | WebDriverKey.Escape, 1787 | WebDriverKey.Enter, 1788 | WebDriverKey.Wait, 1789 | WebDriverKey.Arrow_down }; 1790 | keys.AddRange(fieldTabs); 1791 | keys.AddRange(new List { 1792 | WebDriverKey.Space, 1793 | WebDriverKey.Tab }); 1794 | driver.SendSpecialKeys(id, keys); 1795 | if (initial == DateValue() || ActiveElement() == id) 1796 | { 1797 | result.Result = ResultType.Half; 1798 | result.AddInfo("\nUnable to accept with accept button via space. Found value: " + DateValue() + " with active element: " + ActiveElement()); 1799 | } 1800 | 1801 | initial = DateValue();//the value hopefully changed above, but just to be safe 1802 | 1803 | //Open the dialog, tab to hue, change hue, tab to accept button, activate it with enter 1804 | //We don't have to worry about why the dialog closed here (button or global enter) 1805 | keys = new List { 1806 | WebDriverKey.Escape, 1807 | WebDriverKey.Enter, 1808 | WebDriverKey.Wait, 1809 | WebDriverKey.Arrow_down }; 1810 | keys.AddRange(fieldTabs); 1811 | keys.AddRange(new List { 1812 | WebDriverKey.Enter, 1813 | WebDriverKey.Tab }); 1814 | driver.SendSpecialKeys(id, keys); 1815 | if (initial == DateValue() || ActiveElement() == id) 1816 | { 1817 | result.Result = ResultType.Half; 1818 | result.AddInfo("\nUnable to accept with accept button via enter. Found value: " + DateValue() + " with active element: " + ActiveElement()); 1819 | } 1820 | } 1821 | 1822 | //open with space, close with enter 1823 | initial = DateValue(); 1824 | driver.SendSpecialKeys(id, new List { 1825 | WebDriverKey.Escape, 1826 | WebDriverKey.Space, 1827 | WebDriverKey.Arrow_down, 1828 | WebDriverKey.Enter }); 1829 | if (DateValue() == initial) 1830 | { 1831 | result.AddInfo("\nUnable to open dialog with space"); 1832 | } 1833 | } 1834 | 1835 | foreach (var element in elements) 1836 | { 1837 | var patternName = "ValuePattern"; 1838 | var patterned = GetPattern(patternName, element); 1839 | if (patterned == null) 1840 | { 1841 | result.Result = ResultType.Half; 1842 | result.AddInfo("\nElement did not support " + patternName); 1843 | } 1844 | else 1845 | { 1846 | if (patterned.CurrentValue == null || patterned.CurrentValue == "") 1847 | { 1848 | result.Result = ResultType.Half; 1849 | result.AddInfo("\nElement did not have value.value set"); 1850 | } 1851 | } 1852 | } 1853 | }); 1854 | } 1855 | 1856 | /// 1857 | /// Test the datetime-local input element with an amended version of the method 1858 | /// above. 1859 | /// 1860 | /// 1861 | public static Action, DriverManager, List, TestCaseResultExt> CheckDatetimeLocal() 1862 | { 1863 | return ((elements, driver, ids, result) => 1864 | { 1865 | var inputFields = new List { 3, 3 }; 1866 | var outputFields = 5; 1867 | 1868 | var previousControllerForElements = new HashSet(); 1869 | foreach (var id in ids.Take(1)) 1870 | { 1871 | driver.SendSpecialKeys(id, new List { 1872 | WebDriverKey.Enter, 1873 | WebDriverKey.Enter, 1874 | WebDriverKey.Escape });//Make sure that the element has focus (gets around weirdness in WebDriver) 1875 | 1876 | Func DateValue = () => (string)driver.ExecuteScript("return document.getElementById('" + id + "').value", 0); 1877 | Func ActiveElement = () => (string)driver.ExecuteScript("return document.activeElement.id", 0); 1878 | 1879 | driver.SendSpecialKeys(id, new List { 1880 | WebDriverKey.Tab, 1881 | WebDriverKey.Enter, 1882 | WebDriverKey.Enter, 1883 | WebDriverKey.Tab, 1884 | WebDriverKey.Enter, 1885 | WebDriverKey.Enter }); 1886 | var today = DateValue(); 1887 | 1888 | //Open the date menu 1889 | driver.SendSpecialKeys(id, new List { 1890 | WebDriverKey.Shift, 1891 | WebDriverKey.Tab, 1892 | WebDriverKey.Enter, 1893 | WebDriverKey.Wait }); 1894 | 1895 | //Check ControllerFor 1896 | var controllerForElements = elements.Where(e => e.CurrentControllerFor != null && e.CurrentControllerFor.Length > 0).ToList().Select(element => elements.IndexOf(element)); 1897 | if (controllerForElements.All(element => previousControllerForElements.Contains(element))) 1898 | { 1899 | result.Result = ResultType.Half; 1900 | result.AddInfo("\nElement controller for not set for id: " + id); 1901 | } 1902 | 1903 | for (var i = 0; i < inputFields.Count;)//NB ++i later 1904 | { 1905 | //Change each field in the calendar 1906 | for (var j = 0; j < inputFields[i]; j++) 1907 | { 1908 | driver.SendSpecialKeys(id, new List { WebDriverKey.Arrow_down, 1909 | WebDriverKey.Tab }); 1910 | } 1911 | driver.SendSpecialKeys(id, new List { 1912 | WebDriverKey.Enter, 1913 | WebDriverKey.Wait, 1914 | WebDriverKey.Tab }); 1915 | // send Enter to open menu 1916 | if (++i != inputFields.Count) 1917 | { 1918 | // expand menu 1919 | driver.SendSpecialKeys(id, WebDriverKey.Enter); 1920 | } 1921 | } 1922 | 1923 | //Get the altered value, which should be one off the default 1924 | //for each field 1925 | var newdate = DateValue(); 1926 | var newdatesplit = newdate.Split('-', ':', 'T'); 1927 | var todaysplit = today.Split('-', ':', 'T'); 1928 | 1929 | //ensure that all fields have been changed 1930 | for (int i = 0; i < outputFields; i++) 1931 | { 1932 | if (newdatesplit[i] == todaysplit[i]) 1933 | { 1934 | result.Result = ResultType.Half; 1935 | result.AddInfo("\nNot all fields were changed by keyboard interaction."); 1936 | } 1937 | } 1938 | 1939 | var initial = ""; 1940 | 1941 | for (var i = 0; i < inputFields.Count(); i++) 1942 | { 1943 | var fieldTabs = new List(); 1944 | for (var j = 0; j < inputFields[0]; j++) 1945 | { 1946 | fieldTabs.Add(WebDriverKey.Tab); 1947 | } 1948 | 1949 | var secondPass = i == 1; 1950 | 1951 | initial = DateValue(); 1952 | //**Dismiss button** 1953 | //Open the dialog, change a field, tab to cancel button, activate it with space, 1954 | //check that tabbing moves to the previous button (on the page not the dialog) 1955 | var keys = new List(); 1956 | if (secondPass) 1957 | { 1958 | keys = new List 1959 | { 1960 | WebDriverKey.Tab, 1961 | WebDriverKey.Enter, 1962 | WebDriverKey.Wait, 1963 | WebDriverKey.Arrow_down 1964 | }; 1965 | keys.AddRange(fieldTabs); 1966 | keys.AddRange(new List { 1967 | WebDriverKey.Tab, 1968 | WebDriverKey.Wait, 1969 | WebDriverKey.Space, 1970 | WebDriverKey.Wait, 1971 | WebDriverKey.Shift, 1972 | WebDriverKey.Tab, 1973 | WebDriverKey.Shift }); 1974 | } 1975 | else//same as above except without the tab to start 1976 | { 1977 | keys = new List 1978 | { 1979 | WebDriverKey.Enter, 1980 | WebDriverKey.Wait, 1981 | WebDriverKey.Arrow_down 1982 | }; 1983 | keys.AddRange(fieldTabs); 1984 | keys.AddRange(new List { 1985 | WebDriverKey.Tab, 1986 | WebDriverKey.Wait, 1987 | WebDriverKey.Space, 1988 | WebDriverKey.Wait, 1989 | WebDriverKey.Shift, 1990 | WebDriverKey.Tab, 1991 | WebDriverKey.Shift }); 1992 | } 1993 | driver.SendSpecialKeys(id, keys); 1994 | 1995 | if (initial != DateValue()) 1996 | { 1997 | result.Result = ResultType.Half; 1998 | result.AddInfo("\nUnable to cancel with dismiss button via space"); 1999 | } 2000 | 2001 | //do the same as above, but activate the button with enter this time 2002 | if (secondPass) 2003 | { 2004 | keys = new List 2005 | { 2006 | WebDriverKey.Tab, 2007 | WebDriverKey.Enter, 2008 | WebDriverKey.Wait, 2009 | WebDriverKey.Arrow_down 2010 | }; 2011 | keys.AddRange(fieldTabs); 2012 | keys.AddRange(new List { 2013 | WebDriverKey.Tab, 2014 | WebDriverKey.Wait, 2015 | WebDriverKey.Enter, 2016 | WebDriverKey.Wait, 2017 | WebDriverKey.Shift, 2018 | WebDriverKey.Tab, 2019 | WebDriverKey.Shift }); 2020 | } 2021 | else//same as above except without the tab to start 2022 | { 2023 | keys = new List 2024 | { 2025 | WebDriverKey.Enter, 2026 | WebDriverKey.Wait, 2027 | WebDriverKey.Arrow_down 2028 | }; 2029 | keys.AddRange(fieldTabs); 2030 | keys.AddRange(new List { 2031 | WebDriverKey.Tab, 2032 | WebDriverKey.Wait, 2033 | WebDriverKey.Enter, 2034 | WebDriverKey.Wait, 2035 | WebDriverKey.Shift, 2036 | WebDriverKey.Tab, 2037 | WebDriverKey.Shift }); 2038 | } 2039 | driver.SendSpecialKeys(id, keys); 2040 | 2041 | if (initial != DateValue()) 2042 | { 2043 | result.Result = ResultType.Half; 2044 | result.AddInfo("\nUnable to cancel with dismiss button via enter"); 2045 | } 2046 | 2047 | 2048 | //**Accept button** 2049 | initial = DateValue(); 2050 | 2051 | //Open the dialog, change a field, tab to accept button, activate it with space, 2052 | //send tab (since the dialog should be closed, this will transfer focus to the next 2053 | //button) 2054 | if (secondPass) 2055 | { 2056 | keys = new List { 2057 | WebDriverKey.Escape, 2058 | WebDriverKey.Tab, 2059 | WebDriverKey.Tab, 2060 | WebDriverKey.Wait, 2061 | WebDriverKey.Enter, 2062 | WebDriverKey.Wait, 2063 | WebDriverKey.Arrow_down, 2064 | WebDriverKey.Wait }; 2065 | keys.AddRange(fieldTabs); 2066 | keys.AddRange(new List { 2067 | WebDriverKey.Wait, 2068 | WebDriverKey.Space, 2069 | WebDriverKey.Wait, 2070 | WebDriverKey.Tab}); 2071 | } 2072 | else 2073 | { 2074 | keys = new List { 2075 | WebDriverKey.Escape, 2076 | WebDriverKey.Tab, 2077 | WebDriverKey.Wait, 2078 | WebDriverKey.Enter, 2079 | WebDriverKey.Wait, 2080 | WebDriverKey.Arrow_down, 2081 | WebDriverKey.Wait }; 2082 | keys.AddRange(fieldTabs); 2083 | keys.AddRange(new List { 2084 | WebDriverKey.Wait, 2085 | WebDriverKey.Space, 2086 | WebDriverKey.Wait, 2087 | WebDriverKey.Tab}); 2088 | } 2089 | driver.SendSpecialKeys(id, keys); 2090 | if (initial == DateValue()) 2091 | { 2092 | result.Result = ResultType.Half; 2093 | result.AddInfo("\nUnable to accept with accept button via space"); 2094 | } 2095 | 2096 | initial = DateValue();//the value hopefully changed above, but just to be safe 2097 | 2098 | //Open the dialog, tab to hue, change hue, tab to accept button, activate it with enter 2099 | //We don't have to worry about why the dialog closed here (button or global enter) 2100 | if (secondPass) 2101 | { 2102 | keys = new List { 2103 | WebDriverKey.Escape, 2104 | WebDriverKey.Tab, 2105 | WebDriverKey.Wait, 2106 | WebDriverKey.Enter, 2107 | WebDriverKey.Wait, 2108 | WebDriverKey.Arrow_down }; 2109 | keys.AddRange(fieldTabs); 2110 | keys.AddRange(new List { 2111 | WebDriverKey.Enter, 2112 | WebDriverKey.Tab}); 2113 | } 2114 | else 2115 | { 2116 | keys = new List { 2117 | WebDriverKey.Escape, 2118 | WebDriverKey.Wait, 2119 | WebDriverKey.Enter, 2120 | WebDriverKey.Wait, 2121 | WebDriverKey.Arrow_down }; 2122 | keys.AddRange(fieldTabs); 2123 | keys.AddRange(new List { 2124 | WebDriverKey.Enter, 2125 | WebDriverKey.Tab}); 2126 | } 2127 | driver.SendSpecialKeys(id, keys); 2128 | if (initial == DateValue()) 2129 | { 2130 | result.Result = ResultType.Half; 2131 | result.AddInfo("\nUnable to accept with accept button via enter"); 2132 | } 2133 | } 2134 | 2135 | //open with space, close with enter 2136 | initial = DateValue(); 2137 | driver.SendSpecialKeys(id, new List { 2138 | WebDriverKey.Escape, 2139 | WebDriverKey.Space, 2140 | WebDriverKey.Wait, 2141 | WebDriverKey.Wait, 2142 | WebDriverKey.Arrow_down, 2143 | WebDriverKey.Enter }); 2144 | if (DateValue() == initial) 2145 | { 2146 | result.Result = ResultType.Half; 2147 | result.AddInfo("\nUnable to open dialog with space"); 2148 | } 2149 | } 2150 | 2151 | foreach (var element in elements) 2152 | { 2153 | var patternName = "ValuePattern"; 2154 | var patterned = GetPattern(patternName, element); 2155 | if (patterned == null) 2156 | { 2157 | result.Result = ResultType.Half; 2158 | result.AddInfo("\nElement did not support " + patternName); 2159 | } 2160 | else 2161 | { 2162 | if (patterned.CurrentValue == null || patterned.CurrentValue == "") 2163 | { 2164 | result.Result = ResultType.Half; 2165 | result.AddInfo("\nElement did not have value.value set"); 2166 | } 2167 | } 2168 | } 2169 | }); 2170 | } 2171 | 2172 | /// 2173 | /// Func factory for checking that when invalid input is entered into a form, 2174 | /// an error message appears. 2175 | /// 2176 | /// 2177 | public static Action, DriverManager, List, TestCaseResultExt> CheckValidation() 2178 | { 2179 | return (elements, driver, ids, result) => 2180 | { 2181 | //The indices of the elements that have been found to be invalid before 2182 | foreach (var id in ids.Take(1)) 2183 | { 2184 | Javascript.ScrollIntoView(driver, 0); 2185 | 2186 | driver.SendKeys(id, "invalid"); 2187 | driver.SendSpecialKeys(id, WebDriverKey.Enter); 2188 | 2189 | //Everything that is invalid on the page 2190 | //We search by both with an OR condition because it gives a better chance to 2191 | //find elements that are partially correct. 2192 | var invalid = elements.Where(e => 2193 | e.CurrentIsDataValidForForm == 0); 2194 | 2195 | if (invalid.Count() > 1) 2196 | { 2197 | throw new Exception("Test assumption failed, multiple elements were invalid"); 2198 | } 2199 | 2200 | if (invalid.Count() < 1) 2201 | { 2202 | result.Result = ResultType.Half; 2203 | result.AddInfo("\nElement failed to validate improper input"); 2204 | } 2205 | 2206 | var invalidElement = invalid.First(); 2207 | 2208 | if (!WaitForCondition(() => invalidElement.CurrentControllerFor.Length == 1, () => driver.SendSpecialKeys(id, WebDriverKey.Enter))) 2209 | { 2210 | result.Result = ResultType.Half; 2211 | result.AddInfo("\n" + id + " did not have 1 ControllerFor " + invalidElement.CurrentControllerFor.Length); 2212 | return; 2213 | } 2214 | 2215 | if (invalidElement.CurrentIsDataValidForForm != 0) 2216 | { 2217 | result.Result = ResultType.Half; 2218 | result.AddInfo("\nElement did not have IsDataValidForForm set to false"); 2219 | } 2220 | 2221 | if (invalidElement.CurrentHelpText == null || invalidElement.CurrentHelpText.Length == 0) 2222 | { 2223 | result.Result = ResultType.Half; 2224 | result.AddInfo("\nElement did not have HelpText"); 2225 | } 2226 | 2227 | try 2228 | { 2229 | var helpPane = invalidElement.CurrentControllerFor.GetElement(0); 2230 | if (GetControlTypeFromCode(helpPane.CurrentControlType) != UIAControlType.Pane) 2231 | { 2232 | result.Result = ResultType.Half; 2233 | result.AddInfo("\nError message did not have correct ControlType"); 2234 | } 2235 | } 2236 | catch 2237 | { 2238 | result.Result = ResultType.Half; 2239 | result.AddInfo("\nUnable to get controller for"); 2240 | } 2241 | } 2242 | }; 2243 | } 2244 | 2245 | /// 2246 | /// Func factory for checking that the required child elements are in the accessibility tree. 2247 | /// 2248 | /// The names of the elements to search for 2249 | /// A Func that can be used to verify whether the elements in the list are child elements 2250 | public static Action, DriverManager, List, TestCaseResultExt> CheckChildNames(List requiredNames, 2251 | bool strict = false, 2252 | Func searchStrategy = null) 2253 | { 2254 | return (elements, driver, ids, result) => 2255 | { 2256 | foreach (var element in elements) 2257 | { 2258 | var names = element.GetChildNames(searchStrategy); 2259 | 2260 | var expectedNotFound = requiredNames.Where(rn => !names.Contains(rn)).ToList();//get a list of all required names not found 2261 | var foundNotExpected = names.Where(n => !requiredNames.Contains(n)).ToList();//get a list of all found names that weren't required 2262 | 2263 | if (strict && names.Count() != requiredNames.Count) 2264 | { 2265 | result.Result = ResultType.Half; 2266 | result.AddInfo( 2267 | expectedNotFound.Any() ? "\n" + 2268 | expectedNotFound.Aggregate((a, b) => a + ", " + b) + 2269 | (expectedNotFound.Count() > 1 ? 2270 | " were expected as names but not found. " : 2271 | " was expected as a name but not found. ") 2272 | : ""); 2273 | result.AddInfo( 2274 | foundNotExpected.Any() ? "\n" + 2275 | foundNotExpected.Aggregate((a, b) => a + ", " + b) + 2276 | (foundNotExpected.Count() > 1 ? 2277 | " were found as names but not expected. " : 2278 | " was found as a name but not expected. ") 2279 | : ""); 2280 | } 2281 | } 2282 | }; 2283 | } 2284 | 2285 | /// 2286 | /// Check that the clear button can be tabbed to and that it can be activated with space 2287 | /// 2288 | /// 2289 | public static Action, DriverManager, List, TestCaseResultExt> CheckClearButton() 2290 | { 2291 | return (elements, driver, ids, result) => 2292 | { 2293 | Func inputValue = (id) => (string)driver.ExecuteScript("return document.getElementById('" + id + "').value", 0); 2294 | Action clearInput = (id) => driver.ExecuteScript("document.getElementById('" + id + "').value = ''", 0); 2295 | 2296 | foreach (var id in ids.Take(1)) 2297 | { 2298 | //Enter something, tab to the clear button, clear with space 2299 | driver.SendKeys(id, "x"); 2300 | driver.SendSpecialKeys(id, WebDriverKey.Wait); 2301 | if (!elements.Any(e => e.GetAllDescendents().Any(d => d.CurrentName.ToLowerInvariant().Contains("clear value")))) 2302 | { 2303 | result.Result = ResultType.Half; 2304 | result.AddInfo("\nUnable to find a clear button as a child of any element"); 2305 | } 2306 | //Don't leave input which could cause problems with other tests 2307 | clearInput(id); 2308 | } 2309 | }; 2310 | } 2311 | 2312 | /// 2313 | /// Wait for a condition that tests a double value 2314 | /// 2315 | /// The condition checker 2316 | /// The double value to look for 2317 | /// true if the condition passes, false otherwise 2318 | public static bool WaitForCondition(Func conditionCheck, double value, int attempts = 20) 2319 | { 2320 | for (var i = 0; i < attempts; i++) 2321 | { 2322 | Thread.Sleep(500); 2323 | if (conditionCheck(value)) 2324 | { 2325 | return true; 2326 | } 2327 | } 2328 | return false; 2329 | } 2330 | 2331 | /// 2332 | /// Wait for a condition that tests a double value 2333 | /// 2334 | /// The condition checker 2335 | /// Whether to reverse the result 2336 | /// How many times to check 2337 | /// true if the condition passes, false otherwise 2338 | public static bool WaitForCondition(Func conditionCheck, Action waitAction = null, bool reverse = false, int attempts = 20) 2339 | { 2340 | for (var i = 0; i < attempts; i++) 2341 | { 2342 | Thread.Sleep(500); 2343 | if (reverse ? !conditionCheck() : conditionCheck()) 2344 | { 2345 | return true; 2346 | } 2347 | waitAction?.Invoke(); 2348 | } 2349 | return false; 2350 | } 2351 | } 2352 | } 2353 | -------------------------------------------------------------------------------- /TestStrategy.cs: -------------------------------------------------------------------------------- 1 | using Interop.UIAutomationCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Microsoft.Edge.A11y 7 | { 8 | /// 9 | /// A strategy for testing Edge Accessibility as scored at 10 | /// http://html5accessibility.com/ 11 | /// 12 | internal abstract class TestStrategy 13 | { 14 | protected DriverManager _driverManager; 15 | protected string _RepositoryPath; 16 | protected string _FileSuffix; 17 | 18 | private string BuildTestUrl(string testName) 19 | { 20 | return _RepositoryPath + testName; 21 | } 22 | 23 | /// 24 | /// This method is pretty minimal, most of the work is done in the 25 | /// DefaultTestCase method below. 26 | /// 27 | /// An object that describes the test 28 | /// 29 | public IEnumerable Execute(TestData testData) 30 | { 31 | _driverManager.NavigateToUrl(BuildTestUrl(testData.TestName + _FileSuffix)); 32 | return TestElement(testData); 33 | } 34 | 35 | /// 36 | /// This handles most of the work of the test cases. 37 | /// 38 | /// N.B. all the test case results are returned in pairs, since we need to be 39 | /// able to give half scores for certain results. 40 | /// 41 | /// An object which stores information about the 42 | /// expected results 43 | /// 44 | /// 45 | internal abstract IEnumerable TestElement(TestData testData); 46 | 47 | /// 48 | /// This wrapper is used to report 100% for a test 49 | /// 50 | /// 51 | /// 52 | protected List Pass(string name) 53 | { 54 | return new List { 55 | new TestCaseResult{ 56 | Result = ResultType.Pass, 57 | Name = name + "-1", 58 | }, 59 | new TestCaseResult{ 60 | Result = ResultType.Pass, 61 | Name = name + "-2" 62 | } 63 | }; 64 | } 65 | 66 | /// 67 | /// This wrapper is used to report 0% for a test 68 | /// 69 | /// 70 | /// 71 | protected List Fail(string name, string cause) 72 | { 73 | return new List { 74 | new TestCaseResult{ 75 | Result = ResultType.Fail, 76 | Name = name + "-1", 77 | MoreInfo = cause 78 | }, 79 | new TestCaseResult{ 80 | Result = ResultType.Fail, 81 | Name = name + "-2", 82 | MoreInfo = cause 83 | } 84 | }; 85 | } 86 | 87 | /// 88 | /// This wrapper is used to report 50% for a test 89 | /// 90 | /// 91 | /// 92 | protected List Half(string name, string cause) 93 | { 94 | return new List { 95 | new TestCaseResult{ 96 | Result = ResultType.Pass, 97 | Name = name + "-1" 98 | }, 99 | new TestCaseResult{ 100 | Result = ResultType.Fail, 101 | Name = name + "-2", 102 | MoreInfo = cause 103 | } 104 | }; 105 | } 106 | 107 | /// 108 | /// This is used to skip a test without influencing the overall score 109 | /// 110 | /// 111 | /// 112 | protected IEnumerable Skip(string testName) 113 | { 114 | return Enumerable.Empty(); 115 | } 116 | 117 | internal void Close() 118 | { 119 | _driverManager.Close(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /diff_runs.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This will output the difference between runs of A11y, by default the first and last runs 4 | Use the LastTwoRuns switch to output the two most recent runs instead 5 | 6 | .PARAMETER LastTwoRuns 7 | Add this switch if you want to diff the last two runs instead of the first and last 8 | #> 9 | param([switch]$LastTwoRuns) 10 | 11 | $scores = Import-Csv .\scores.csv 12 | 13 | switch($LastTwoRuns){ 14 | $true {$firstRun = $scores.Length - 2; break} 15 | default {$firstRun = 0; break} 16 | } 17 | 18 | if($scores.Length -gt 1){ 19 | $scores | 20 | gm -MemberType NoteProperty | 21 | select -expand Name | 22 | % {Compare-Object $scores[$firstRun] $scores[$scores.Length - 1] -Property $_ | 23 | Format-Table 24 | } 25 | } else { 26 | "Run at least twice to diff" 27 | } 28 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | ------------------------------------------ START OF LICENSE ----------------------------------------- 2 | A11y 3 | Copyright (c) Microsoft Corporation 4 | All rights reserved. 5 | MIT License 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | software and associated documentation files (the ""Software""), to deal in the Software 8 | without restriction, including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit 10 | persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY 16 | OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | ----------------------------------------------- END OF LICENSE ------------------------------------------ 23 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | if(-Not (Test-Path .\nuget.exe)){ 2 | (New-Object net.WebClient).DownloadFile('https://nuget.org/nuget.exe', 'nuget.exe') 3 | } 4 | 5 | .\nuget.exe restore A11y.sln -Verbosity quiet 6 | 7 | $msbuild = Get-Item C:\Windows\Microsoft.NET\Framework\v4*\MSBuild\ 8 | & $msbuild .\A11y.sln /v:q /nologo 9 | 10 | .\bin\Debug\Microsoft.Edge.A11y.exe $args[0] 11 | --------------------------------------------------------------------------------