WindowTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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 _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 Extended_Client_Window_Shows_With_Requested_Size()
  171. {
  172. var clientSize = new Size(400, 400);
  173. using var window = OpenWindow(clientSize, ShowWindowMode.NonOwned, WindowStartupLocation.CenterScreen, extendClientArea: true);
  174. var windowState = _session.FindElementByAccessibilityId("CurrentWindowState");
  175. var current = GetWindowInfo();
  176. Assert.Equal(current.ClientSize, clientSize);
  177. }
  178. [Fact]
  179. public void TransparentWindow()
  180. {
  181. var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow");
  182. showTransparentWindow.Click();
  183. Thread.Sleep(1000);
  184. var window = _session.FindElementByAccessibilityId("TransparentWindow");
  185. var screenshot = window.GetScreenshot();
  186. window.Click();
  187. var img = SixLabors.ImageSharp.Image.Load<Rgba32>(screenshot.AsByteArray);
  188. var topLeftColor = img[10, 10];
  189. var centerColor = img[img.Width / 2, img.Height / 2];
  190. Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
  191. Assert.Equal(new Rgba32(255, 0, 0), centerColor);
  192. }
  193. [Fact]
  194. public void TransparentPopup()
  195. {
  196. var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup");
  197. showTransparentWindow.Click();
  198. Thread.Sleep(1000);
  199. var window = _session.FindElementByAccessibilityId("TransparentPopupBackground");
  200. var container = window.FindElementByAccessibilityId("PopupContainer");
  201. var screenshot = container.GetScreenshot();
  202. window.Click();
  203. var img = SixLabors.ImageSharp.Image.Load<Rgba32>(screenshot.AsByteArray);
  204. var topLeftColor = img[10, 10];
  205. var centerColor = img[img.Width / 2, img.Height / 2];
  206. Assert.Equal(new Rgba32(0, 128, 0), topLeftColor);
  207. Assert.Equal(new Rgba32(255, 0, 0), centerColor);
  208. }
  209. [Theory]
  210. [InlineData(ShowWindowMode.NonOwned, true)]
  211. [InlineData(ShowWindowMode.Owned, true)]
  212. [InlineData(ShowWindowMode.Modal, true)]
  213. [InlineData(ShowWindowMode.NonOwned, false)]
  214. [InlineData(ShowWindowMode.Owned, false)]
  215. [InlineData(ShowWindowMode.Modal, false)]
  216. public void Window_Has_Disabled_Maximize_Button_When_CanResize_Is_False(ShowWindowMode mode, bool extendClientArea)
  217. {
  218. using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false, extendClientArea: extendClientArea))
  219. {
  220. var secondaryWindow = GetWindow("SecondaryWindow");
  221. AppiumWebElement? maximizeButton;
  222. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  223. {
  224. maximizeButton = extendClientArea ?
  225. secondaryWindow.FindElementByXPath("//Button[@Name='Maximize']") :
  226. secondaryWindow.FindElementByXPath("//TitleBar/Button[2]");
  227. }
  228. else
  229. {
  230. maximizeButton = mode == ShowWindowMode.NonOwned ?
  231. secondaryWindow.FindElementByAccessibilityId("_XCUI:FullScreenWindow") :
  232. secondaryWindow.FindElementByAccessibilityId("_XCUI:ZoomWindow");
  233. }
  234. Assert.False(maximizeButton.Enabled);
  235. }
  236. }
  237. public static TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool> StartupLocationData()
  238. {
  239. var sizes = new Size?[] { null, new Size(400, 300) };
  240. var data = new TheoryData<Size?, ShowWindowMode, WindowStartupLocation, bool>();
  241. foreach (var size in sizes)
  242. {
  243. foreach (var mode in Enum.GetValues<ShowWindowMode>())
  244. {
  245. foreach (var location in Enum.GetValues<WindowStartupLocation>())
  246. {
  247. if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned))
  248. {
  249. data.Add(size, mode, location, true);
  250. data.Add(size, mode, location, false);
  251. }
  252. }
  253. }
  254. }
  255. return data;
  256. }
  257. public static TheoryData<Size?, ShowWindowMode, WindowState, bool> WindowStateData()
  258. {
  259. var sizes = new Size?[] { null, new Size(400, 300) };
  260. var data = new TheoryData<Size?, ShowWindowMode, WindowState, bool>();
  261. foreach (var size in sizes)
  262. {
  263. foreach (var mode in Enum.GetValues<ShowWindowMode>())
  264. {
  265. foreach (var state in Enum.GetValues<WindowState>())
  266. {
  267. // Not sure how to handle testing minimized windows currently.
  268. if (state == Controls.WindowState.Minimized)
  269. continue;
  270. // Child/Modal windows cannot be fullscreen on macOS.
  271. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&
  272. state == Controls.WindowState.FullScreen &&
  273. mode != ShowWindowMode.NonOwned)
  274. continue;
  275. data.Add(size, mode, state, true);
  276. data.Add(size, mode, state, false);
  277. }
  278. }
  279. }
  280. return data;
  281. }
  282. private static void AssertCloseEnough(PixelPoint expected, PixelPoint actual)
  283. {
  284. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  285. {
  286. // On win32, accurate frame information cannot be obtained until a window is shown but
  287. // WindowStartupLocation needs to be calculated before the window is shown, meaning that
  288. // the position of a centered window can be off by a bit. From initial testing, looks
  289. // like this shouldn't be more than 10 pixels.
  290. if (Math.Abs(expected.X - actual.X) > 10)
  291. throw new EqualException(expected, actual);
  292. if (Math.Abs(expected.Y - actual.Y) > 10)
  293. throw new EqualException(expected, actual);
  294. }
  295. else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  296. {
  297. if (Math.Abs(expected.X - actual.X) > 15)
  298. throw new EqualException(expected, actual);
  299. if (Math.Abs(expected.Y - actual.Y) > 15)
  300. throw new EqualException(expected, actual);
  301. }
  302. else
  303. {
  304. Assert.Equal(expected, actual);
  305. }
  306. }
  307. private IDisposable OpenWindow(
  308. Size? size,
  309. ShowWindowMode mode,
  310. WindowStartupLocation location = WindowStartupLocation.Manual,
  311. WindowState state = Controls.WindowState.Normal,
  312. bool canResize = true,
  313. bool extendClientArea = false)
  314. {
  315. var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
  316. var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
  317. var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation");
  318. var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState");
  319. var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize");
  320. var showButton = _session.FindElementByAccessibilityId("ShowWindow");
  321. var extendClientAreaCheckBox = _session.FindElementByAccessibilityId("ShowWindowExtendClientAreaToDecorationsHint");
  322. if (size.HasValue)
  323. sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}");
  324. if (modeComboBox.GetComboBoxValue() != mode.ToString())
  325. {
  326. modeComboBox.Click();
  327. _session.FindElementByName(mode.ToString()).SendClick();
  328. }
  329. if (locationComboBox.GetComboBoxValue() != location.ToString())
  330. {
  331. locationComboBox.Click();
  332. _session.FindElementByName(location.ToString()).SendClick();
  333. }
  334. if (stateComboBox.GetComboBoxValue() != state.ToString())
  335. {
  336. stateComboBox.Click();
  337. _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick();
  338. }
  339. if (canResizeCheckBox.GetIsChecked() != canResize)
  340. canResizeCheckBox.Click();
  341. if (extendClientAreaCheckBox.GetIsChecked() != extendClientArea)
  342. extendClientAreaCheckBox.Click();
  343. return showButton.OpenWindowWithClick();
  344. }
  345. private AppiumWebElement GetWindow(string identifier)
  346. {
  347. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  348. {
  349. return _session.FindElementByXPath(
  350. $"XCUIElementTypeWindow[@identifier='{identifier}']");
  351. }
  352. else
  353. {
  354. return _session.FindElementByXPath($"//Window[@AutomationId='{identifier}']");
  355. }
  356. }
  357. private WindowInfo GetWindowInfo()
  358. {
  359. PixelRect? ReadOwnerRect()
  360. {
  361. var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text;
  362. return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
  363. }
  364. var retry = 0;
  365. for (;;)
  366. {
  367. try
  368. {
  369. return new(
  370. Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text),
  371. Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text),
  372. PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text),
  373. ReadOwnerRect(),
  374. PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text),
  375. double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text),
  376. Enum.Parse<WindowState>(_session.FindElementByAccessibilityId("CurrentWindowState").Text));
  377. }
  378. catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3)
  379. {
  380. // MacOS sometimes seems to need a bit of time to get itself back in order after switching out
  381. // of fullscreen.
  382. Thread.Sleep(1000);
  383. }
  384. }
  385. }
  386. public enum ShowWindowMode
  387. {
  388. NonOwned,
  389. Owned,
  390. Modal
  391. }
  392. private record WindowInfo(
  393. Size ClientSize,
  394. Size FrameSize,
  395. PixelPoint Position,
  396. PixelRect? OwnerRect,
  397. PixelRect ScreenRect,
  398. double Scaling,
  399. WindowState WindowState);
  400. }
  401. }