added tray menu
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Services;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
|
||||
namespace CursorLang.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The icon in the notification area and its menu — the whole interface of an
|
||||
/// application that works in the background.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The icon needs a window of its own, so the checks run on the interface thread.
|
||||
/// Whether the tray accepts the icon is up to Windows: there is no notification area
|
||||
/// in a session without a desktop, and such a check skips itself.
|
||||
/// </remarks>
|
||||
public sealed class TrayIconTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_icon_ends_up_in_the_notification_area()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
|
||||
if (!tray.Install())
|
||||
{
|
||||
Assert.Skip("Windows did not take the icon — there is no notification area here");
|
||||
}
|
||||
|
||||
// Asking twice changes nothing: a second icon of the same
|
||||
// application in the tray is not what anyone wants
|
||||
Assert.True(tray.Install());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_icon_is_taken_away_on_the_way_out()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var tray = new TrayIcon(new FakeLocalizationService());
|
||||
tray.Install();
|
||||
|
||||
tray.Dispose();
|
||||
|
||||
// A second disposal is what the container does after the application
|
||||
// has already shut the tray down itself
|
||||
tray.Dispose();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_menu_offers_the_settings_and_the_way_out()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
|
||||
List<MenuItem> items = [.. tray.Menu.Items.OfType<MenuItem>()];
|
||||
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal("en:TrayMenuSettings", items[0].Header);
|
||||
Assert.Equal("en:TrayMenuExit", items[1].Header);
|
||||
|
||||
// The way out is set apart from the rest: it is the one point of no return
|
||||
Assert.Single(tray.Menu.Items.OfType<Separator>());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_menu_follows_the_language_chosen_in_the_settings()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var localization = new FakeLocalizationService();
|
||||
using var tray = new TrayIcon(localization);
|
||||
|
||||
MenuItem settings = tray.Menu.Items.OfType<MenuItem>().First();
|
||||
Assert.Equal("en:TrayMenuSettings", settings.Header);
|
||||
|
||||
localization.CurrentLanguage = "ru";
|
||||
|
||||
Assert.Equal("ru:TrayMenuSettings", settings.Header);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_settings_point_of_the_menu_asks_for_the_window()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
int opened = 0;
|
||||
int exits = 0;
|
||||
|
||||
tray.OpenRequested += (_, _) => opened++;
|
||||
tray.ExitRequested += (_, _) => exits++;
|
||||
|
||||
Click(tray.Menu.Items.OfType<MenuItem>().First());
|
||||
|
||||
Assert.Equal(1, opened);
|
||||
Assert.Equal(0, exits);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_exit_point_of_the_menu_asks_for_the_way_out()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
int opened = 0;
|
||||
int exits = 0;
|
||||
|
||||
tray.OpenRequested += (_, _) => opened++;
|
||||
tray.ExitRequested += (_, _) => exits++;
|
||||
|
||||
Click(tray.Menu.Items.OfType<MenuItem>().Last());
|
||||
|
||||
Assert.Equal(1, exits);
|
||||
Assert.Equal(0, opened);
|
||||
});
|
||||
}
|
||||
|
||||
// The menu belongs to no window and hangs on no element tree: WPF has to
|
||||
// raise it all the same
|
||||
[Fact]
|
||||
public void The_menu_shows_up_on_request()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
tray.Install();
|
||||
|
||||
try
|
||||
{
|
||||
tray.ShowMenu();
|
||||
|
||||
Assert.True(tray.Menu.IsOpen);
|
||||
Assert.Equal(PlacementMode.MousePoint, tray.Menu.Placement);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// An open menu holds the mouse: the checks that follow would
|
||||
// never see a click of their own
|
||||
tray.Menu.IsOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The line between the points of the menu is drawn by the theme of the app.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// WPF gives a separator inside a menu a style of its own, found by a key rather
|
||||
/// than by the type: a style by type never reaches it. Left with the system one,
|
||||
/// the line comes out indented from the left, where the icons of a system menu
|
||||
/// would go, and stretched past the right edge of the menu.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void The_line_between_the_points_of_the_menu_keeps_to_the_theme()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var themed = Application.Current.TryFindResource(MenuItem.SeparatorStyleKey) as Style;
|
||||
|
||||
Assert.NotNull(themed);
|
||||
Assert.Equal(typeof(Separator), themed.TargetType);
|
||||
|
||||
using var tray = new TrayIcon(new FakeLocalizationService());
|
||||
|
||||
try
|
||||
{
|
||||
tray.ShowMenu();
|
||||
|
||||
Separator line = tray.Menu.Items.OfType<Separator>().Single();
|
||||
|
||||
// The style has reached the line: it is the theme drawing it,
|
||||
// margins and all
|
||||
Assert.Same(themed, line.Style);
|
||||
Assert.True(line.ActualWidth > 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tray.Menu.IsOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The image comes from the executable itself; the icon Windows keeps for an
|
||||
// application without one is the last resort. An empty spot in the tray is not
|
||||
// an option either way
|
||||
[Fact]
|
||||
public void There_is_an_icon_to_show()
|
||||
{
|
||||
IntPtr icon = TrayIconNative.LoadApplicationIcon();
|
||||
|
||||
Assert.NotEqual(IntPtr.Zero, icon);
|
||||
|
||||
TrayIconNative.ReleaseIcon(icon);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Explorer_restarting_is_a_message_of_its_own()
|
||||
{
|
||||
Assert.NotEqual(0, TrayIconNative.TaskbarCreatedMessage);
|
||||
}
|
||||
|
||||
private static void Click(MenuItem item) => item.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
|
||||
}
|
||||
Reference in New Issue
Block a user