Skip to content

Commit ad0bc0d

Browse files
authored
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
1 parent cc082f9 commit ad0bc0d

File tree

8 files changed

+215
-20
lines changed

8 files changed

+215
-20
lines changed

samples/IntegrationTestApp/App.axaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
</Application.Styles>
1010
<TrayIcon.Icons>
1111
<TrayIcons>
12-
<TrayIcon Icon="/Assets/icon.ico">
12+
<TrayIcon Icon="/Assets/icon.ico"
13+
ToolTipText="IntegrationTestApp TrayIcon"
14+
Command="{Binding TrayIconCommand}"
15+
CommandParameter="TrayIconClicked">
1316
<TrayIcon.Menu>
1417
<NativeMenu>
15-
<NativeMenuItem Header="Show _Test Window" Command="{Binding ShowWindowCommand}" />
18+
<NativeMenuItem Header="Raise Menu Clicked"
19+
Command="{Binding TrayIconCommand}"
20+
CommandParameter="TrayIconMenuClicked" />
1621
</NativeMenu>
1722
</TrayIcon.Menu>
1823
</TrayIcon>

samples/IntegrationTestApp/App.axaml.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
using Avalonia.Controls;
55
using Avalonia.Controls.ApplicationLifetimes;
66
using Avalonia.Markup.Xaml;
7+
using Avalonia.Media;
78
using MiniMvvm;
89

910
namespace IntegrationTestApp
1011
{
1112
public class App : Application
1213
{
14+
private MainWindow? _mainWindow;
15+
1316
public App()
1417
{
15-
ShowWindowCommand = MiniCommand.Create(() =>
18+
TrayIconCommand = MiniCommand.Create<string>(name =>
1619
{
17-
var window = new Window() { Title = "TrayIcon demo window" };
18-
window.Show();
20+
_mainWindow!.Get<CheckBox>(name).IsChecked = true;
1921
});
2022
DataContext = this;
2123
}
@@ -29,12 +31,12 @@ public override void OnFrameworkInitializationCompleted()
2931
{
3032
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
3133
{
32-
desktop.MainWindow = new MainWindow();
34+
desktop.MainWindow = _mainWindow = new MainWindow();
3335
}
3436

3537
base.OnFrameworkInitializationCompleted();
3638
}
3739

38-
public ICommand ShowWindowCommand { get; }
40+
public ICommand TrayIconCommand { get; }
3941
}
4042
}

samples/IntegrationTestApp/MainWindow.axaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@
9393
</StackPanel>
9494
</TabItem>
9595

96+
<TabItem Header="Desktop">
97+
<StackPanel>
98+
<CheckBox x:FieldModifier="public" Name="TrayIconClicked">Tray Icon Clicked</CheckBox>
99+
<CheckBox x:FieldModifier="public" Name="TrayIconMenuClicked">Tray Icon Menu Clicked</CheckBox>
100+
<Button Name="ToggleTrayIconVisible" Content="Toggle TrayIcon Visible" />
101+
</StackPanel>
102+
</TabItem>
103+
96104
<TabItem Header="Gestures">
97105
<DockPanel>
98106
<DockPanel DockPanel.Dock="Top">

samples/IntegrationTestApp/MainWindow.axaml.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ private void OnShowTopmostWindow()
223223
ownedWindow.Show(mainWindow);
224224
}
225225

226+
private void OnToggleTrayIconVisible()
227+
{
228+
var icon = TrayIcon.GetIcons(Application.Current!)!.FirstOrDefault()!;
229+
icon.IsVisible = !icon.IsVisible;
230+
}
231+
226232
private void InitializeGesturesTab()
227233
{
228234
var gestureBorder = GestureBorder;
@@ -295,6 +301,8 @@ private void OnButtonClick(object? sender, RoutedEventArgs e)
295301
OnApplyWindowDecorations(this);
296302
if (source?.Name == nameof(ShowNewWindowDecorations))
297303
OnShowNewWindowDecorations();
304+
if (source?.Name == nameof(ToggleTrayIconVisible))
305+
OnToggleTrayIconVisible();
298306
}
299307

300308
private void OnApplyWindowDecorations(Window window)

src/Windows/Avalonia.Win32/TrayIconImpl.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@ private void OnRightClicked()
218218
return;
219219
}
220220

