浏览代码

TrayIcon integration tests (#16154)

* Add accessibility ID to the TrayPopupRoot on Windows

* [Windows] Add left click and menu item click e2e tests for TrayIcon

* [Windows] Add TrayIcon visibility toggle tests

* Implement macOS tray icon tests

* Make it easier to read tray icon logs

* Try to handle win10 accessibility names

* Try to upload PageSource

* Set condition: always

* Hopefully, it works on CI

* Try to upload PageSource #2

* Fix win10, hopefully for the last time
Max Katz 1 年之前
父节点
当前提交
ad0bc0dabf

+ 7 - 2
samples/IntegrationTestApp/App.axaml

@@ -9,10 +9,15 @@
     </Application.Styles>
     <TrayIcon.Icons>
         <TrayIcons>
-            <TrayIcon Icon="/Assets/icon.ico">
+            <TrayIcon Icon="/Assets/icon.ico"
+                      ToolTipText="IntegrationTestApp TrayIcon"
+                      Command="{Binding TrayIconCommand}"
+                      CommandParameter="TrayIconClicked">
                 <TrayIcon.Menu>
                     <NativeMenu>
-                        <NativeMenuItem Header="Show _Test Window" Command="{Binding ShowWindowCommand}" />
+                        <NativeMenuItem Header="Raise Menu Clicked"
+                                        Command="{Binding TrayIconCommand}"
+                                        CommandParameter="TrayIconMenuClicked" />
                     </NativeMenu>
                 </TrayIcon.Menu>
             </TrayIcon>

+ 7 - 5
samples/IntegrationTestApp/App.axaml.cs

@@ -4,18 +4,20 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
+using Avalonia.Media;
 using MiniMvvm;
 
 namespace IntegrationTestApp
 {
     public class App : Application
     {
+        private MainWindow? _mainWindow;
+
         public App()
         {
-            ShowWindowCommand = MiniCommand.Create(() =>
+            TrayIconCommand = MiniCommand.Create<string>(name =>
             {
-                var window = new Window() { Title = "TrayIcon demo window" };
-                window.Show();
+                _mainWindow!.Get<CheckBox>(name).IsChecked = true;
             });
             DataContext = this;
         }
@@ -29,12 +31,12 @@ namespace IntegrationTestApp
         {
             if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
             {
-                desktop.MainWindow = new MainWindow();
+                desktop.MainWindow = _mainWindow = new MainWindow();
             }
 
             base.OnFrameworkInitializationCompleted();
         }
 
-        public ICommand ShowWindowCommand { get; }
+        public ICommand TrayIconCommand { get; }
     }
 }

+ 8 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -93,6 +93,14 @@
         </StackPanel>
       </TabItem>
 
+      <TabItem Header="Desktop">
+        <StackPanel>
+          <CheckBox x:FieldModifier="public" Name="TrayIconClicked">Tray Icon Clicked</CheckBox>
+          <CheckBox x:FieldModifier="public" Name="TrayIconMenuClicked">Tray Icon Menu Clicked</CheckBox>
+          <Button Name="ToggleTrayIconVisible" Content="Toggle TrayIcon Visible" />
+        </StackPanel>
+      </TabItem>
+
       <TabItem Header="Gestures">
         <DockPanel>
           <DockPanel DockPanel.Dock="Top">

+ 8 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -223,6 +223,12 @@ namespace IntegrationTestApp
             ownedWindow.Show(mainWindow);
         }
 
+        private void OnToggleTrayIconVisible()
+        {
+            var icon = TrayIcon.GetIcons(Application.Current!)!.FirstOrDefault()!;
+            icon.IsVisible = !icon.IsVisible;
+        }
+
         private void InitializeGesturesTab()
         {
             var gestureBorder = GestureBorder;
@@ -295,6 +301,8 @@ namespace IntegrationTestApp
                 OnApplyWindowDecorations(this);
             if (source?.Name == nameof(ShowNewWindowDecorations))
                 OnShowNewWindowDecorations();
+            if (source?.Name == nameof(ToggleTrayIconVisible))
+                OnToggleTrayIconVisible();
         }
 
         private void OnApplyWindowDecorations(Window window)

+ 2 - 1
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@@ -218,8 +218,9 @@ namespace Avalonia.Win32
                 return;
             }
 
