From 6093d8ff57d4a9992e7524f30c3d4e363962c278 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Wed, 1 May 2024 08:15:39 -0700 Subject: [PATCH 01/23] Allow callers to pass ScanOptions specifying the Native Window Handle for the root element of the UIA subtree that should be scanned. --- src/Automation/Data/ScanOptions.cs | 5 +++++ src/Automation/Interfaces/ITargetElementLocator.cs | 1 + src/Automation/Scanner.cs | 1 + src/Automation/TargetElementLocator.cs | 6 +++++- src/Desktop/UIAutomation/A11yAutomation.cs | 12 +++++++++--- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Automation/Data/ScanOptions.cs b/src/Automation/Data/ScanOptions.cs index 381fce504..87bf8ba98 100644 --- a/src/Automation/Data/ScanOptions.cs +++ b/src/Automation/Data/ScanOptions.cs @@ -14,6 +14,11 @@ public class ScanOptions /// public string ScanId { get; } + /// + /// The window handle for the root of the UIA subtree to scan. + /// + public System.IntPtr WindowHandle { get; set; } = System.IntPtr.Zero; + /// /// Constructor /// diff --git a/src/Automation/Interfaces/ITargetElementLocator.cs b/src/Automation/Interfaces/ITargetElementLocator.cs index f8ca117fe..5dcd8a2ff 100644 --- a/src/Automation/Interfaces/ITargetElementLocator.cs +++ b/src/Automation/Interfaces/ITargetElementLocator.cs @@ -10,5 +10,6 @@ namespace Axe.Windows.Automation internal interface ITargetElementLocator { IEnumerable LocateRootElements(int processId, IActionContext actionContext); + void SetRootWindowHandle(System.IntPtr windowHandle); } // interface } // namespace diff --git a/src/Automation/Scanner.cs b/src/Automation/Scanner.cs index 1475a9f8c..3e188b351 100644 --- a/src/Automation/Scanner.cs +++ b/src/Automation/Scanner.cs @@ -42,6 +42,7 @@ private void HandleScanOptions(ScanOptions scanOptions) { scanOptions = scanOptions ?? DefaultScanOptions; _scanTools.OutputFileHelper.SetScanId(scanOptions.ScanId); + _scanTools.TargetElementLocator.SetRootWindowHandle(scanOptions.WindowHandle); } } // class } // namespace diff --git a/src/Automation/TargetElementLocator.cs b/src/Automation/TargetElementLocator.cs index 6be04dfe9..2e9cea266 100644 --- a/src/Automation/TargetElementLocator.cs +++ b/src/Automation/TargetElementLocator.cs @@ -14,11 +14,13 @@ namespace Axe.Windows.Automation { class TargetElementLocator : ITargetElementLocator { + private IntPtr _rootWindowHandle; + public IEnumerable LocateRootElements(int processId, IActionContext actionContext) { try { - var desktopElements = A11yAutomation.ElementsFromProcessId(processId, actionContext.DesktopDataContext); + var desktopElements = A11yAutomation.ElementsFromProcessId(processId, _rootWindowHandle, actionContext.DesktopDataContext); return GetA11yElementsFromDesktopElements(desktopElements); } catch (Exception ex) @@ -38,5 +40,7 @@ private static IEnumerable GetA11yElementsFromDesktopElements(IEnum #pragma warning restore CA2000 } } + + public void SetRootWindowHandle(IntPtr rootWindowHandle) => _rootWindowHandle = rootWindowHandle; } } diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index 773fc08e7..731354ab5 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -109,10 +109,14 @@ private static void ReleaseElements(IUIAutomationTreeWalker walker, params IList /// /// Get DesktopElements based on Process Id. /// - /// + /// The whoese elements should be retrieved. + /// + /// The window handle for the that should be used + /// as the root of the sub-tree to be scanned. + /// /// The data context /// return null if we fail to get elements by process Id - public static IEnumerable ElementsFromProcessId(int pid, DesktopDataContext dataContext) + public static IEnumerable ElementsFromProcessId(int pid, IntPtr rootWindowHandle, DesktopDataContext dataContext) { if (dataContext == null) throw new ArgumentNullException(nameof(dataContext)); @@ -130,7 +134,9 @@ public static IEnumerable ElementsFromProcessId(int pid, Desktop // exists inside UIAutomation.GetRootElement, and this works around the problem. lock (LockObject) { - root = dataContext.A11yAutomation.UIAutomation.GetRootElement(); + root = rootWindowHandle == IntPtr.Zero + ? dataContext.A11yAutomation.UIAutomation.GetRootElement() + : dataContext.A11yAutomation.UIAutomation.ElementFromHandle(rootWindowHandle); } matchingElements = dataContext.A11yAutomation.FindProcessMatchingChildrenOrGrandchildren(root, pid); elements = ElementsFromUIAElements(matchingElements, dataContext); From 5e1bdda4cc8ddc513ca8dc4ea43d9c481a31aa85 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Wed, 1 May 2024 13:10:05 -0700 Subject: [PATCH 02/23] Add Integration Test for Scan scoped via HWND of a sub-tree root element. --- .../AutomationIntegrationTests.cs | 50 +++++++++++++++---- src/Core/Bases/A11yElement.cs | 6 +++ src/Desktop/UIAutomation/A11yAutomation.cs | 33 +++++++++++- src/RulesTest/MockA11yElement.cs | 2 +- 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 3123ad3fe..2accdca56 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -3,6 +3,7 @@ using Axe.Windows.Automation; using Axe.Windows.Automation.Data; +using Axe.Windows.Desktop.UIAutomation; using Axe.Windows.UnitTestSharedLibrary; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -92,6 +93,25 @@ public void Scan_Integration_WildlifeManager(bool sync) }); } + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void Scan_Integration_WildlifeManager_Scoped(bool sync) + { + RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => + { + static ScanOptions makeScopedScanOptions(int pid) + { + using (DesktopElement focusedElement = A11yAutomation.GetFocusedElement()) + { + var leafElement = A11yAutomation.GetDepthFirstLastLeafControlElement(focusedElement); + return new ScanOptions() { WindowHandle = leafElement.NativeWindowHandle }; + } + } + ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: 1, processId: null, makeScopedScanOptions); + }); + } + // [DataTestMethod] // [DataRow(true)] // [DataRow(false)] @@ -121,7 +141,7 @@ public void Scan_Integration_WindowsFormsMultiWindowSample(bool sync) { RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => { - ScanIntegrationCore(sync, _windowsFormsMultiWindowSamplerAppPath, WindowsFormsMultiWindowSamplerAppAllErrorCount, 2); + ScanIntegrationCore(sync, _windowsFormsMultiWindowSamplerAppPath, WindowsFormsMultiWindowSamplerAppAllErrorCount, expectedWindowCount: 2); }); } @@ -265,7 +285,7 @@ private void RunWithTimedExecutionWrapper(TimeSpan allowedTime, Action testActio } } - private WindowScanOutput ScanIntegrationCore(bool sync, string testAppPath, int expectedErrorCount, int expectedWindowCount = 1, int? processId = null) + private WindowScanOutput ScanIntegrationCore(bool sync, string testAppPath, int expectedErrorCount, int expectedWindowCount = 1, int? processId = null, Func makeScanOptions = null) { if (processId == null) { @@ -279,14 +299,15 @@ private WindowScanOutput ScanIntegrationCore(bool sync, string testAppPath, int var scanner = ScannerFactory.CreateScanner(config); + ScanOptions scanOptions = makeScanOptions is null ? null : makeScanOptions(processId.Value); IReadOnlyCollection output; if (sync) { - output = ScanSyncWithProvisionForBuildAgents(scanner); + output = ScanSyncWithProvisionForBuildAgents(scanner, scanOptions); } else { - output = ScanAsyncWithProvisionForBuildAgents(scanner); + output = ScanAsyncWithProvisionForBuildAgents(scanner, scanOptions); } return ValidateOutput(output, expectedErrorCount, expectedWindowCount); @@ -324,8 +345,17 @@ private IEnumerable> GetAsyncScanTasks(string testAppPath, IEnu private WindowScanOutput ValidateOutput(IReadOnlyCollection output, int expectedErrorCount, int expectedWindowCount = 1) { Assert.AreEqual(expectedWindowCount, output.Count); - Assert.AreEqual(expectedErrorCount, output.Sum(x => x.ErrorCount)); - Assert.AreEqual(expectedErrorCount, output.Sum(x => x.Errors.Count())); + + int aggregateErrorCount = output.Sum(x => x.ErrorCount); + int totalErrors = output.Sum(x => x.Errors.Count()); + Assert.AreEqual(expectedErrorCount, aggregateErrorCount, message: PrintOutput()); + Assert.AreEqual(expectedErrorCount, totalErrors); + + string PrintOutput() => StringJoin(output.Select(PrintErrors),Environment.NewLine); + static string PrintErrors(WindowScanOutput output, int index) => $"Output #{index}:\r\n\t{StringJoin(output.Errors.Select(PrintError), "\r\n\t")}"; + static string PrintError(ScanResult error, int index) => $"Error #{index}: {error.Rule}\r\n\t\t{PrintElementProperties(error.Element)}"; + static string PrintElementProperties(ElementInfo e) => StringJoin(e.Properties.Select(p => $"{p.Key}='{p.Value}'"),"\r\n\t\t"); + static string StringJoin(IEnumerable lines, string separator) => string.Join(separator, lines); if (expectedErrorCount > 0) { @@ -354,11 +384,11 @@ private static void ValidateTaskCancelled(Task task) Assert.IsTrue(task.IsCanceled); } - private IReadOnlyCollection ScanSyncWithProvisionForBuildAgents(IScanner scanner) + private IReadOnlyCollection ScanSyncWithProvisionForBuildAgents(IScanner scanner, ScanOptions scanOptions = null) { try { - return scanner.Scan(null).WindowScanOutputs; + return scanner.Scan(scanOptions).WindowScanOutputs; } catch (Exception) { @@ -370,11 +400,11 @@ private IReadOnlyCollection ScanSyncWithProvisionForBuildAgent } } - private IReadOnlyCollection ScanAsyncWithProvisionForBuildAgents(IScanner scanner) + private IReadOnlyCollection ScanAsyncWithProvisionForBuildAgents(IScanner scanner, ScanOptions scanOptions = null) { try { - return scanner.ScanAsync(null, CancellationToken.None).Result.WindowScanOutputs; + return scanner.ScanAsync(scanOptions, CancellationToken.None).Result.WindowScanOutputs; } catch (Exception) { diff --git a/src/Core/Bases/A11yElement.cs b/src/Core/Bases/A11yElement.cs index 77197bcef..e1b2ec8d9 100644 --- a/src/Core/Bases/A11yElement.cs +++ b/src/Core/Bases/A11yElement.cs @@ -74,6 +74,12 @@ public string RuntimeId } } + /// + /// the HWND for the element + /// + [JsonIgnore] + public IntPtr NativeWindowHandle => new IntPtr(value: GetPropertySafely(PropertyType.UIA_NativeWindowHandlePropertyId)?.Value); + /// /// a text description of a keystroke which activates the element; valid within a dialog, pane, or app /// diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index 731354ab5..255fa4478 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -14,6 +14,8 @@ using System.Runtime.InteropServices; using UIAutomationClient; +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AutomationTests")] + namespace Axe.Windows.Desktop.UIAutomation { /// @@ -130,7 +132,7 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr // if not, it will throw an ArgumentException using (var proc = Process.GetProcessById(pid)) { - // This lock should not be needed, but E2E tests hvae revealed that a problem + // This lock should not be needed, but E2E tests have revealed that a problem // exists inside UIAutomation.GetRootElement, and this works around the problem. lock (LockObject) { @@ -153,6 +155,35 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr return elements; } + internal static DesktopElement GetFocusedElement() + { + IUIAutomation uiAutomation = GetDefaultInstance().UIAutomation; + IUIAutomationElement focusedElement = uiAutomation.GetFocusedElement(); + return new DesktopElement(focusedElement, keepElement: true, setMembers: true); + } + + internal static DesktopElement GetDepthFirstLastLeafControlElement(DesktopElement rootElement) + { + var walker = GetDefaultInstance().GetTreeWalker(TreeViewMode.Control); + try + { + IUIAutomationElement leafElement = (IUIAutomationElement)rootElement.PlatformObject; + for (IUIAutomationElement currentElement = walker.GetLastChildElement(leafElement); currentElement != null; currentElement = walker.GetLastChildElement(leafElement)) + { + Marshal.ReleaseComObject(leafElement); + leafElement = currentElement; + } + + return leafElement is null + ? rootElement + : new DesktopElement(leafElement, keepElement: true, setMembers: true); + } + finally + { + Marshal.ReleaseComObject(walker); + } + } + /// /// Get DesktopElement from UIAElement interface. /// diff --git a/src/RulesTest/MockA11yElement.cs b/src/RulesTest/MockA11yElement.cs index e11c292e8..85ffa5694 100644 --- a/src/RulesTest/MockA11yElement.cs +++ b/src/RulesTest/MockA11yElement.cs @@ -299,7 +299,7 @@ private void SetProperty(int id, dynamic value) } } - public int NativeWindowHandle + public new int NativeWindowHandle { get { From e9b8554097ad736834fdbe465add549832f67aef Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 12:04:29 -0700 Subject: [PATCH 03/23] Rename ScanOptions.WindowHandle -> ScanRootWindowHandle --- src/Automation/Data/ScanOptions.cs | 2 +- src/Automation/Scanner.cs | 2 +- src/AutomationTests/AutomationIntegrationTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Automation/Data/ScanOptions.cs b/src/Automation/Data/ScanOptions.cs index 87bf8ba98..96391090f 100644 --- a/src/Automation/Data/ScanOptions.cs +++ b/src/Automation/Data/ScanOptions.cs @@ -17,7 +17,7 @@ public class ScanOptions /// /// The window handle for the root of the UIA subtree to scan. /// - public System.IntPtr WindowHandle { get; set; } = System.IntPtr.Zero; + public System.IntPtr ScanRootWindowHandle { get; set; } = System.IntPtr.Zero; /// /// Constructor diff --git a/src/Automation/Scanner.cs b/src/Automation/Scanner.cs index 3e188b351..cd0e9af2a 100644 --- a/src/Automation/Scanner.cs +++ b/src/Automation/Scanner.cs @@ -42,7 +42,7 @@ private void HandleScanOptions(ScanOptions scanOptions) { scanOptions = scanOptions ?? DefaultScanOptions; _scanTools.OutputFileHelper.SetScanId(scanOptions.ScanId); - _scanTools.TargetElementLocator.SetRootWindowHandle(scanOptions.WindowHandle); + _scanTools.TargetElementLocator.SetRootWindowHandle(scanOptions.ScanRootWindowHandle); } } // class } // namespace diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 2accdca56..b864f0cb3 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -105,7 +105,7 @@ static ScanOptions makeScopedScanOptions(int pid) using (DesktopElement focusedElement = A11yAutomation.GetFocusedElement()) { var leafElement = A11yAutomation.GetDepthFirstLastLeafControlElement(focusedElement); - return new ScanOptions() { WindowHandle = leafElement.NativeWindowHandle }; + return new() { ScanRootWindowHandle = leafElement.NativeWindowHandle }; } } ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: 1, processId: null, makeScopedScanOptions); From bfc03652616d4b6e859fc60361dc5e67b860f9a1 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 12:26:40 -0700 Subject: [PATCH 04/23] Pass ScanOptions.ScanRootWindowHandle -> ScanTools.ScanRootWindowHandle -> TargetElementLocator.LocateRootElements instead of setting a field on the ScanTools' TargetElementLocator. --- src/Automation/Data/ScanOptions.cs | 6 ++++-- src/Automation/Interfaces/IScanTools.cs | 3 +++ src/Automation/Interfaces/ITargetElementLocator.cs | 3 +-- src/Automation/Scanner.cs | 2 +- src/Automation/SnapshotCommand.cs | 2 +- src/Automation/TargetElementLocator.cs | 8 ++------ src/AutomationTests/AutomationIntegrationTests.cs | 2 +- src/AutomationTests/SnapshotCommandTests.cs | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Automation/Data/ScanOptions.cs b/src/Automation/Data/ScanOptions.cs index 96391090f..b723a6e4d 100644 --- a/src/Automation/Data/ScanOptions.cs +++ b/src/Automation/Data/ScanOptions.cs @@ -17,15 +17,17 @@ public class ScanOptions /// /// The window handle for the root of the UIA subtree to scan. /// - public System.IntPtr ScanRootWindowHandle { get; set; } = System.IntPtr.Zero; + public System.IntPtr ScanRootWindowHandle { get; } /// /// Constructor /// /// The ID of this scan. Must be null or meet the requirements for a file name. - public ScanOptions(string scanId = null) + /// The window handle for the root of the UIA subtree to scan. + public ScanOptions(string scanId = null, System.IntPtr? scanRootWindowHandle = null) { ScanId = scanId; + ScanRootWindowHandle = scanRootWindowHandle.GetValueOrDefault(System.IntPtr.Zero); } } } diff --git a/src/Automation/Interfaces/IScanTools.cs b/src/Automation/Interfaces/IScanTools.cs index f1df9030c..5d7a5560d 100644 --- a/src/Automation/Interfaces/IScanTools.cs +++ b/src/Automation/Interfaces/IScanTools.cs @@ -3,6 +3,8 @@ namespace Axe.Windows.Automation { + using System; + /// /// Encapsulates the set of tools used to scan, assemble results, and write output files /// @@ -13,5 +15,6 @@ internal interface IScanTools ITargetElementLocator TargetElementLocator { get; } IAxeWindowsActions Actions { get; } IDPIAwareness DpiAwareness { get; } + IntPtr ScanRootWindowHandle { get; set; } } // interface } // namespace diff --git a/src/Automation/Interfaces/ITargetElementLocator.cs b/src/Automation/Interfaces/ITargetElementLocator.cs index 5dcd8a2ff..b12064c3f 100644 --- a/src/Automation/Interfaces/ITargetElementLocator.cs +++ b/src/Automation/Interfaces/ITargetElementLocator.cs @@ -9,7 +9,6 @@ namespace Axe.Windows.Automation { internal interface ITargetElementLocator { - IEnumerable LocateRootElements(int processId, IActionContext actionContext); - void SetRootWindowHandle(System.IntPtr windowHandle); + IEnumerable LocateRootElements(int processId, IActionContext actionContext, System.IntPtr rootWindowHandle); } // interface } // namespace diff --git a/src/Automation/Scanner.cs b/src/Automation/Scanner.cs index cd0e9af2a..a5d331d6c 100644 --- a/src/Automation/Scanner.cs +++ b/src/Automation/Scanner.cs @@ -42,7 +42,7 @@ private void HandleScanOptions(ScanOptions scanOptions) { scanOptions = scanOptions ?? DefaultScanOptions; _scanTools.OutputFileHelper.SetScanId(scanOptions.ScanId); - _scanTools.TargetElementLocator.SetRootWindowHandle(scanOptions.ScanRootWindowHandle); + _scanTools.ScanRootWindowHandle = scanOptions.ScanRootWindowHandle; } } // class } // namespace diff --git a/src/Automation/SnapshotCommand.cs b/src/Automation/SnapshotCommand.cs index ecc47269a..3bfc18adf 100644 --- a/src/Automation/SnapshotCommand.cs +++ b/src/Automation/SnapshotCommand.cs @@ -66,7 +66,7 @@ private static ScanOutput GetScanOutput(Config config, IScanTools scanTools, Can using (var actionContext = ScopedActionContext.CreateInstance(cancellationToken)) { - var rootElements = scanTools.TargetElementLocator.LocateRootElements(config.ProcessId, actionContext); + var rootElements = scanTools.TargetElementLocator.LocateRootElements(config.ProcessId, actionContext, scanTools.ScanRootWindowHandle); if (rootElements is null || !rootElements.Any()) { diff --git a/src/Automation/TargetElementLocator.cs b/src/Automation/TargetElementLocator.cs index 2e9cea266..283a093b3 100644 --- a/src/Automation/TargetElementLocator.cs +++ b/src/Automation/TargetElementLocator.cs @@ -14,13 +14,11 @@ namespace Axe.Windows.Automation { class TargetElementLocator : ITargetElementLocator { - private IntPtr _rootWindowHandle; - - public IEnumerable LocateRootElements(int processId, IActionContext actionContext) + public IEnumerable LocateRootElements(int processId, IActionContext actionContext, IntPtr rootWindowHandle) { try { - var desktopElements = A11yAutomation.ElementsFromProcessId(processId, _rootWindowHandle, actionContext.DesktopDataContext); + var desktopElements = A11yAutomation.ElementsFromProcessId(processId, rootWindowHandle, actionContext.DesktopDataContext); return GetA11yElementsFromDesktopElements(desktopElements); } catch (Exception ex) @@ -40,7 +38,5 @@ private static IEnumerable GetA11yElementsFromDesktopElements(IEnum #pragma warning restore CA2000 } } - - public void SetRootWindowHandle(IntPtr rootWindowHandle) => _rootWindowHandle = rootWindowHandle; } } diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index b864f0cb3..25941a24f 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -105,7 +105,7 @@ static ScanOptions makeScopedScanOptions(int pid) using (DesktopElement focusedElement = A11yAutomation.GetFocusedElement()) { var leafElement = A11yAutomation.GetDepthFirstLastLeafControlElement(focusedElement); - return new() { ScanRootWindowHandle = leafElement.NativeWindowHandle }; + return new ScanOptions(scanRootWindowHandle: leafElement.NativeWindowHandle); } } ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: 1, processId: null, makeScopedScanOptions); diff --git a/src/AutomationTests/SnapshotCommandTests.cs b/src/AutomationTests/SnapshotCommandTests.cs index 231bf1a8e..a2dc68a8f 100644 --- a/src/AutomationTests/SnapshotCommandTests.cs +++ b/src/AutomationTests/SnapshotCommandTests.cs @@ -120,7 +120,7 @@ private void SetupActionsMock(string expectedPath = "") private void SetupTargetElementLocatorMock(int processId = -1, bool overrideElements = false, IEnumerable elements = null) { var overriddenElements = overrideElements ? elements : CreateMockElementArray(); - _targetElementLocatorMock.Setup(x => x.LocateRootElements(processId, It.IsAny())).Returns(overriddenElements); + _targetElementLocatorMock.Setup(x => x.LocateRootElements(processId, It.IsAny(), IntPtr.Zero)).Returns(overriddenElements); } private void SetupOutputFileHelperMock(string filePath = "Test.File") From c00d9784e8aec4f79b1e23f5b661d607b585c988 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 12:41:25 -0700 Subject: [PATCH 05/23] Move A11yAutomation test-specific implementations to an A11yAutomationUtilities class in AutomationTests. --- src/Automation/ScanTools.cs | 1 + .../A11yAutomationUtilities.cs | 45 +++++++++++++++++++ .../AutomationIntegrationTests.cs | 4 +- src/AutomationTests/AutomationTests.csproj | 14 ++++++ src/Desktop/UIAutomation/A11yAutomation.cs | 29 ------------ 5 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 src/AutomationTests/A11yAutomationUtilities.cs diff --git a/src/Automation/ScanTools.cs b/src/Automation/ScanTools.cs index b8d2daf90..b8db5dcb0 100644 --- a/src/Automation/ScanTools.cs +++ b/src/Automation/ScanTools.cs @@ -12,6 +12,7 @@ class ScanTools : IScanTools public ITargetElementLocator TargetElementLocator { get; } public IAxeWindowsActions Actions { get; } public IDPIAwareness DpiAwareness { get; } + public IntPtr ScanRootWindowHandle { get; set; } = IntPtr.Zero; public ScanTools(IOutputFileHelper outputFileHelper, IScanResultsAssembler resultsAssembler, ITargetElementLocator targetElementLocator, IAxeWindowsActions actions, IDPIAwareness dpiAwareness) { diff --git a/src/AutomationTests/A11yAutomationUtilities.cs b/src/AutomationTests/A11yAutomationUtilities.cs new file mode 100644 index 000000000..ca14a9949 --- /dev/null +++ b/src/AutomationTests/A11yAutomationUtilities.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Axe.Windows.Core.Enums; +using Axe.Windows.Desktop.UIAutomation; +using System.Runtime.InteropServices; +using UIAutomationClient; + +namespace Axe.Windows.AutomationTests +{ + /// + /// Wrapper for CUIAutomation COM object + /// + public class A11yAutomationUtilities + { + internal static DesktopElement GetFocusedElement() + { + IUIAutomation uiAutomation = A11yAutomation.GetDefaultInstance().UIAutomation; + IUIAutomationElement focusedElement = uiAutomation.GetFocusedElement(); + return new DesktopElement(focusedElement, keepElement: true, setMembers: true); + } + + internal static DesktopElement GetDepthFirstLastLeafControlElement(DesktopElement rootElement) + { + var walker = A11yAutomation.GetDefaultInstance().GetTreeWalker(TreeViewMode.Control); + try + { + IUIAutomationElement leafElement = (IUIAutomationElement)rootElement.PlatformObject; + for (IUIAutomationElement currentElement = walker.GetLastChildElement(leafElement); currentElement != null; currentElement = walker.GetLastChildElement(leafElement)) + { + Marshal.ReleaseComObject(leafElement); + leafElement = currentElement; + } + + return leafElement is null + ? rootElement + : new DesktopElement(leafElement, keepElement: true, setMembers: true); + } + finally + { + Marshal.ReleaseComObject(walker); + } + } + } +} diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 25941a24f..4b762dfd9 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -102,9 +102,9 @@ public void Scan_Integration_WildlifeManager_Scoped(bool sync) { static ScanOptions makeScopedScanOptions(int pid) { - using (DesktopElement focusedElement = A11yAutomation.GetFocusedElement()) + using (DesktopElement focusedElement = A11yAutomationUtilities.GetFocusedElement()) { - var leafElement = A11yAutomation.GetDepthFirstLastLeafControlElement(focusedElement); + var leafElement = A11yAutomationUtilities.GetDepthFirstLastLeafControlElement(focusedElement); return new ScanOptions(scanRootWindowHandle: leafElement.NativeWindowHandle); } } diff --git a/src/AutomationTests/AutomationTests.csproj b/src/AutomationTests/AutomationTests.csproj index ed0978658..40806f54b 100644 --- a/src/AutomationTests/AutomationTests.csproj +++ b/src/AutomationTests/AutomationTests.csproj @@ -26,6 +26,20 @@ + + + ..\UIAAssemblies\Win10.17713\Interop.UIAutomationClient.dll + true + + + + + + ..\InteropDummy\bin\$(Configuration)\net6.0\Interop.UIAutomationCore.dll + true + + + diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index 255fa4478..b985219d9 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -155,35 +155,6 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr return elements; } - internal static DesktopElement GetFocusedElement() - { - IUIAutomation uiAutomation = GetDefaultInstance().UIAutomation; - IUIAutomationElement focusedElement = uiAutomation.GetFocusedElement(); - return new DesktopElement(focusedElement, keepElement: true, setMembers: true); - } - - internal static DesktopElement GetDepthFirstLastLeafControlElement(DesktopElement rootElement) - { - var walker = GetDefaultInstance().GetTreeWalker(TreeViewMode.Control); - try - { - IUIAutomationElement leafElement = (IUIAutomationElement)rootElement.PlatformObject; - for (IUIAutomationElement currentElement = walker.GetLastChildElement(leafElement); currentElement != null; currentElement = walker.GetLastChildElement(leafElement)) - { - Marshal.ReleaseComObject(leafElement); - leafElement = currentElement; - } - - return leafElement is null - ? rootElement - : new DesktopElement(leafElement, keepElement: true, setMembers: true); - } - finally - { - Marshal.ReleaseComObject(walker); - } - } - /// /// Get DesktopElement from UIAElement interface. /// From 70a8b7ee0c7b0ec97f6e654a0dfaf87fa9f981d2 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 12:45:02 -0700 Subject: [PATCH 06/23] Fix A11yAutomation interals visibility in signed environments. --- src/Desktop/UIAutomation/A11yAutomation.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index b985219d9..e20ad5fcd 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -11,10 +11,16 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UIAutomationClient; - -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AutomationTests")] +#if ENABLE_SIGNING +[assembly: InternalsVisibleTo("AutomationTests,PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2,PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +#else +[assembly: InternalsVisibleTo("AutomationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif namespace Axe.Windows.Desktop.UIAutomation { From 15d8ce31500a19f73ad654940be856cfbc055423 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 13:40:22 -0700 Subject: [PATCH 07/23] Fallback to full process scan when scoped window handle does not find an IUIAutomationElement. --- .../AutomationIntegrationTests.cs | 14 +++- src/Desktop/UIAutomation/A11yAutomation.cs | 68 ++++++++++++++----- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 4b762dfd9..8c2949222 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -100,7 +100,7 @@ public void Scan_Integration_WildlifeManager_Scoped(bool sync) { RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => { - static ScanOptions makeScopedScanOptions(int pid) + static ScanOptions makeScopedScanOptions(int _) { using (DesktopElement focusedElement = A11yAutomationUtilities.GetFocusedElement()) { @@ -112,6 +112,18 @@ static ScanOptions makeScopedScanOptions(int pid) }); } + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public void Scan_Integration_WildlifeManager_InvalidRoot(bool sync) + { + RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => + { + static ScanOptions makeScanOptionsWithInvalidRoot(int _) => new(scanRootWindowHandle: new IntPtr(42)); + ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: WildlifeManagerKnownErrorCount, processId: null, makeScanOptionsWithInvalidRoot); + }); + } + // [DataTestMethod] // [DataRow(true)] // [DataRow(false)] diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index e20ad5fcd..2ef120fe9 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -61,20 +61,25 @@ private static bool FindProcessMatchingChildren(IUIAutomationElement parent, IUI { for (var child = walker.GetFirstChildElement(parent); child != null; child = walker.GetNextSiblingElement(child)) { - if (child.CurrentProcessId == pid) - { - matchingElements.Add(child); - } - else - { - nonMatchingElements.Add(child); - } + TestProcessMatchingELement(pid, matchingElements, nonMatchingElements, child); } return matchingElements.Any(); } - private IList FindProcessMatchingChildrenOrGrandchildren(IUIAutomationElement root, int pid) + private static void TestProcessMatchingELement(int pid, IList matchingElements, IList nonMatchingElements, IUIAutomationElement element) + { + if (element.CurrentProcessId == pid) + { + matchingElements.Add(element); + } + else + { + nonMatchingElements.Add(element); + } + } + + private IList FindProcessMatchingChildrenOrGrandchildren(IUIAutomationElement root, int pid, bool includeSelf = false) { IUIAutomationTreeWalker walker = GetTreeWalker(TreeViewMode.Control); List matchingElements = new List(); @@ -82,6 +87,11 @@ private IList FindProcessMatchingChildrenOrGrandchildren(I List nonMatchingElementsSecondLevel = new List(); try { + if (includeSelf) + { + TestProcessMatchingELement(pid, matchingElements, nonMatchingElements, root); + } + if (FindProcessMatchingChildren(root, walker, pid, matchingElements, nonMatchingElements)) { return matchingElements; @@ -118,13 +128,9 @@ private static void ReleaseElements(IUIAutomationTreeWalker walker, params IList /// Get DesktopElements based on Process Id. /// /// The whoese elements should be retrieved. - /// - /// The window handle for the that should be used - /// as the root of the sub-tree to be scanned. - /// /// The data context /// return null if we fail to get elements by process Id - public static IEnumerable ElementsFromProcessId(int pid, IntPtr rootWindowHandle, DesktopDataContext dataContext) + public static IEnumerable ElementsFromProcessId(int pid, DesktopDataContext dataContext) { if (dataContext == null) throw new ArgumentNullException(nameof(dataContext)); @@ -142,9 +148,7 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr // exists inside UIAutomation.GetRootElement, and this works around the problem. lock (LockObject) { - root = rootWindowHandle == IntPtr.Zero - ? dataContext.A11yAutomation.UIAutomation.GetRootElement() - : dataContext.A11yAutomation.UIAutomation.ElementFromHandle(rootWindowHandle); + root = dataContext.A11yAutomation.UIAutomation.GetRootElement(); } matchingElements = dataContext.A11yAutomation.FindProcessMatchingChildrenOrGrandchildren(root, pid); elements = ElementsFromUIAElements(matchingElements, dataContext); @@ -161,6 +165,36 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr return elements; } + /// + /// Get DesktopElements whose matches the , + /// in the sub-tree rooted at the element with the specified . + /// + /// The whoese s should be retrieved. + /// + /// The window handle for the that should be used + /// as the root of the sub-tree to be scanned. If the handle is invalid, scan the entire process tree. + /// + /// The data context + /// + /// return all elements by process Id if the root window handle is invalid + /// return null if we fail to get elements by process Id + /// + public static IEnumerable ElementsFromProcessId(int pid, IntPtr rootWindowHandle, DesktopDataContext dataContext) + { + if (dataContext == null) throw new ArgumentNullException(nameof(dataContext)); + + IUIAutomationElement subtreeRootElement = dataContext.A11yAutomation.UIAutomation.ElementFromHandle(rootWindowHandle); + if (subtreeRootElement is null) + { + // If the root window handle is invalid, fallback to full-process scan. + return ElementsFromProcessId(pid, dataContext); + } + + // Get all elements in the subtree rooted at the specified element - the root element should not be Released. + IList matchingElements = dataContext.A11yAutomation.FindProcessMatchingChildrenOrGrandchildren(subtreeRootElement, pid, includeSelf: true); + return ElementsFromUIAElements(matchingElements, dataContext); + } + /// /// Get DesktopElement from UIAElement interface. /// From 98772bd55e9ffb8dac9590028567ceadbe256676 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Thu, 2 May 2024 14:27:19 -0700 Subject: [PATCH 08/23] Add CLI support for ScanRootWindowHandle --- src/CLI/IOptions.cs | 1 + src/CLI/Options.cs | 3 +++ src/CLI/OptionsEvaluator.cs | 1 + src/CLI/OutputGenerator.cs | 7 +++++++ src/CLI/Resources/DisplayStrings.Designer.cs | 9 +++++++++ src/CLI/Resources/DisplayStrings.resx | 4 ++++ src/CLI/Resources/OptionsHelpText.Designer.cs | 9 +++++++++ src/CLI/Resources/OptionsHelpText.resx | 3 +++ src/CLI/ScanRunner.cs | 2 +- 9 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/CLI/IOptions.cs b/src/CLI/IOptions.cs index d469e8255..9796cfab0 100644 --- a/src/CLI/IOptions.cs +++ b/src/CLI/IOptions.cs @@ -7,6 +7,7 @@ internal interface IOptions { string OutputDirectory { get; } string ScanId { get; } + System.IntPtr ScanRootWindowHandle { get; } int ProcessId { get; } string ProcessName { get; } VerbosityLevel VerbosityLevel { get; } diff --git a/src/CLI/Options.cs b/src/CLI/Options.cs index 3d2ebeff6..106bf24ed 100644 --- a/src/CLI/Options.cs +++ b/src/CLI/Options.cs @@ -19,6 +19,9 @@ public class Options : IOptions [Option(Required = false, HelpText = "ScanId", ResourceType = typeof(Resources.OptionsHelpText))] public string ScanId { get; set; } + + [Option(Required = false, HelpText = "ScanRootWindowHandle", ResourceType = typeof(Resources.OptionsHelpText))] + public System.IntPtr ScanRootWindowHandle { get; set; } = System.IntPtr.Zero; [Option(Required = false, HelpText = "Verbosity", ResourceType = typeof(Resources.OptionsHelpText))] public string Verbosity { get; set; } diff --git a/src/CLI/OptionsEvaluator.cs b/src/CLI/OptionsEvaluator.cs index 4fe0c006b..7847165d9 100644 --- a/src/CLI/OptionsEvaluator.cs +++ b/src/CLI/OptionsEvaluator.cs @@ -61,6 +61,7 @@ public static IOptions ProcessInputs(Options rawInputs, IProcessHelper processHe ProcessId = processId, ProcessName = processName, ScanId = rawInputs.ScanId, + ScanRootWindowHandle = rawInputs.ScanRootWindowHandle, VerbosityLevel = verbosityLevel, DelayInSeconds = delayInSeconds, CustomUia = rawInputs.CustomUia, diff --git a/src/CLI/OutputGenerator.cs b/src/CLI/OutputGenerator.cs index f579112c3..6f040c4a1 100644 --- a/src/CLI/OutputGenerator.cs +++ b/src/CLI/OutputGenerator.cs @@ -84,6 +84,13 @@ private void WriteBanner(IOptions options, VerbosityLevel minimumVerbosity) { _writer.Write(DisplayStrings.ScanTargetProcessIdFormat, options.ProcessId); } + + bool haveScanRootWindowHandle = options.ScanRootWindowHandle != IntPtr.Zero; + if (haveScanRootWindowHandle) + { + _writer.Write(DisplayStrings.ScanTargetSeparator); + _writer.Write(DisplayStrings.ScanTargetRootWindowHandleFormat, options.ScanRootWindowHandle); + } _writer.WriteLine(); } if (!string.IsNullOrEmpty(options.ScanId)) diff --git a/src/CLI/Resources/DisplayStrings.Designer.cs b/src/CLI/Resources/DisplayStrings.Designer.cs index bc488a42b..bea4d348e 100644 --- a/src/CLI/Resources/DisplayStrings.Designer.cs +++ b/src/CLI/Resources/DisplayStrings.Designer.cs @@ -312,6 +312,15 @@ internal static string ScanTargetProcessNameFormat { } } + /// + /// Looks up a localized string similar to HWND = {0}. + /// + internal static string ScanTargetRootWindowHandleFormat { + get { + return ResourceManager.GetString("ScanTargetRootWindowHandleFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to ,. /// diff --git a/src/CLI/Resources/DisplayStrings.resx b/src/CLI/Resources/DisplayStrings.resx index 68a87b71e..9fec040f9 100644 --- a/src/CLI/Resources/DisplayStrings.resx +++ b/src/CLI/Resources/DisplayStrings.resx @@ -222,6 +222,10 @@ Process Name = {0} {0} is the process name. Leading space is intentional + + HWND = {0} + {0} is the scan root window handle + , diff --git a/src/CLI/Resources/OptionsHelpText.Designer.cs b/src/CLI/Resources/OptionsHelpText.Designer.cs index 43a792a6e..f537f5848 100644 --- a/src/CLI/Resources/OptionsHelpText.Designer.cs +++ b/src/CLI/Resources/OptionsHelpText.Designer.cs @@ -123,6 +123,15 @@ public static string ScanId { } } + /// + /// Looks up a localized string similar to The HWND for a UI Automation element whose sub-tree should be scanned. + /// + public static string ScanRootWindowHandle { + get { + return ResourceManager.GetString("ScanRootWindowHandle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display Third Party Notices (opens file in browser without executing scan). If specified, all other options will be ignored.. /// diff --git a/src/CLI/Resources/OptionsHelpText.resx b/src/CLI/Resources/OptionsHelpText.resx index 270f6fd02..e712187a7 100644 --- a/src/CLI/Resources/OptionsHelpText.resx +++ b/src/CLI/Resources/OptionsHelpText.resx @@ -138,6 +138,9 @@ Scan ID + + The HWND for a UI Automation element whose sub-tree should be scanned + Display Third Party Notices (opens file in browser without executing scan). If specified, all other options will be ignored. diff --git a/src/CLI/ScanRunner.cs b/src/CLI/ScanRunner.cs index bac9efd7d..64ace0866 100644 --- a/src/CLI/ScanRunner.cs +++ b/src/CLI/ScanRunner.cs @@ -12,7 +12,7 @@ internal static class ScanRunner public static IReadOnlyCollection RunScan(IOptions options) { IScanner scanner = BuildScanner(options); - return scanner.Scan(new ScanOptions(options.ScanId)).WindowScanOutputs; + return scanner.Scan(new ScanOptions(options.ScanId, options.ScanRootWindowHandle)).WindowScanOutputs; } private static IScanner BuildScanner(IOptions options) From 92632248819a4890aa1b7090ab93dcab68d787fe Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 08:27:34 -0700 Subject: [PATCH 09/23] Add CLI Options test for ScanRootWindowHandle. --- src/CLITests/OptionsEvaluatorTests.cs | 21 ++++++++++++++++++++- src/CLITests/OptionsTests.cs | 2 ++ src/CLITests/OutputGeneratorTests.cs | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/CLITests/OptionsEvaluatorTests.cs b/src/CLITests/OptionsEvaluatorTests.cs index 3cb51ecab..efda6a1e2 100644 --- a/src/CLITests/OptionsEvaluatorTests.cs +++ b/src/CLITests/OptionsEvaluatorTests.cs @@ -31,12 +31,14 @@ private void VerifyAllMocks() private static void ValidateOptions(IOptions options, string processName = TestProcessName, int processId = TestProcessId, string outputDirectory = null, string scanId = null, + IntPtr scanRootWindowHandle = default, VerbosityLevel verbosityLevel = VerbosityLevel.Default, int delayInSeconds = 0, bool alwaysWriteTestFile = false, bool testAllChromiumContent = false) { Assert.AreEqual(processName, options.ProcessName); Assert.AreEqual(processId, options.ProcessId); Assert.AreEqual(scanId, options.ScanId); + Assert.AreEqual(scanRootWindowHandle, options.ScanRootWindowHandle); Assert.AreEqual(outputDirectory, options.OutputDirectory); Assert.AreEqual(verbosityLevel, options.VerbosityLevel); Assert.AreEqual(delayInSeconds, options.DelayInSeconds); @@ -156,7 +158,7 @@ public void ProcessInputs_SpecifiesOutputDirectory_RetainsOutputDirectory() [TestMethod] [Timeout(1000)] - public void ProcessInputs_SpecifiesScanId_RetainsOutputDirectory() + public void ProcessInputs_SpecifiesScanId_RetainsScanId() { const string testScanId = "SuperScan"; _processHelperMock.Setup(x => x.ProcessIdFromName(TestProcessName)).Returns(TestProcessId); @@ -170,6 +172,23 @@ public void ProcessInputs_SpecifiesScanId_RetainsOutputDirectory() VerifyAllMocks(); } + [TestMethod] + [Timeout(1000)] + public void ProcessInputs_SpecifiesScanRootWindowHandle_RetainsScanRootWindowHandle() + { + const int testScanRootWindowHandleValue = 42; + IntPtr testScanRootWindowHandle = new(testScanRootWindowHandleValue); + _processHelperMock.Setup(x => x.ProcessIdFromName(TestProcessName)).Returns(TestProcessId); + Options input = new Options + { + ProcessName = TestProcessName, + ScanRootWindowHandle = testScanRootWindowHandle, + }; + ValidateOptions(OptionsEvaluator.ProcessInputs(input, _processHelperMock.Object), + processId: TestProcessId, scanRootWindowHandle: testScanRootWindowHandle); + VerifyAllMocks(); + } + [TestMethod] [Timeout(1000)] public void ProcessInputs_SpecifiesInvalidVerbosity_ThrowsParameterException() diff --git a/src/CLITests/OptionsTests.cs b/src/CLITests/OptionsTests.cs index 7e8622f3a..a7f49238f 100644 --- a/src/CLITests/OptionsTests.cs +++ b/src/CLITests/OptionsTests.cs @@ -35,12 +35,14 @@ private int FailIfCalled(IEnumerable errors) private static int ValidateOptions(Options options, string processName = null, int processId = 0, string outputDirectory = null, string scanId = null, + System.IntPtr scanRootWindowHandle = default, string verbosity = null, bool showThirdPartyNotices = false, int delayInSeconds = 0, string customUia = null, bool alwaysSaveTestFile = false) { Assert.AreEqual(processName, options.ProcessName); Assert.AreEqual(processId, options.ProcessId); Assert.AreEqual(scanId, options.ScanId); + Assert.AreEqual(scanRootWindowHandle, options.ScanRootWindowHandle); Assert.AreEqual(outputDirectory, options.OutputDirectory); Assert.AreEqual(verbosity, options.Verbosity); Assert.AreEqual(VerbosityLevel.Default, options.VerbosityLevel); diff --git a/src/CLITests/OutputGeneratorTests.cs b/src/CLITests/OutputGeneratorTests.cs index ea5a8bb8a..5aeb725cb 100644 --- a/src/CLITests/OutputGeneratorTests.cs +++ b/src/CLITests/OutputGeneratorTests.cs @@ -69,6 +69,10 @@ private void SetOptions(VerbosityLevel verbosityLevel = VerbosityLevel.Default, _optionsMock.Setup(x => x.ProcessName).Returns(processName); _optionsMock.Setup(x => x.ProcessId).Returns(processId); _optionsMock.Setup(x => x.ScanId).Returns(scanId); + if (processId > 0 || !string.IsNullOrEmpty(processName)) + { + _optionsMock.Setup(x => x.ScanRootWindowHandle).Returns(IntPtr.Zero); + } } [TestMethod] From 09563ece75fa5da2669d4c733dc294d464b3e7bd Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 10:56:12 -0700 Subject: [PATCH 10/23] Fallback to pid search from root element when ElementFromHandle fails. --- src/Desktop/UIAutomation/A11yAutomation.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Desktop/UIAutomation/A11yAutomation.cs b/src/Desktop/UIAutomation/A11yAutomation.cs index 2ef120fe9..a8d24c893 100644 --- a/src/Desktop/UIAutomation/A11yAutomation.cs +++ b/src/Desktop/UIAutomation/A11yAutomation.cs @@ -61,13 +61,13 @@ private static bool FindProcessMatchingChildren(IUIAutomationElement parent, IUI { for (var child = walker.GetFirstChildElement(parent); child != null; child = walker.GetNextSiblingElement(child)) { - TestProcessMatchingELement(pid, matchingElements, nonMatchingElements, child); + TestProcessMatchingElement(pid, matchingElements, nonMatchingElements, child); } return matchingElements.Any(); } - private static void TestProcessMatchingELement(int pid, IList matchingElements, IList nonMatchingElements, IUIAutomationElement element) + private static void TestProcessMatchingElement(int pid, IList matchingElements, IList nonMatchingElements, IUIAutomationElement element) { if (element.CurrentProcessId == pid) { @@ -89,7 +89,7 @@ private IList FindProcessMatchingChildrenOrGrandchildren(I { if (includeSelf) { - TestProcessMatchingELement(pid, matchingElements, nonMatchingElements, root); + TestProcessMatchingElement(pid, matchingElements, nonMatchingElements, root); } if (FindProcessMatchingChildren(root, walker, pid, matchingElements, nonMatchingElements)) @@ -183,7 +183,16 @@ public static IEnumerable ElementsFromProcessId(int pid, IntPtr { if (dataContext == null) throw new ArgumentNullException(nameof(dataContext)); - IUIAutomationElement subtreeRootElement = dataContext.A11yAutomation.UIAutomation.ElementFromHandle(rootWindowHandle); + IUIAutomationElement subtreeRootElement; + try + { + subtreeRootElement = dataContext.A11yAutomation.UIAutomation.ElementFromHandle(rootWindowHandle); + } + catch (COMException) + { + subtreeRootElement = null; + } + if (subtreeRootElement is null) { // If the root window handle is invalid, fallback to full-process scan. From 430ac50ac88d5606c226141ee60ce468c045ec1e Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 11:00:38 -0700 Subject: [PATCH 11/23] Implement GetDepthFirstLastLeafHWNDElement to ensure we find a control that has nonzero HWND for scoped UI scan testing. --- .../A11yAutomationUtilities.cs | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/src/AutomationTests/A11yAutomationUtilities.cs b/src/AutomationTests/A11yAutomationUtilities.cs index ca14a9949..bac6a0df6 100644 --- a/src/AutomationTests/A11yAutomationUtilities.cs +++ b/src/AutomationTests/A11yAutomationUtilities.cs @@ -3,6 +3,10 @@ using Axe.Windows.Core.Enums; using Axe.Windows.Desktop.UIAutomation; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using UIAutomationClient; @@ -13,6 +17,13 @@ namespace Axe.Windows.AutomationTests /// public class A11yAutomationUtilities { + internal static DesktopElement GetRootElement() + { + IUIAutomation uiAutomation = A11yAutomation.GetDefaultInstance().UIAutomation; + IUIAutomationElement focusedElement = uiAutomation.GetRootElement(); + return new DesktopElement(focusedElement, keepElement: true, setMembers: true); + } + internal static DesktopElement GetFocusedElement() { IUIAutomation uiAutomation = A11yAutomation.GetDefaultInstance().UIAutomation; @@ -20,17 +31,13 @@ internal static DesktopElement GetFocusedElement() return new DesktopElement(focusedElement, keepElement: true, setMembers: true); } - internal static DesktopElement GetDepthFirstLastLeafControlElement(DesktopElement rootElement) + internal static DesktopElement GetDepthFirstLastLeafHWNDElement(DesktopElement rootElement) { var walker = A11yAutomation.GetDefaultInstance().GetTreeWalker(TreeViewMode.Control); try { - IUIAutomationElement leafElement = (IUIAutomationElement)rootElement.PlatformObject; - for (IUIAutomationElement currentElement = walker.GetLastChildElement(leafElement); currentElement != null; currentElement = walker.GetLastChildElement(leafElement)) - { - Marshal.ReleaseComObject(leafElement); - leafElement = currentElement; - } + IUIAutomationElement rootAutomationElement = rootElement.PlatformObject; + IUIAutomationElement leafElement = FindLastWindowHandleElement(rootAutomationElement); return leafElement is null ? rootElement @@ -40,6 +47,52 @@ internal static DesktopElement GetDepthFirstLastLeafControlElement(DesktopElemen { Marshal.ReleaseComObject(walker); } + + IUIAutomationElement FindLastWindowHandleElement(IUIAutomationElement parent) + { + List matchingElements = new(); + for (var child = walker.GetFirstChildElement(parent); child != null; child = MatchAndMoveNext(child)) + { + } + + foreach (var element in matchingElements) + { + FindLastWindowHandleElement(element); + } + + foreach (var element in matchingElements.Take(matchingElements.Count - 1)) + { + Marshal.ReleaseComObject(element); + } + + return matchingElements.LastOrDefault(); + + IUIAutomationElement MatchAndMoveNext(IUIAutomationElement element) + { + IUIAutomationElement next; + try + { + next = walker.GetNextSiblingElement(element); + } + catch(COMException e) + { + Debug.WriteLine(message: $"Error getting sibling of {element.CurrentName}: {e}"); + next = null; + } + + if (element.CurrentNativeWindowHandle != IntPtr.Zero) + { + Debug.WriteLine(message: $"HWND={element.CurrentNativeWindowHandle}; Name='{element.CurrentName}'; ClassName='{element.CurrentClassName}'"); + matchingElements.Add(element); + } + else + { + Marshal.ReleaseComObject(element); + } + + return next; + } + } } } } From 4f524db860e698a1c54d245cde3306a66b2036fd Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 11:03:12 -0700 Subject: [PATCH 12/23] Print the WindowScanOutput.Errors to help with debugging ValidateOutput --- src/AutomationTests/AutomationIntegrationTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 8c2949222..4d952c612 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -360,7 +360,7 @@ private WindowScanOutput ValidateOutput(IReadOnlyCollection ou int aggregateErrorCount = output.Sum(x => x.ErrorCount); int totalErrors = output.Sum(x => x.Errors.Count()); - Assert.AreEqual(expectedErrorCount, aggregateErrorCount, message: PrintOutput()); + Assert.AreEqual(expectedErrorCount, aggregateErrorCount, message: IsTestRunningInPipeline() ? string.Empty : PrintAll(output)); Assert.AreEqual(expectedErrorCount, totalErrors); string PrintOutput() => StringJoin(output.Select(PrintErrors),Environment.NewLine); @@ -483,5 +483,11 @@ private static bool IsTestRunningInPipeline() // The BUILD_BUILDID environment variable is only set on build agents return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_BUILDID")); } + + static string PrintAll(IReadOnlyCollection output) => StringJoin(output.Select(PrintErrors), Environment.NewLine); + static string PrintErrors(WindowScanOutput output, int index) => $"Output #{index}:\r\n\t{StringJoin(output.Errors.Select(PrintError), "\r\n\t")}"; + static string PrintError(ScanResult error, int index) => $"Error #{index}: {error.Rule}\r\n\t\t{PrintElementProperties(error.Element)}"; + static string PrintElementProperties(ElementInfo e) => StringJoin(e.Properties.Select(p => $"{p.Key}='{p.Value}'"), "\r\n\t\t"); + static string StringJoin(IEnumerable lines, string separator) => string.Join(separator, lines); } } From 4b13a970c810f1facc1ebe9077f102f723b00760 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 11:04:37 -0700 Subject: [PATCH 13/23] Rename Scan_Integration_WildlifeManager_Scoped -> Scan_Integration_WildlifeManager_ValidRoot, ensure valid HWND is passed to Scanner. --- src/AutomationTests/AutomationIntegrationTests.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 4d952c612..41ba93021 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -96,7 +96,7 @@ public void Scan_Integration_WildlifeManager(bool sync) [DataTestMethod] [DataRow(true)] [DataRow(false)] - public void Scan_Integration_WildlifeManager_Scoped(bool sync) + public void Scan_Integration_WildlifeManager_ValidRoot(bool sync) { RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => { @@ -104,11 +104,11 @@ static ScanOptions makeScopedScanOptions(int _) { using (DesktopElement focusedElement = A11yAutomationUtilities.GetFocusedElement()) { - var leafElement = A11yAutomationUtilities.GetDepthFirstLastLeafControlElement(focusedElement); + var leafElement = A11yAutomationUtilities.GetDepthFirstLastLeafHWNDElement(focusedElement); return new ScanOptions(scanRootWindowHandle: leafElement.NativeWindowHandle); } } - ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: 1, processId: null, makeScopedScanOptions); + ScanIntegrationCore(sync, _wildlifeManagerAppPath, 2 * WildlifeManagerKnownErrorCount, expectedWindowCount: WildlifeManagerKnownErrorCount, processId: null, makeScanOptions: makeScopedScanOptions); }); } @@ -120,7 +120,7 @@ public void Scan_Integration_WildlifeManager_InvalidRoot(bool sync) RunWithTimedExecutionWrapper(TimeSpan.FromSeconds(30), () => { static ScanOptions makeScanOptionsWithInvalidRoot(int _) => new(scanRootWindowHandle: new IntPtr(42)); - ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, expectedWindowCount: WildlifeManagerKnownErrorCount, processId: null, makeScanOptionsWithInvalidRoot); + ScanIntegrationCore(sync, _wildlifeManagerAppPath, WildlifeManagerKnownErrorCount, processId: null, makeScanOptions: makeScanOptionsWithInvalidRoot); }); } @@ -363,12 +363,6 @@ private WindowScanOutput ValidateOutput(IReadOnlyCollection ou Assert.AreEqual(expectedErrorCount, aggregateErrorCount, message: IsTestRunningInPipeline() ? string.Empty : PrintAll(output)); Assert.AreEqual(expectedErrorCount, totalErrors); - string PrintOutput() => StringJoin(output.Select(PrintErrors),Environment.NewLine); - static string PrintErrors(WindowScanOutput output, int index) => $"Output #{index}:\r\n\t{StringJoin(output.Errors.Select(PrintError), "\r\n\t")}"; - static string PrintError(ScanResult error, int index) => $"Error #{index}: {error.Rule}\r\n\t\t{PrintElementProperties(error.Element)}"; - static string PrintElementProperties(ElementInfo e) => StringJoin(e.Properties.Select(p => $"{p.Key}='{p.Value}'"),"\r\n\t\t"); - static string StringJoin(IEnumerable lines, string separator) => string.Join(separator, lines); - if (expectedErrorCount > 0) { var regexForExpectedFile = $"{_outputDir.Replace("\\", "\\\\")}.*\\.a11ytest"; From 4ae37bc129d1aca82f6be36e17ad81500a9c144a Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 12:05:12 -0700 Subject: [PATCH 14/23] Setup ScanTools.ScanRootWindowHandle for SnapshotCommandTests. --- src/AutomationTests/SnapshotCommandTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AutomationTests/SnapshotCommandTests.cs b/src/AutomationTests/SnapshotCommandTests.cs index a2dc68a8f..163e255bf 100644 --- a/src/AutomationTests/SnapshotCommandTests.cs +++ b/src/AutomationTests/SnapshotCommandTests.cs @@ -80,7 +80,7 @@ private void SetupDpiAwarenessMock(object dataFromEnable) _scanToolsMock.Setup(x => x.DpiAwareness).Returns(_dpiAwarenessMock.Object); } - private void SetupScanToolsMock(bool withResultsAssembler = true, bool withOutputFileHelper = false, bool withShouldTestAllChromiumContent = true) + private void SetupScanToolsMock(bool withResultsAssembler = true, bool withOutputFileHelper = false, bool withShouldTestAllChromiumContent = true, bool setupRootHandle = true) { _scanToolsMock.Setup(x => x.TargetElementLocator).Returns(_targetElementLocatorMock.Object); _scanToolsMock.Setup(x => x.Actions).Returns(_actionsMock.Object); @@ -96,6 +96,10 @@ private void SetupScanToolsMock(bool withResultsAssembler = true, bool withOutpu { _actionsMock.Setup(x => x.SetShouldTestAllChromiumContent(false)); } + if (setupRootHandle) + { + _scanToolsMock.Setup(x => x.ScanRootWindowHandle).Returns(IntPtr.Zero); + } } private void SetupActionsMock(string expectedPath = "") @@ -194,7 +198,7 @@ public async Task ExecuteAsync_NullAxeWindowsActions_ThrowsException() [Timeout(1000)] public async Task ExecuteAsync_NullDpiAwareness_ThrowsException() { - SetupScanToolsMock(withResultsAssembler: false, withShouldTestAllChromiumContent: false); + SetupScanToolsMock(withResultsAssembler: false, withShouldTestAllChromiumContent: false, setupRootHandle: false); _scanToolsMock.Setup(x => x.DpiAwareness).Returns(null); var action = new Func(() => SnapshotCommand.ExecuteAsync(_minimalConfig, _scanToolsMock.Object, CancellationToken.None)); From 1ddf12cd67dee625ea047491865b90fdcc906589 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 12:06:05 -0700 Subject: [PATCH 15/23] Update WebViewSampleKnownErrorCount to match actual number reported by the test. --- src/AutomationTests/AutomationIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 41ba93021..61f41fb97 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -28,7 +28,7 @@ public class AutomationIntegrationTests const int WpfControlSamplerKnownErrorCount = 7; const int WindowsFormsMultiWindowSamplerAppAllErrorCount = 12; // Note: This should change to 159 after https://github.com/MicrosoftEdge/WebView2Feedback/issues/3530 is fixed and integrated - const int WebViewSampleKnownErrorCount = 6; + const int WebViewSampleKnownErrorCount = 7; readonly string _wildlifeManagerAppPath = Path.GetFullPath("../../../../../tools/WildlifeManager/WildlifeManager.exe"); readonly string _win32ControlSamplerAppPath = Path.GetFullPath("../../../../../tools/Win32ControlSampler/Win32ControlSampler.exe"); From 3e5f8ebddecdf5dfbb54483e29fc0d5067592e1d Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 3 May 2024 12:19:01 -0700 Subject: [PATCH 16/23] Update CLI docs to describe ScanRootWindowHandle parameter. --- src/CLI/README.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CLI/README.MD b/src/CLI/README.MD index b82a6bcf0..8857e0df1 100644 --- a/src/CLI/README.MD +++ b/src/CLI/README.MD @@ -27,6 +27,8 @@ Copyright c 2020 --scanid Scan ID + --scanrootwindowhandle The HWND of the window to scan. + --verbosity Verbosity level (Quiet/Default/Verbose) --showthirdpartynotices Display Third Party Notices (opens file in browser @@ -61,6 +63,7 @@ processId|Identifies the process ID of the application to be scanned. Must be sp processName|Identifies the name of the process to be scanned. Requires that the process to scan be the _only_ process of that name currently running on the system. outputDirectory|Identifies the folder where output files will be created. If not specified, this will default to `.\AxeWindowsOutputFiles` (relative to the current working directory) scanId|Identifies the specific ID of the scan. This allows you to preassign a name to the given scan (and output file). If omitted, an dynamic name in the format AxeWindows_YY-MM-DD_hh-mm-ss.fffffff will be used. +scanRootWindowHandle|The HWND of the UIA element whose subtree should be scanned. If omitted, the root's children/grandchildren with matching processId will be scanned. verbosity|Identifies the level of detail you want in the output. Valid values are `quiet` (minimal output), `default` (typical output), or `verbose` (maximum output). showThirdPartyNotices|If specified, displays the third party notices for components used by AxeWindowsCLI. This information is also available in the `thirdpartynotices.html` file that is installed with AxeWindowsCLI. delayInSeconds|Optionally inserts a delay before triggering the scan. This allows transient controls (menus, drop-down-lists, etc.) to be scanned. From 028fa58eb5dd7da7268298cf694206eafbcbaff5 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Fri, 10 May 2024 07:32:21 -0700 Subject: [PATCH 17/23] Update docs for ScanOptions' constructor. --- docs/AutomationReference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/AutomationReference.md b/docs/AutomationReference.md index 2a522602e..0f8b659f4 100644 --- a/docs/AutomationReference.md +++ b/docs/AutomationReference.md @@ -172,6 +172,7 @@ The `ScanOptions` constructor accepts the following arguments: **Name** | **Type** | **Description** | **Default value** ---|---|---|--- scanId | `string` | A string identifier for the scan. If the scan produces output files based on the `Config` object used to create the scanner, the output files will be given the name of the scan id (e.g., MyScanId.a11ytest). | `null` +scanRootWindowHandle | `IntPtr?` | The native window handle (HWND) of the UIA element whose sub-tree should be scanned. If not specified or no element matches the HWND, the process's full UIA tree will be scanned. | `null` #### ScanOutput Methods of `IScanner` return a `ScanOutput` object with the following properties: From 66373f3f669fdee9380c3d5d56dbc088714da35a Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Sat, 11 May 2024 19:45:01 -0700 Subject: [PATCH 18/23] Move using --- src/Automation/Interfaces/IScanTools.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Automation/Interfaces/IScanTools.cs b/src/Automation/Interfaces/IScanTools.cs index 5d7a5560d..10ab6409e 100644 --- a/src/Automation/Interfaces/IScanTools.cs +++ b/src/Automation/Interfaces/IScanTools.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + namespace Axe.Windows.Automation { - using System; - /// /// Encapsulates the set of tools used to scan, assemble results, and write output files /// From a5c92426addc0177f1fa301b9e992272414f9b7e Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Mon, 13 May 2024 09:35:02 -0700 Subject: [PATCH 19/23] Remove nullable operator from IntPtr docs. --- docs/AutomationReference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/AutomationReference.md b/docs/AutomationReference.md index 0f8b659f4..de1e0fa20 100644 --- a/docs/AutomationReference.md +++ b/docs/AutomationReference.md @@ -172,7 +172,7 @@ The `ScanOptions` constructor accepts the following arguments: **Name** | **Type** | **Description** | **Default value** ---|---|---|--- scanId | `string` | A string identifier for the scan. If the scan produces output files based on the `Config` object used to create the scanner, the output files will be given the name of the scan id (e.g., MyScanId.a11ytest). | `null` -scanRootWindowHandle | `IntPtr?` | The native window handle (HWND) of the UIA element whose sub-tree should be scanned. If not specified or no element matches the HWND, the process's full UIA tree will be scanned. | `null` +scanRootWindowHandle | `IntPtr` | The native window handle (HWND) of the UIA element whose sub-tree should be scanned. If not specified or no element matches the HWND, the process's full UIA tree will be scanned. | `null` #### ScanOutput Methods of `IScanner` return a `ScanOutput` object with the following properties: From 52b76d59dfeca76ea2c9496ab9f43d0c1a3d16ed Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Mon, 13 May 2024 11:34:47 -0700 Subject: [PATCH 20/23] Prepend HWND CLI banner output with space. --- src/CLI/Resources/DisplayStrings.Designer.cs | 2 +- src/CLI/Resources/DisplayStrings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CLI/Resources/DisplayStrings.Designer.cs b/src/CLI/Resources/DisplayStrings.Designer.cs index bea4d348e..c6aae3c98 100644 --- a/src/CLI/Resources/DisplayStrings.Designer.cs +++ b/src/CLI/Resources/DisplayStrings.Designer.cs @@ -313,7 +313,7 @@ internal static string ScanTargetProcessNameFormat { } /// - /// Looks up a localized string similar to HWND = {0}. + /// Looks up a localized string similar to HWND = {0}. /// internal static string ScanTargetRootWindowHandleFormat { get { diff --git a/src/CLI/Resources/DisplayStrings.resx b/src/CLI/Resources/DisplayStrings.resx index 9fec040f9..4bffb063c 100644 --- a/src/CLI/Resources/DisplayStrings.resx +++ b/src/CLI/Resources/DisplayStrings.resx @@ -223,7 +223,7 @@ {0} is the process name. Leading space is intentional - HWND = {0} + HWND = {0} {0} is the scan root window handle From b0662ae16d812029e131364b13c1fb7d628a9ee7 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Mon, 13 May 2024 11:35:13 -0700 Subject: [PATCH 21/23] Add test to cover root window handle banner output. --- src/CLITests/OutputGeneratorTests.cs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/CLITests/OutputGeneratorTests.cs b/src/CLITests/OutputGeneratorTests.cs index 5aeb725cb..3f4919638 100644 --- a/src/CLITests/OutputGeneratorTests.cs +++ b/src/CLITests/OutputGeneratorTests.cs @@ -26,6 +26,7 @@ public class OutputGeneratorTests const string ScanTargetIntro = "Scan Target:"; const string ScanTargetProcessNameStart = " Process Name ="; const string ScanTargetProcessIdStart = " Process ID ="; + const string ScanTargetRootWindowHandleStart = " HWND ="; const string ScanTargetComma = ","; const string ScanIdStart = "Scan Id ="; const string ErrorCountGeneralStart = "{0} errors "; @@ -63,7 +64,7 @@ private void VerifyAllMocks() private void SetOptions(VerbosityLevel verbosityLevel = VerbosityLevel.Default, string processName = null, int processId = -1, - string scanId = null) + string scanId = null, IntPtr? scanRootWindowHandle = null) { _optionsMock.Setup(x => x.VerbosityLevel).Returns(verbosityLevel); _optionsMock.Setup(x => x.ProcessName).Returns(processName); @@ -71,7 +72,7 @@ private void SetOptions(VerbosityLevel verbosityLevel = VerbosityLevel.Default, _optionsMock.Setup(x => x.ScanId).Returns(scanId); if (processId > 0 || !string.IsNullOrEmpty(processName)) { - _optionsMock.Setup(x => x.ScanRootWindowHandle).Returns(IntPtr.Zero); + _optionsMock.Setup(x => x.ScanRootWindowHandle).Returns(scanRootWindowHandle.GetValueOrDefault(IntPtr.Zero)); } } @@ -183,6 +184,28 @@ public void WriteBanner_VerbosityIsDefault_ProcessName_ProcessId_NoScanId_Writes VerifyAllMocks(); } + [TestMethod] + [Timeout(1000)] + public void WriteBanner_VerbosityIsDefault_ProcessId_ScanRootWindowHandle_WritesHWND() + { + SetOptions(processId: TestProcessId, scanRootWindowHandle: new(47)); + WriteCall[] expectedCalls = + { + new WriteCall(AppTitleStart, WriteSource.WriteLineOneParam), + new WriteCall(ScanTargetIntro, WriteSource.WriteStringOnly), + new WriteCall(ScanTargetProcessIdStart, WriteSource.WriteOneParam), + new WriteCall(ScanTargetComma, WriteSource.WriteStringOnly), + new WriteCall(ScanTargetRootWindowHandleStart, WriteSource.WriteOneParam), + new WriteCall(null, WriteSource.WriteLineEmpty), + }; + TextWriterVerifier textWriterVerifier = new TextWriterVerifier(_writerMock, expectedCalls); + + _testSubject.WriteBanner(_optionsMock.Object); + + textWriterVerifier.VerifyAll(); + VerifyAllMocks(); + } + [TestMethod] [Timeout(1000)] public void WriteBanner_VerbosityIsDefault_NoProcessName_NoProcessId_ScanId_WritesAppHeaderAndScanId() From 0a39f01010394ae89f6fc3cd6c2094426b5655f4 Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Mon, 13 May 2024 11:36:26 -0700 Subject: [PATCH 22/23] Make TextWriterVerifier.VerifyAll write the expected format with the actual format to clarify why comparisons failed. --- src/CLITests/TextWriterVerifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CLITests/TextWriterVerifier.cs b/src/CLITests/TextWriterVerifier.cs index 90bfe5423..f45b92b7f 100644 --- a/src/CLITests/TextWriterVerifier.cs +++ b/src/CLITests/TextWriterVerifier.cs @@ -100,7 +100,7 @@ public void VerifyAll() } else { - Assert.IsTrue(actualCall.Format.StartsWith(expectedWriteCall.Format), "Actual Format = " + actualCall.Format); + Assert.IsTrue(actualCall.Format.StartsWith(expectedWriteCall.Format), $"Actual Format = '{actualCall.Format}'; Expected Format = '{expectedWriteCall.Format}'"); } verified++; } From 37b41b36ca4d681a7b65bc7d5325bf298513c2db Mon Sep 17 00:00:00 2001 From: Forrest Dillaway Date: Mon, 13 May 2024 12:28:17 -0700 Subject: [PATCH 23/23] Add Integration test to ensure invalid process ID throws expected Exception. --- src/AutomationTests/AutomationIntegrationTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/AutomationTests/AutomationIntegrationTests.cs b/src/AutomationTests/AutomationIntegrationTests.cs index 61f41fb97..6844078ad 100644 --- a/src/AutomationTests/AutomationIntegrationTests.cs +++ b/src/AutomationTests/AutomationIntegrationTests.cs @@ -80,6 +80,14 @@ public void Cleanup() CleanupTestOutput(); } + [TestMethod] + [ExpectedException(typeof(AxeWindowsAutomationException))] + public void Scan_Integration_InvalidProcessId() + { + const int BogusProcessId = 47; + ScanIntegrationCore(sync: true, testAppPath: null, expectedErrorCount: 0, processId: BogusProcessId); + } + [DataTestMethod] [DataRow(true)] [DataRow(false)]