Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TrayIcon integration tests #16154

Merged
merged 12 commits into from
Jul 8, 2024
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 @@ -222,6 +222,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 @@ -294,6 +300,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();
}
}
Loading