-            var _trayMenu = new TrayPopupRoot()
+            var _trayMenu = new TrayPopupRoot
             {
+                Name = "AvaloniaTrayPopupRoot_" + _tooltipText,
                 SystemDecorations = SystemDecorations.None,
                 SizeToContent = SizeToContent.WidthAndHeight,
                 Background = null,

+ 27 - 8
tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs

@@ -16,7 +16,7 @@ namespace Avalonia.IntegrationTests.Appium
         {
             var options = new AppiumOptions();
 
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            if (OperatingSystem.IsWindows())
             {
                 ConfigureWin32Options(options);
                 Session = new WindowsDriver(
@@ -28,7 +28,7 @@ namespace Avalonia.IntegrationTests.Appium
                     Session.WindowHandles[0].Substring(2),
                     NumberStyles.AllowHexSpecifier)));
             }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            else if (OperatingSystem.IsMacOS())
             {
                 ConfigureMacOptions(options);
                 Session = new MacDriver(
@@ -37,21 +37,20 @@ namespace Avalonia.IntegrationTests.Appium
             }
             else
             {
-                throw new NotSupportedException("Unsupported platform.");
+                throw new PlatformNotSupportedException();
             }
         }
 
-        protected virtual void ConfigureWin32Options(AppiumOptions options)
+        protected virtual void ConfigureWin32Options(AppiumOptions options, string? app = null)
         {
-            var path = Path.GetFullPath(TestAppPath);
-            options.AddAdditionalCapability(MobileCapabilityType.App, path);
+            options.AddAdditionalCapability(MobileCapabilityType.App, app ?? Path.GetFullPath(TestAppPath));
             options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows);
             options.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC");
         }
 
-        protected virtual void ConfigureMacOptions(AppiumOptions options)
+        protected virtual void ConfigureMacOptions(AppiumOptions options, string? app = null)
         {
-            options.AddAdditionalCapability("appium:bundleId", TestAppBundleId);
+            options.AddAdditionalCapability("appium:bundleId", app ?? TestAppBundleId);
             options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS);
             options.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2");
             options.AddAdditionalCapability("appium:showServerLogs", true);
@@ -71,6 +70,26 @@ namespace Avalonia.IntegrationTests.Appium
             }
         }
 
+        public AppiumDriver CreateNestedSession(string appName)
+        {
+            var options = new AppiumOptions();
+            if (OperatingSystem.IsWindows())
+            {
+                ConfigureWin32Options(options, appName);
+            
+                return new WindowsDriver(new Uri("http://127.0.0.1:4723"), options);
+            }
+            else if (OperatingSystem.IsMacOS())
+            {
+                ConfigureMacOptions(options, appName);
+                return new MacDriver(new Uri("http://127.0.0.1:4723/wd/hub"), options);
+            }
+            else
+            {
+                throw new PlatformNotSupportedException();
+            }
+        }
+
         [DllImport("user32.dll")]
         [return: MarshalAs(UnmanagedType.Bool)]
         private static extern bool SetForegroundWindow(IntPtr hWnd);

+ 4 - 4
tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs

