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; /// /// The icon in the notification area and its menu — the whole interface of an /// application that works in the background. /// /// /// 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. /// 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 items = [.. tray.Menu.Items.OfType()]; 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()); }); } [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().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().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().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; } }); } /// /// The line between the points of the menu is drawn by the theme of the app. /// /// /// 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. /// [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().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)); }