Skip to content

Commit

Permalink
TrayIcon integration tests (#16154)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maxkatz6 authored Jul 8, 2024
1 parent cc082f9 commit ad0bc0d
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 20 deletions.
9 changes: 7 additions & 2 deletions samples/IntegrationTestApp/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
12 changes: 7 additions & 5 deletions samples/IntegrationTestApp/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
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;
}
Expand All @@ -29,12 +31,12 @@ public override void OnFrameworkInitializationCompleted()
{
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 changes: 8 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
8 changes: 8 additions & 0 deletions samples/IntegrationTestApp/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ private void OnShowTopmostWindow()
ownedWindow.Show(mainWindow);
}

private void OnToggleTrayIconVisible()
{
var icon = TrayIcon.GetIcons(Application.Current!)!.FirstOrDefault()!;
icon.IsVisible = !icon.IsVisible;
}

private void InitializeGesturesTab()
{
var gestureBorder = GestureBorder;
Expand Down Expand Up @@ -295,6 +301,8 @@ private void OnButtonClick(object? sender, RoutedEventArgs e)
OnApplyWindowDecorations(this);
if (source?.Name == nameof(ShowNewWindowDecorations))
OnShowNewWindowDecorations();
if (source?.Name == nameof(ToggleTrayIconVisible))
OnToggleTrayIconVisible();
}

private void OnApplyWindowDecorations(Window window)
Expand Down
3 changes: 2 additions & 1 deletion src/Windows/Avalonia.Win32/TrayIconImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ private void OnRightClicked()
return;
}

var _trayMenu = new TrayPopupRoot()
var _trayMenu = new TrayPopupRoot
{
Name = "AvaloniaTrayPopupRoot_" + _tooltipText,
SystemDecorations = SystemDecorations.None,
SizeToContent = SizeToContent.WidthAndHeight,
Background = null,
Expand Down
35 changes: 27 additions & 8 deletions tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public DefaultAppFixture()
{
var options = new AppiumOptions();

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
ConfigureWin32Options(options);
Session = new WindowsDriver(
Expand All @@ -28,7 +28,7 @@ public DefaultAppFixture()
Session.WindowHandles[0].Substring(2),
NumberStyles.AllowHexSpecifier)));
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
else if (OperatingSystem.IsMacOS())
{
ConfigureMacOptions(options);
Session = new MacDriver(
Expand All @@ -37,21 +37,20 @@ public DefaultAppFixture()
}
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);
Expand All @@ -71,6 +70,26 @@ public void Dispose()
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
}
}
Expand Down
152 changes: 152 additions & 0 deletions tests/Avalonia.IntegrationTests.Appium/TrayIconTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit ad0bc0d

Please sign in to comment.