@@ -4,15 +4,15 @@ namespace Avalonia.IntegrationTests.Appium
 {
     public class OverlayPopupsAppFixture : DefaultAppFixture
     {
-        protected override void ConfigureWin32Options(AppiumOptions options)
+        protected override void ConfigureWin32Options(AppiumOptions options, string? app = null)
         {
-            base.ConfigureWin32Options(options);
+            base.ConfigureWin32Options(options, app);
             options.AddAdditionalCapability("appArguments", "--overlayPopups");
         }
 
-        protected override void ConfigureMacOptions(AppiumOptions options)
+        protected override void ConfigureMacOptions(AppiumOptions options, string? app = null)
         {
-            base.ConfigureMacOptions(options);
+            base.ConfigureMacOptions(options, app);
             options.AddAdditionalCapability("appium:arguments", new[] { "--overlayPopups" });
         }
     }

+ 152 - 0
tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs

@@ -0,0 +1,152 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Appium;
+using OpenQA.Selenium.Interactions;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Appium;
+
+[Collection("Default")]
+public class TrayIconTests : IDisposable
+{
+    private readonly AppiumDriver _session;
+    private readonly AppiumDriver? _rootSession;
+    private const string TrayIconName = "IntegrationTestApp TrayIcon";
+
+    public TrayIconTests(DefaultAppFixture fixture)
+    {
+        _session = fixture.Session;
+
+        // "Root" is a special name for windows the desktop session, that has access to task bar.
+        if (OperatingSystem.IsWindows())
+        {
+            _rootSession = fixture.CreateNestedSession("Root");
+        }
+
+        var tabs = _session.FindElementByAccessibilityId("MainTabs");
+        var tab = tabs.FindElementByName("Desktop");
+        tab.Click();
+    }
+
+    // Left click is only supported on Windows.
+    [PlatformFact(TestPlatforms.Windows)]
+    public void Should_Handle_Left_Click()
+    {
+        var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
+        Assert.NotNull(avaloinaTrayIconButton);
+
+        avaloinaTrayIconButton.SendClick();
+
+        Thread.Sleep(2000);
+        
+        var checkBox = _session.FindElementByAccessibilityId("TrayIconClicked");
+        Assert.True(checkBox.GetIsChecked());
+    }
+
+    [Fact]
+    public void Should_Handle_Context_Menu_Item_Click()
+    {
+        var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
+        Assert.NotNull(avaloinaTrayIconButton);
+
+        var contextMenu = ShowAndGetTrayMenu(avaloinaTrayIconButton, TrayIconName);
+        Assert.NotNull(contextMenu);
+
+        var menuItem = contextMenu.FindElementByName("Raise Menu Clicked");
+        menuItem.SendClick();
+
+        Thread.Sleep(2000);
+
+        var checkBox = _session.FindElementByAccessibilityId("TrayIconMenuClicked");
+        Assert.True(checkBox.GetIsChecked());
+    }
+
+    [Fact]
+    public void Can_Toggle_TrayIcon_Visibility()
+    {
+        var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
+        Assert.NotNull(avaloinaTrayIconButton);
+
+        var toggleButton = _session.FindElementByAccessibilityId("ToggleTrayIconVisible");
+        toggleButton.SendClick();
+
+        avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
+        Assert.Null(avaloinaTrayIconButton);
+
+        toggleButton.SendClick();
+
+        avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
+        Assert.NotNull(avaloinaTrayIconButton);
+    }
+
+    private static AppiumWebElement? GetTrayIconButton(AppiumDriver session, string trayIconName)
+    {
+        if (OperatingSystem.IsWindows())
+        {
+            var taskBar = session.FindElementsByClassName("Shell_TrayWnd")
+                .FirstOrDefault() ?? throw new InvalidOperationException("Couldn't find Taskbar on current system.");
+
+            if (TryToGetIcon(taskBar, trayIconName) is { } trayIcon)
+            {
+                return trayIcon;
+            }
+            else
+            {
+                // Add a sleep here, as previous test might still run popup closing animation.
+                Thread.Sleep(1000);
+
+                // win11: SystemTrayIcon
+                // win10: Notification Chevron
+                var trayIconsButton = taskBar.FindElementsByAccessibilityId("SystemTrayIcon").FirstOrDefault()
+                    ?? taskBar.FindElementsByName("Notification Chevron").FirstOrDefault()
+                    ?? throw new InvalidOperationException("SystemTrayIcon cannot be found.");
+                trayIconsButton.Click();
+
+                // win11: TopLevelWindowForOverflowXamlIsland
+                // win10: NotifyIconOverflowWindow
+                var trayIconsFlyout = session.FindElementsByClassName("TopLevelWindowForOverflowXamlIsland").FirstOrDefault()
+                      ?? session.FindElementsByClassName("NotifyIconOverflowWindow").FirstOrDefault()
+                      ?? throw new InvalidOperationException("System tray overflow window cannot be found.");
+                return TryToGetIcon(trayIconsFlyout, trayIconName);
+            }
+
+            static AppiumWebElement? TryToGetIcon(AppiumWebElement parent, string trayIconName) =>
+                parent.FindElementsByName(trayIconName).LastOrDefault()
+                // Some icons (including Avalonia) for some reason include leading whitespace in their name.
+                // Couldn't find any info on that, which is weird.
+                ?? parent.FindElementsByName(" " + trayIconName).LastOrDefault();
+        }
+        if (OperatingSystem.IsMacOS())
+        {
+            return session.FindElementsByXPath("//XCUIElementTypeStatusItem").FirstOrDefault();
+        }
+
+        throw new PlatformNotSupportedException();
+    }
+
+    private static AppiumWebElement ShowAndGetTrayMenu(AppiumWebElement trayIcon, string trayIconName)
+    {
+        if (OperatingSystem.IsWindows())
+        {
+            var session = (AppiumDriver)trayIcon.WrappedDriver;
+            new Actions(trayIcon.WrappedDriver).ContextClick(trayIcon).Perform();
+
+            Thread.Sleep(1000);
+
+            return session.FindElementByXPath($"//Window[@AutomationId='AvaloniaTrayPopupRoot_{trayIconName}']");
+        }
+        else
+        {
+            trayIcon.Click();
+            return trayIcon.FindElementByXPath("//XCUIElementTypeStatusItem/XCUIElementTypeMenu");
+        }
+    }
+
+    public void Dispose()
+    {
+        _rootSession?.Dispose();
+    }
+}