ElementExtensions.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Reactive.Disposables;
  5. using System.Runtime.InteropServices;
  6. using System.Threading;
  7. using OpenQA.Selenium;
  8. using OpenQA.Selenium.Appium;
  9. using OpenQA.Selenium.Interactions;
  10. using Xunit;
  11. namespace Avalonia.IntegrationTests.Appium
  12. {
  13. public record WindowChrome(
  14. AppiumWebElement? Close,
  15. AppiumWebElement? Minimize,
  16. AppiumWebElement? Maximize,
  17. AppiumWebElement? FullScreen,
  18. AppiumWebElement? TitleBar)
  19. {
  20. public bool IsAnyButtonEnabled => (TitleBar is null || TitleBar.Enabled) &&
  21. (Close?.Enabled == true
  22. || Minimize?.Enabled == true
  23. || Maximize?.Enabled == true
  24. || FullScreen?.Enabled == true);
  25. public int TitleBarHeight => TitleBar?.Size.Height ?? -1;
  26. public int MaxButtonHeight =>
  27. Math.Max(
  28. Math.Max(Close?.Size.Height ?? -1, Minimize?.Size.Height ?? -1),
  29. Math.Max(Maximize?.Size.Height ?? -1, FullScreen?.Size.Height ?? -1));
  30. }
  31. internal static class ElementExtensions
  32. {
  33. public static IReadOnlyList<AppiumWebElement> GetChildren(this AppiumWebElement element) =>
  34. element.FindElementsByXPath("*/*");
  35. public static WindowChrome GetSystemChromeButtons(this AppiumWebElement window)
  36. {
  37. if (OperatingSystem.IsMacOS())
  38. {
  39. var closeButton = window.FindElementsByAccessibilityId("_XCUI:CloseWindow").FirstOrDefault();
  40. var fullscreenButton = window.FindElementsByAccessibilityId("_XCUI:FullScreenWindow").FirstOrDefault();
  41. var minimizeButton = window.FindElementsByAccessibilityId("_XCUI:MinimizeWindow").FirstOrDefault();
  42. var zoomButton = window.FindElementsByAccessibilityId("_XCUI:ZoomWindow").FirstOrDefault();
  43. return new(closeButton, minimizeButton, zoomButton, fullscreenButton, null);
  44. }
  45. if (OperatingSystem.IsWindows())
  46. {
  47. var titlebar = window.FindElementsByTagName("TitleBar").FirstOrDefault();
  48. var closeButton = titlebar?.FindElementByName("Close");
  49. var minimizeButton = titlebar?.FindElementByName("Minimize");
  50. var maximizeButton = titlebar?.FindElementByName("Maximize");
  51. return new(closeButton, minimizeButton, maximizeButton, null, titlebar);
  52. }
  53. throw new NotSupportedException("GetChromeButtons not supported on this platform.");
  54. }
  55. public static WindowChrome GetClientChromeButtons(this AppiumWebElement window)
  56. {
  57. var titlebar = window.FindElementsByAccessibilityId("AvaloniaTitleBar")?.FirstOrDefault();
  58. var closeButton = titlebar?.FindElementByName("Close");
  59. var minimizeButton = titlebar?.FindElementByName("Minimize");
  60. var maximizeButton = titlebar?.FindElementByName("Maximize");
  61. return new(closeButton, minimizeButton, maximizeButton, null, titlebar);
  62. }
  63. public static string GetComboBoxValue(this AppiumWebElement element)
  64. {
  65. return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
  66. element.Text :
  67. element.GetAttribute("value");
  68. }
  69. public static string GetName(this AppiumWebElement element) => GetAttribute(element, "Name", "title");
  70. public static bool? GetIsChecked(this AppiumWebElement element) =>
  71. GetAttribute(element, "Toggle.ToggleState", "value") switch
  72. {
  73. "0" => false,
  74. "1" => true,
  75. "2" => null,
  76. _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.")
  77. };
  78. public static bool GetIsFocused(this AppiumWebElement element)
  79. {
  80. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  81. {
  82. return element.GetAttribute("HasKeyboardFocus") == "True";
  83. }
  84. else
  85. {
  86. // https://stackoverflow.com/questions/71807788/check-if-element-is-focused-in-appium
  87. throw new NotSupportedException("Couldn't work out how to check if an element is focused on mac.");
  88. }
  89. }
  90. public static AppiumWebElement GetCurrentSingleWindow(this AppiumDriver session)
  91. {
  92. if (OperatingSystem.IsMacOS())
  93. {
  94. // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed
  95. // but in the meantime use the `parent::' selector to return the parent "real" window.
  96. return session.FindElementByXPath(
  97. $"XCUIElementTypeWindow//*/parent::XCUIElementTypeWindow");
  98. }
  99. else
  100. {
  101. return session.FindElementByXPath($"//Window");
  102. }
  103. }
  104. public static AppiumWebElement GetWindowById(this AppiumDriver session, string identifier)
  105. {
  106. if (OperatingSystem.IsMacOS())
  107. {
  108. return session.FindElementByXPath(
  109. $"XCUIElementTypeWindow[@identifier='{identifier}']");
  110. }
  111. else
  112. {
  113. return session.FindElementByXPath($"//Window[@AutomationId='{identifier}']");
  114. }
  115. }
  116. /// <summary>
  117. /// Clicks a button which is expected to open a new window.
  118. /// </summary>
  119. /// <param name="element">The button to click.</param>
  120. /// <returns>
  121. /// An object which when disposed will cause the newly opened window to close.
  122. /// </returns>
  123. public static IDisposable OpenWindowWithClick(this AppiumWebElement element, TimeSpan? delay = null)
  124. {
  125. var session = element.WrappedDriver;
  126. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  127. {
  128. var oldHandle = session.CurrentWindowHandle;
  129. var oldHandles = session.WindowHandles.ToList();
  130. var oldChildWindows = session.FindElements(By.XPath("//Window"));
  131. element.Click();
  132. if (delay is not null)
  133. Thread.Sleep((int)delay.Value.TotalMilliseconds);
  134. string? newHandle = null;
  135. IWebElement? newChildWindow = null;
  136. for (var i = 0; i < 10; ++i)
  137. {
  138. newHandle = session.WindowHandles.Except(oldHandles).SingleOrDefault();
  139. if (newHandle is not null)
  140. break;
  141. // If a new window handle hasn't been added to the session then it's likely
  142. // that a child window was opened. These don't appear in session.WindowHandles
  143. // so we have to use an XPath query to get hold of it.
  144. var newChildWindows = session.FindElements(By.XPath("//Window"));
  145. newChildWindow = newChildWindows.Except(oldChildWindows).SingleOrDefault();
  146. if (newChildWindow is not null)
  147. break;
  148. Thread.Sleep(100);
  149. }
  150. if (newHandle is not null)
  151. {
  152. // A new top-level window was opened. We need to switch to it.
  153. session.SwitchTo().Window(newHandle);
  154. return Disposable.Create(() =>
  155. {
  156. session.Close();
  157. session.SwitchTo().Window(oldHandle);
  158. });
  159. }
  160. if (newChildWindow is not null)
  161. {
  162. return Disposable.Create(() =>
  163. {
  164. newChildWindow.SendKeys(Keys.Alt + Keys.F4 + Keys.Alt);
  165. });
  166. }
  167. Assert.Fail("Could not find the newly opened window");
  168. return Disposable.Empty;
  169. }
  170. else
  171. {
  172. var oldWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"));
  173. var oldWindowTitles = oldWindows.ToDictionary(x => x.Text);
  174. element.Click();
  175. // Wait for animations to run.
  176. Thread.Sleep(1000);
  177. var newWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"));
  178. // Try to find the new window by looking for a window with a title that didn't exist before the button
  179. // was clicked. Sometimes it seems that when a window becomes fullscreen, all other windows in the
  180. // application lose their titles, so filter out windows with no title (this may have started happening
  181. // with macOS 13.1?)
  182. var newWindowTitles = newWindows
  183. .Select(x => (x.Text, x))
  184. .Where(x => !string.IsNullOrEmpty(x.Text))
  185. .ToDictionary(x => x.Text, x => x.x);
  186. var newWindowTitle = Assert.Single(newWindowTitles.Keys.Except(oldWindowTitles.Keys));
  187. return Disposable.Create(() =>
  188. {
  189. // TODO: We should be able to use Cmd+W here but Avalonia apps don't seem to have this shortcut
  190. // set up by default.
  191. var windows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"));
  192. var text = windows.Select(x => x.Text).ToList();
  193. var newWindow = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow"))
  194. .First(x => x.Text == newWindowTitle);
  195. var close = ((AppiumWebElement)newWindow).FindElementByAccessibilityId("_XCUI:CloseWindow");
  196. close!.Click();
  197. Thread.Sleep(1000);
  198. });
  199. }
  200. }
  201. public static void SendClick(this AppiumWebElement element)
  202. {
  203. // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls
  204. // such as list items don't support this action, so instead simulate a physical click as VoiceOver
  205. // does. On Windows, Click() seems to fail with the WindowState checkbox for some reason.
  206. new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform();
  207. }
  208. public static void SendDoubleClick(this AppiumWebElement element)
  209. {
  210. new Actions(element.WrappedDriver).MoveToElement(element).DoubleClick().Perform();
  211. }
  212. public static void MovePointerOver(this AppiumWebElement element)
  213. {
  214. new Actions(element.WrappedDriver).MoveToElement(element).Perform();
  215. }
  216. public static string GetAttribute(AppiumWebElement element, string windows, string macOS)
  217. {
  218. return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS);
  219. }
  220. }
  221. }