221-
var _trayMenu = new TrayPopupRoot()
221+
var _trayMenu = new TrayPopupRoot
222222
{
223+
Name = "AvaloniaTrayPopupRoot_" + _tooltipText,
223224
SystemDecorations = SystemDecorations.None,
224225
SizeToContent = SizeToContent.WidthAndHeight,
225226
Background = null,

tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public DefaultAppFixture()
1616
{
1717
var options = new AppiumOptions();
1818

19-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
19+
if (OperatingSystem.IsWindows())
2020
{
2121
ConfigureWin32Options(options);
2222
Session = new WindowsDriver(
@@ -28,7 +28,7 @@ public DefaultAppFixture()
2828
Session.WindowHandles[0].Substring(2),
2929
NumberStyles.AllowHexSpecifier)));
3030
}
31-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
31+
else if (OperatingSystem.IsMacOS())
3232
{
3333
ConfigureMacOptions(options);
3434
Session = new MacDriver(
@@ -37,21 +37,20 @@ public DefaultAppFixture()
3737
}
3838
else
3939
{
40-
throw new NotSupportedException("Unsupported platform.");
40+
throw new PlatformNotSupportedException();
4141
}
4242
}
4343

44-
protected virtual void ConfigureWin32Options(AppiumOptions options)
44+
protected virtual void ConfigureWin32Options(AppiumOptions options, string? app = null)
4545
{
46-
var path = Path.GetFullPath(TestAppPath);
47-
options.AddAdditionalCapability(MobileCapabilityType.App, path);
46+
options.AddAdditionalCapability(MobileCapabilityType.App, app ?? Path.GetFullPath(TestAppPath));
4847
options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows);
4948
options.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC");
5049
}
5150

52-
protected virtual void ConfigureMacOptions(AppiumOptions options)
51+
protected virtual void ConfigureMacOptions(AppiumOptions options, string? app = null)
5352
{
54-
options.AddAdditionalCapability("appium:bundleId", TestAppBundleId);
53+
options.AddAdditionalCapability("appium:bundleId", app ?? TestAppBundleId);
5554
options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS);
5655
options.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2");
5756
options.AddAdditionalCapability("appium:showServerLogs", true);
@@ -71,6 +70,26 @@ public void Dispose()
7170
}
7271
}
7372

73+
public AppiumDriver CreateNestedSession(string appName)
74+
{
75+
var options = new AppiumOptions();
76+
if (OperatingSystem.IsWindows())
77+
{
78+
ConfigureWin32Options(options, appName);
79+
80+
return new WindowsDriver(new Uri("http://127.0.0.1:4723"), options);
81+
}
82+
else if (OperatingSystem.IsMacOS())
83+
{
84+
ConfigureMacOptions(options, appName);
85+
return new MacDriver(new Uri("http://127.0.0.1:4723/wd/hub"), options);
86+
}
87+
else
88+
{
89+
throw new PlatformNotSupportedException();
90+
}
91+
}
92+
7493
[DllImport("user32.dll")]
7594
[return: MarshalAs(UnmanagedType.Bool)]
7695
private static extern bool SetForegroundWindow(IntPtr hWnd);

tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ namespace Avalonia.IntegrationTests.Appium
44
{
55
public class OverlayPopupsAppFixture : DefaultAppFixture
66
{
7-
protected override void ConfigureWin32Options(AppiumOptions options)
7+
protected override void ConfigureWin32Options(AppiumOptions options, string? app = null)
88
{
9-
base.ConfigureWin32Options(options);
9+
base.ConfigureWin32Options(options, app);
1010
options.AddAdditionalCapability("appArguments", "--overlayPopups");
1111
}
1212

13-
protected override void ConfigureMacOptions(AppiumOptions options)
13+
protected override void ConfigureMacOptions(AppiumOptions options, string? app = null)
1414
{
15-
base.ConfigureMacOptions(options);
15+
base.ConfigureMacOptions(options, app);
1616
options.AddAdditionalCapability("appium:arguments", new[] { "--overlayPopups" });
1717
}
1818
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Threading;
5+
using OpenQA.Selenium;
6+
using OpenQA.Selenium.Appium;
7+
using OpenQA.Selenium.Interactions;
8+
using Xunit;
9+
10+
namespace Avalonia.IntegrationTests.Appium;
11+
12+
[Collection("Default")]
13+
public class TrayIconTests : IDisposable
14+
{
15+
private readonly AppiumDriver _session;
16+
private readonly AppiumDriver? _rootSession;
17+
private const string TrayIconName = "IntegrationTestApp TrayIcon";
18+
19+
public TrayIconTests(DefaultAppFixture fixture)
20+
{
21+
_session = fixture.Session;
22+
23+
// "Root" is a special name for windows the desktop session, that has access to task bar.
24+
if (OperatingSystem.IsWindows())
25+
{
26+
_rootSession = fixture.CreateNestedSession("Root");
27+
}
28+
29+
var tabs = _session.FindElementByAccessibilityId("MainTabs");
30+
var tab = tabs.FindElementByName("Desktop");
31+
tab.Click();
32+
}
33+
34+
// Left click is only supported on Windows.
35+
[PlatformFact(TestPlatforms.Windows)]
36+
public void Should_Handle_Left_Click()
37+
{
38+
var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
39+
Assert.NotNull(avaloinaTrayIconButton);
40+
41+
avaloinaTrayIconButton.SendClick();
42+
43+
Thread.Sleep(2000);
44+
45+
var checkBox = _session.FindElementByAccessibilityId("TrayIconClicked");
46+
Assert.True(checkBox.GetIsChecked());
47+
}
48+
49+
[Fact]
50+
public void Should_Handle_Context_Menu_Item_Click()
51+
{
52+
var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
53+
Assert.NotNull(avaloinaTrayIconButton);
54+
55+
var contextMenu = ShowAndGetTrayMenu(avaloinaTrayIconButton, TrayIconName);
56+
Assert.NotNull(contextMenu);
57+
58+
var menuItem = contextMenu.FindElementByName("Raise Menu Clicked");
59+
menuItem.SendClick();
60+
61+
Thread.Sleep(2000);
62+
63+
var checkBox = _session.FindElementByAccessibilityId("TrayIconMenuClicked");
64+
Assert.True(checkBox.GetIsChecked());
65+
}
66+
67+
[Fact]
68+
public void Can_Toggle_TrayIcon_Visibility()
69+
{
70+
var avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
71+
Assert.NotNull(avaloinaTrayIconButton);
72+
73+
var toggleButton = _session.FindElementByAccessibilityId("ToggleTrayIconVisible");
74+
toggleButton.SendClick();
75+
76+
avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
77+
Assert.Null(avaloinaTrayIconButton);
78+
79+
toggleButton.SendClick();
80+
81+
avaloinaTrayIconButton = GetTrayIconButton(_rootSession ?? _session, TrayIconName);
82+
Assert.NotNull(avaloinaTrayIconButton);
83+
}
84+
85+
private static AppiumWebElement? GetTrayIconButton(AppiumDriver session, string trayIconName)
86+
{
87+
if (OperatingSystem.IsWindows())
88+
{
89+
var taskBar = session.FindElementsByClassName("Shell_TrayWnd")
90+
.FirstOrDefault() ?? throw new InvalidOperationException("Couldn't find Taskbar on current system.");
91+
92+
if (TryToGetIcon(taskBar, trayIconName) is { } trayIcon)
93+
{
94+
return trayIcon;
95+
}
96+
else
97+
{
98+
// Add a sleep here, as previous test might still run popup closing animation.
99+
Thread.Sleep(1000);
100+
101+
// win11: SystemTrayIcon
102+
// win10: Notification Chevron
103+
var trayIconsButton = taskBar.FindElementsByAccessibilityId("SystemTrayIcon").FirstOrDefault()
104+
?? taskBar.FindElementsByName("Notification Chevron").FirstOrDefault()
105+
?? throw new InvalidOperationException("SystemTrayIcon cannot be found.");
106+
trayIconsButton.Click();
107+
108+
// win11: TopLevelWindowForOverflowXamlIsland
109+
// win10: NotifyIconOverflowWindow
110+
var trayIconsFlyout = session.FindElementsByClassName("TopLevelWindowForOverflowXamlIsland").FirstOrDefault()
111+
?? session.FindElementsByClassName("NotifyIconOverflowWindow").FirstOrDefault()
112+
?? throw new InvalidOperationException("System tray overflow window cannot be found.");
113+
return TryToGetIcon(trayIconsFlyout, trayIconName);
114+
}
115+
116+
static AppiumWebElement? TryToGetIcon(AppiumWebElement parent, string trayIconName) =>
117+
parent.FindElementsByName(trayIconName).LastOrDefault()
118+
// Some icons (including Avalonia) for some reason include leading whitespace in their name.
119+
// Couldn't find any info on that, which is weird.
120+
?? parent.FindElementsByName(" " + trayIconName).LastOrDefault();
121+
}
122+
if (OperatingSystem.IsMacOS())
123+
{
124+
return session.FindElementsByXPath("//XCUIElementTypeStatusItem").FirstOrDefault();
125+
}
126+
127+
throw new PlatformNotSupportedException();
128+
}
129+
130+
private static AppiumWebElement ShowAndGetTrayMenu(AppiumWebElement trayIcon, string trayIconName)
131+
{
132+
if (OperatingSystem.IsWindows())
133+
{
134+
var session = (AppiumDriver)trayIcon.WrappedDriver;
135+
new Actions(trayIcon.WrappedDriver).ContextClick(trayIcon).Perform();
136+
137+
Thread.Sleep(1000);
138+
139+
return session.FindElementByXPath($"//Window[@AutomationId='AvaloniaTrayPopupRoot_{trayIconName}']");
140+
}
141+
else
142+
{
143+
trayIcon.Click();
144+
return trayIcon.FindElementByXPath("//XCUIElementTypeStatusItem/XCUIElementTypeMenu");
145+
}
146+
}
147+
148+
public void Dispose()
149+
{
150+
_rootSession?.Dispose();
151+
}
152+
}

0 commit comments

Comments
 (0)