WindowTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. using System;
  2. using System.Linq;
  3. using System.Runtime.InteropServices;
  4. using System.Threading;
  5. using Avalonia.Controls;
  6. using OpenQA.Selenium;
  7. using OpenQA.Selenium.Appium;
  8. using OpenQA.Selenium.Interactions;
  9. using SixLabors.ImageSharp.PixelFormats;
  10. using Xunit;
  11. using Xunit.Sdk;
  12. namespace Avalonia.IntegrationTests.Appium
  13. {
  14. [Collection("Default")]
  15. public class WindowTests
  16. {
  17. private readonly AppiumDriver<AppiumWebElement> _session;
  18. public WindowTests(DefaultAppFixture fixture)
  19. {
  20. _session = fixture.Session;
  21. var tabs = _session.FindElementByAccessibilityId("MainTabs");
  22. var tab = tabs.FindElementByName("Window");
  23. tab.Click();
  24. }
  25. [Theory]
  26. [MemberData(nameof(StartupLocationData))]
  27. public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location, bool canResize)
  28. {
  29. using var window = OpenWindow(size, mode, location, canResize: canResize);
  30. var info = GetWindowInfo();
  31. if (size.HasValue)
  32. Assert.Equal(size.Value, info.ClientSize);
  33. Assert.True(info.FrameSize.Width >= info.ClientSize.Width, "Expected frame width >= client width.");
  34. Assert.True(info.FrameSize.Height > info.ClientSize.Height, "Expected frame height > client height.");
  35. var frameRect = new PixelRect(info.Position, PixelSize.FromSize(info.FrameSize, info.Scaling));
  36. switch (location)
  37. {
  38. case WindowStartupLocation.CenterScreen:
  39. {
  40. var expected = info.ScreenRect.CenterRect(frameRect);
  41. AssertCloseEnough(expected.Position, frameRect.Position);
  42. break;
  43. }
  44. case WindowStartupLocation.CenterOwner:
  45. {
  46. Assert.NotNull(info.OwnerRect);
  47. var expected = info.OwnerRect!.Value.CenterRect(frameRect);
  48. AssertCloseEnough(expected.Position, frameRect.Position);
  49. break;
  50. }
  51. }
  52. }
  53. [Theory]
  54. [MemberData(nameof(WindowStateData))]
  55. public void WindowState(Size? size, ShowWindowMode mode, WindowState state, bool canResize)
  56. {
  57. using var window = OpenWindow(size, mode, state: state, canResize: canResize);
  58. try
  59. {
  60. var info = GetWindowInfo();
  61. Assert.Equal(state, info.WindowState);
  62. switch (state)
  63. {
  64. case Controls.WindowState.Normal:
  65. Assert.True(info.FrameSize.Width * info.Scaling < info.ScreenRect.Size.Width);
  66. Assert.True(info.FrameSize.Height * info.Scaling < info.ScreenRect.Size.Height);
  67. break;
  68. case Controls.WindowState.Maximized:
  69. case Controls.WindowState.FullScreen:
  70. Assert.True(info.FrameSize.Width * info.Scaling >= info.ScreenRect.Size.Width);
  71. Assert.True(info.FrameSize.Height * info.Scaling >= info.ScreenRect.Size.Height);
  72. break;
  73. }
  74. }
  75. finally
  76. {
  77. if (state == Controls.WindowState.FullScreen)
  78. {
  79. try
  80. {
  81. _session.FindElementByAccessibilityId("CurrentWindowState").SendClick();
  82. _session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
  83. // Wait for animations to run.
  84. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  85. Thread.Sleep(1000);
  86. }
  87. catch
  88. {
  89. /* Ignore errors in cleanup */
  90. }
  91. }
  92. }
  93. }
  94. [PlatformFact(TestPlatforms.Windows)]
  95. public void OnWindows_Docked_Windows_Retain_Size_Position_When_Restored()
  96. {
  97. using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
  98. {
  99. var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
  100. Assert.Equal("Normal", windowState.GetComboBoxValue());
  101. var window = _session.FindElements(By.XPath("//Window")).First();
  102. new Actions(_session)
  103. .KeyDown(Keys.Meta)
  104. .SendKeys(Keys.Left)
  105. .KeyUp(Keys.Meta)
  106. .Perform();
  107. var original = GetWindowInfo();
  108. windowState.Click();
  109. _session.FindElementByName("Minimized").SendClick();
  110. new Actions(_session)
  111. .KeyDown(Keys.Alt)
  112. .SendKeys(Keys.Tab)
  113. .KeyUp(Keys.Alt)
  114. .Perform();
  115. var current = GetWindowInfo();
  116. Assert.Equal(original.Position, current.Position);
  117. Assert.Equal(original.FrameSize, current.FrameSize);
  118. }
  119. }
  120. [Fact]
  121. public void Showing_Window_With_Size_Larger_Than_Screen_Measures_Content_With_Working_Area()
  122. {
  123. using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
  124. {
  125. var screenRectTextBox = _session.FindElementByAccessibilityId("CurrentClientSize");
  126. var measuredWithTextBlock = _session.FindElementByAccessibilityId("CurrentMeasuredWithText");
  127. var measuredWithString = measuredWithTextBlock.Text;
  128. var workingAreaString = screenRectTextBox.Text;
  129. var workingArea = Size.Parse(workingAreaString);
  130. var measuredWith = Size.Parse(measuredWithString);
  131. Assert.Equal(workingArea, measuredWith);
  132. }
  133. }
  134. [Theory]
  135. [InlineData(ShowWindowMode.NonOwned)]
  136. [InlineData(ShowWindowMode.Owned)]
  137. [InlineData(ShowWindowMode.Modal)]
  138. public void ShowMode(ShowWindowMode mode)
  139. {
  140. using var window = OpenWindow(null, mode, WindowStartupLocation.Manual);
  141. var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
  142. var original = GetWindowInfo();
  143. Assert.Equal("Normal", windowState.GetComboBoxValue());
  144. windowState.Click();
  145. _session.FindElementByAccessibilityId("WindowStateMaximized").SendClick();
  146. Assert.Equal("Maximized", windowState.GetComboBoxValue());
  147. windowState.Click();
  148. _session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
  149. var current = GetWindowInfo();
  150. Assert.Equal(original.Position, current.Position);
  151. Assert.Equal(original.FrameSize, current.FrameSize);
  152. // On macOS, only non-owned windows can go fullscreen.
  153. if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned)
  154. {
  155. windowState.Click();
  156. _session.FindElementByAccessibilityId("WindowStateFullScreen").SendClick();
  157. Assert.Equal("FullScreen", windowState.GetComboBoxValue());
  158. current = GetWindowInfo();
  159. var clientSize = PixelSize.FromSize(current.ClientSize, current.Scaling);
  160. Assert.True(clientSize.Width >= current.ScreenRect.Width);
  161. Assert.True(clientSize.Height >= current.ScreenRect.Height);
  162. windowState.SendClick();
  163. _session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
  164. current = GetWindowInfo();
  165. Assert.Equal(original.Position, current.Position);
  166. Assert.Equal(original.FrameSize, current.FrameSize);
  167. }
  168. }
  169. [Fact]
  170. public void TransparentWindow()
  171. {
  172. var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow");
  173. showTransparentWindow.Click();
  174. Thread.Sleep(1000);
  175. var window = _session.FindElementByAccessibilityId("TransparentWindow");
  176. var screenshot = window.GetScreenshot();
  177. window.Click();
  178. var img = SixLabors.ImageSharp.Image.Load<Rgba32>(screenshot.AsByteArray);
  179. var topLeftColor = img[10, 10];
  180. var centerColor = img[img.Width / 2, img.Height / 2];
  181. Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
  182. Assert.Equal(new Rgba32(255, 0, 0), centerColor);
  183. }
  184. [Fact]
  185. public void TransparentPopup()
  186. {
  187. var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup");
  188. showTransparentWindow.Click();
  189. Thread.Sleep(1000);
  190. var window = _session.FindElementByAccessibilityId("TransparentPopupBackground");
  191. var container = window.FindElementByAccessibilityId("PopupContainer");
  192. var screenshot = container.GetScreenshot();
  193. window.Click();
  194. var img = SixLabors.ImageSharp.Image.Load<Rgba32>(screenshot.AsByteArray);
  195. var topLeftColor = img[10, 10];
  196. var centerColor = img[img.Width / 2, img.Height / 2];
  197. Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
  198. Assert.Equal(new Rgba32(255, 0, 0), centerColor);
  199. }
  200. [Theory]
  201. [InlineData(ShowWindowMode.NonOwned, true)]
  202. [InlineData(ShowWindowMode.Owned, true)]
  203. [InlineData(ShowWindowMode.Modal, true)]
  204. [InlineData(ShowWindowMode.NonOwned, false)]
  205. [InlineData(ShowWindowMode.Owned, false)]
  206. [InlineData(ShowWindowMode.Modal, false)]
  207. public void Window_Has_Disabled_Maximize_Button_When_CanResize_Is_False(ShowWindowMode mode, bool extendClientArea)
  208. {
  209. using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false, extendClientArea: extendClientArea))
  210. {
  211. var secondaryWindow = GetWindow("SecondaryWindow");
  212. AppiumWebElement? maximizeButton;
  213. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  214. {
  215. maximizeButton = extendClientArea ?
  216. secondaryWindow.FindElementByXPath("//Button[@Name='Maximize']") :
  217. secondaryWindow.FindElementByXPath("//TitleBar/Button[2]");
  218. }
  219. else
  220. {
  221. maximizeButton = mode == ShowWindowMode.NonOwned ?
  222. secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
  223. secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
  224. }
  225. Assert.False(maximizeButton.Enabled);
  226. }
  227. }
  228. public static TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool> StartupLocationData()
  229. {
  230. var sizes = new Size?[] { null, new Size(400, 300) };
  231. var data = new TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool>();
  232. foreach (var size in sizes)
  233. {
  234. foreach (var mode in Enum.GetValues<ShowWindowMode>())
  235. {
  236. foreach (var location in Enum.GetValues<WindowStartupLocation>())
  237. {
  238. if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned))
  239. {
  240. data.Add(size, mode, location, true);
  241. data.Add(size, mode, location, false);
  242. }
  243. }
  244. }
  245. }
  246. return data;
  247. }
  248. public static TheoryData<Size?, ShowWindowMode, WindowState, bool> WindowStateData()
  249. {
  250. var sizes = new Size?[] { null, new Size(400, 300) };
  251. var data = new TheoryData<Size?, ShowWindowMode, WindowState, bool>();
  252. foreach (var size in sizes)
  253. {
  254. foreach (var mode in Enum.GetValues<ShowWindowMode>())
  255. {
  256. foreach (var state in Enum.GetValues<WindowState>())
  257. {
  258. // Not sure how to handle testing minimized windows currently.
  259. if (state == Controls.WindowState.Minimized)
  260. continue;
  261. // Child/Modal windows cannot be fullscreen on macOS.
  262. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&
  263. state == Controls.WindowState.FullScreen &&
  264. mode != ShowWindowMode.NonOwned)
  265. continue;
  266. data.Add(size, mode, state, true);
  267. data.Add(size, mode, state, false);
  268. }
  269. }
  270. }
  271. return data;
  272. }
  273. private static void AssertCloseEnough(PixelPoint expected, PixelPoint actual)
  274. {
  275. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  276. {
  277. // On win32, accurate frame information cannot be obtained until a window is shown but
  278. // WindowStartupLocation needs to be calculated before the window is shown, meaning that
  279. // the position of a centered window can be off by a bit. From initial testing, looks
  280. // like this shouldn't be more than 10 pixels.
  281. if (Math.Abs(expected.X - actual.X) > 10)
  282. throw new EqualException(expected, actual);
  283. if (Math.Abs(expected.Y - actual.Y) > 10)
  284. throw new EqualException(expected, actual);
  285. }
  286. else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  287. {
  288. if (Math.Abs(expected.X - actual.X) > 15)
  289. throw new EqualException(expected, actual);
  290. if (Math.Abs(expected.Y - actual.Y) > 15)
  291. throw new EqualException(expected, actual);
  292. }
  293. else
  294. {
  295. Assert.Equal(expected, actual);
  296. }
  297. }
  298. private IDisposable OpenWindow(
  299. Size? size,
  300. ShowWindowMode mode,
  301. WindowStartupLocation location = WindowStartupLocation.Manual,
  302. WindowState state = Controls.WindowState.Normal,
  303. bool canResize = true,
  304. bool extendClientArea = false)
  305. {
  306. var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
  307. var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
  308. var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation");
  309. var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState");
  310. var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize");
  311. var showButton = _session.FindElementByAccessibilityId("ShowWindow");
  312. var extendClientAreaCheckBox = _session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
  313. if (size.HasValue)
  314. sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
  315. if (modeComboBox.GetComboBoxValue() != mode.ToString())
  316. {
  317. modeComboBox.Click();
  318. _session.FindElementByName(mode.ToString()).SendClick();
  319. }
  320. if (locationComboBox.GetComboBoxValue() != location.ToString())
  321. {
  322. locationComboBox.Click();
  323. _session.FindElementByName(location.ToString()).SendClick();
  324. }
  325. if (stateComboBox.GetComboBoxValue() != state.ToString())
  326. {
  327. stateComboBox.Click();
  328. _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick();
  329. }
  330. if (canResizeCheckBox.GetIsChecked() != canResize)
  331. canResizeCheckBox.Click();
  332. if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
  333. extendClientAreaCheckBox.Click();
  334. return showButton.OpenWindowWithClick();
  335. }
  336. private AppiumWebElement GetWindow(string identifier)
  337. {
  338. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  339. {
  340. // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed
  341. // but in the meantime use the `parent::' selector to return the parent "real" window.
  342. return _session.FindElementByXPath(
  343. $"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow");
  344. }
  345. else
  346. {
  347. return _session.FindElementByXPath($"//Window[@AutomationId='{identifier}']");
  348. }
  349. }
  350. private WindowInfo GetWindowInfo()
  351. {
  352. PixelRect? ReadOwnerRect()
  353. {
  354. var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text;
  355. return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
  356. }
  357. var retry = 0;
  358. for (;;)
  359. {
  360. try
  361. {
  362. return new(
  363. Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text),
  364. Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text),
  365. PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text),
  366. ReadOwnerRect(),
  367. PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text),
  368. double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text),
  369. Enum.Parse<WindowState>(_session.FindElementByAccessibilityId("CurrentWindowState").Text));
  370. }
  371. catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3)
  372. {
  373. // MacOS sometimes seems to need a bit of time to get itself back in order after switching out
  374. // of fullscreen.
  375. Thread.Sleep(1000);
  376. }
  377. }
  378. }
  379. public enum ShowWindowMode
  380. {
  381. NonOwned,
  382. Owned,
  383. Modal
  384. }
  385. private record WindowInfo(
  386. Size ClientSize,
  387. Size FrameSize,
  388. PixelPoint Position,
  389. PixelRect? OwnerRect,
  390. PixelRect ScreenRect,
  391. double Scaling,
  392. WindowState WindowState);
  393. }
  394. }