using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Media; using CursorLang.Core.Models; using CursorLang.Core.Services; using CursorLang.Settings.Services; using CursorLang.Settings.Tests.Infrastructure; using CursorLang.Settings.ViewModels; using CursorLang.Settings.Views; using CursorLang.Tests.Shared; namespace CursorLang.Settings.Tests.Views; /// /// The settings window as a whole: the markup, the bindings and the hook-up /// to the theme. /// /// /// The window has to be shown for real: before the show WPF builds no element /// tree and computes no bindings. Full transparency keeps it out of sight. /// public sealed class MainWindowTests { [Fact] public void The_window_is_built_and_takes_its_data_from_the_view_model() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); var theme = new FakeThemeService(); Open(viewModel, theme, window => { Assert.Same(viewModel, window.DataContext); Assert.Equal([window], theme.Registered); }); }); } [Fact] public void The_window_title_comes_from_the_interface_strings() { Sta.Run(() => { var localization = new FakeLocalizationService(); using SettingsViewModel viewModel = CreateViewModel(localization: localization); Open(viewModel, window => { Assert.Equal("en:SettingsTitle", window.Title); // A language change goes over every binding to a string localization.CurrentLanguage = "ru"; Assert.Equal("ru:SettingsTitle", window.Title); }); }); } [Fact] public void The_window_fits_its_height_to_its_content() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { Assert.Equal(SizeToContent.Height, window.SizeToContent); Assert.Equal(ResizeMode.CanMinimize, window.ResizeMode); Assert.True(window.ActualHeight > 0); Assert.True(window.ActualWidth > 0); }); }); } // The markup asks the view model for lists and palettes: if a name drifts // apart from the model, the binding silently shows an empty list [Fact] public void The_lists_in_the_window_are_filled() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { List boxes = [.. FindAll(window)]; Assert.NotEmpty(boxes); Assert.All(boxes, box => Assert.NotEmpty(box.Items)); }); }); } [Fact] public void The_lists_show_captions_in_the_chosen_language() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { // Languages are named in themselves, while the enum options are // named by strings from the resources: the latter are checked List displays = [ .. FindAll(window) .SelectMany(box => box.Items.OfType()) .Where(item => item.GetType().Name.StartsWith("EnumOption", StringComparison.Ordinal)) .Select(item => item.ToString() ?? string.Empty), ]; Assert.NotEmpty(displays); Assert.All(displays, display => Assert.StartsWith("en:", display, StringComparison.Ordinal)); }); }); } [Fact] public void The_window_shows_a_preview_of_the_tooltip() { Sta.Run(() => { var settings = new AppSettings(); settings.Current.FontSize = 33; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => // The font size from the settings is visible right in the window Assert.Contains(FindAll(window), text => Math.Abs(text.FontSize - 33) < 0.001)); }); } /// /// The preview follows the mode: switching it shows the settings of the new one. /// /// /// The look is bound through the current mode, so a switch has to reach the window. /// Bound to a mode by name, the sliders would keep showing the cursor mode whatever /// was chosen above them. /// [Fact] public void A_switch_of_the_mode_shows_the_settings_of_that_mode() { Sta.Run(() => { var settings = new AppSettings(); settings.AtCursor.FontSize = 24; settings.FixedPoint.FontSize = 56; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => { Assert.Contains(FindAll(window), text => Math.Abs(text.FontSize - 24) < 0.001); settings.PlacementMode = PopupPlacementMode.FixedPoint; window.UpdateLayout(); Assert.Contains(FindAll(window), text => Math.Abs(text.FontSize - 56) < 0.001); }); }); } /// /// In the middle of the monitor the offset from the edge cannot be set, but it stays /// in place. /// /// /// There is no edge to stand off from there, so the setting goes grey. Taking the row /// out of the section instead would move everything below it on every switch of the /// place — the section is not to jump about. /// [Fact] public void In_the_middle_of_the_monitor_the_offset_from_the_edge_goes_grey() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.FixedPoint }; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => { Slider offset = Assert.Single(FindSlidersBoundTo(window, "Settings.FixedPoint.Offset")); Assert.True(offset.IsVisible); Assert.False(offset.IsEnabled); // And it looks the part: the template draws from brushes that know // nothing of the state, so the style has to fade it Assert.True(offset.Opacity < 1); settings.FixedPoint.Position = ScreenPosition.TopRight; window.UpdateLayout(); Assert.True(offset.IsVisible); Assert.True(offset.IsEnabled); Assert.Equal(1, offset.Opacity); settings.FixedPoint.Position = ScreenPosition.Center; window.UpdateLayout(); Assert.True(offset.IsVisible); Assert.False(offset.IsEnabled); Assert.True(offset.Opacity < 1); }); }); } // Another mode has no fixed point at all, and its offset has no place on screen [Fact] public void Away_from_the_fixed_point_its_offset_is_not_shown() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCursor }; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => Assert.All(FindSlidersBoundTo(window, "Settings.FixedPoint.Offset"), slider => Assert.False(slider.IsVisible))); }); } /// /// What the look belongs to is explained in a tooltip next to the mode. /// /// /// The sliders of the look show other numbers after a switch of the mode, and that /// has to be explained. A line of text saying so would sit in the section forever, /// while the question is asked once. /// [Fact] public void The_window_explains_that_the_look_belongs_to_the_mode() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { // The text of a tooltip is bound but not computed until the tooltip is // shown, so what it is bound to is what gets checked List hints = [ .. FindAll(window) .Where(text => text.ToolTip is ToolTip tip && tip.Content is TextBlock content && PathOf(content, TextBlock.TextProperty) == "Localization[PopupLookPerModeHint]"), ]; TextBlock link = Assert.Single(hints); Assert.Equal("en:MoreInfoLink", link.Text); Assert.True(link.IsVisible); }); }); } // Beside the caret and nowhere else: the list offers the two sides it has [Fact] public void The_caret_is_offered_two_sides_in_the_window() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCaret }; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => { ComboBox sides = Assert.Single( FindAll(window), box => BindingOperations.GetBinding(box, Selector.SelectedValueProperty)?.Path.Path == "Settings.AtCaret.Side"); Assert.Equal(2, sides.Items.Count); sides.SelectedValue = CaretSide.Left; Assert.Equal(CaretSide.Left, settings.AtCaret.Side); }); }); } // The sliders of the look write into the mode chosen above them [Fact] public void The_look_is_edited_in_the_mode_that_is_chosen() { Sta.Run(() => { var settings = new AppSettings { PlacementMode = PopupPlacementMode.AtCaret }; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => { Slider slider = Assert.Single(FindSlidersBoundTo(window, "Settings.Current.FontSize")); slider.Value = 44; Assert.Equal(44, settings.AtCaret.FontSize); Assert.Equal(20, settings.AtCursor.FontSize); }); }); } [Fact] public void The_tooltip_colours_are_shown_as_swatches() { Sta.Run(() => { var settings = new AppSettings(); using SettingsViewModel viewModel = CreateViewModel(settings); // The chosen colour is shown as a swatch with a caption — in the // same notation the settings file uses System.Drawing.Color chosen = viewModel.BackgroundPalette[2]; settings.Current.BackgroundColor = chosen; string expected = $"#{chosen.R:X2}{chosen.G:X2}{chosen.B:X2}"; Open(viewModel, window => Assert.Contains( FindAll(window), text => text.Text.Equals(expected, StringComparison.OrdinalIgnoreCase))); }); } [Fact] public void The_startup_setting_hides_until_Windows_answers() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { Assert.False(viewModel.IsStartupAvailable); Assert.All(FindStartupCheckBoxes(window), box => Assert.False(box.IsVisible)); }); }); } [Fact] public void An_allowed_startup_shows_up_in_the_window() { Sta.Run(() => { var startup = new FakeStartupService { State = StartupState.Disabled }; using SettingsViewModel viewModel = CreateViewModel(startup: startup); Open(viewModel, window => { viewModel.InitializeAsync().GetAwaiter().GetResult(); window.UpdateLayout(); CheckBox box = Assert.Single(FindStartupCheckBoxes(window)); Assert.True(box.IsVisible); Assert.True(box.IsEnabled); Assert.False(box.IsChecked); }); }); } // A ban by the user is not for the app to argue with: the tick is shown, // but it cannot be moved [Fact] public void A_startup_banned_by_Windows_is_shown_as_unavailable() { Sta.Run(() => { var startup = new FakeStartupService { State = StartupState.DisabledByUser }; using SettingsViewModel viewModel = CreateViewModel(startup: startup); Open(viewModel, window => { viewModel.InitializeAsync().GetAwaiter().GetResult(); window.UpdateLayout(); CheckBox box = Assert.Single(FindStartupCheckBoxes(window)); Assert.True(box.IsVisible); Assert.False(box.IsEnabled); }); }); } [Fact] public void The_Caps_Lock_interception_is_toggled_by_a_tick() { Sta.Run(() => { var settings = new AppSettings { UseCapsLockHotkey = false }; using SettingsViewModel viewModel = CreateViewModel(settings); Open(viewModel, window => { CheckBox box = Assert.Single(FindCheckBoxesBoundTo(window, "Settings.UseCapsLockHotkey")); box.IsChecked = true; Assert.True(settings.UseCapsLockHotkey); }); }); } // The version is nowhere else in the window, so the title has to carry it [Fact] public void The_title_of_the_window_shows_the_version() { Sta.Run(() => { var localization = new FakeLocalizationService(); localization.Strings["SettingsTitle"] = "CursorLang {0} — Settings"; using SettingsViewModel viewModel = CreateViewModel( localization: localization, version: new Version(1, 2, 3, 0)); Open(viewModel, window => Assert.Equal("CursorLang 1.2.3 — Settings", window.Title)); }); } // The application lives in the tray, and the settings window is a guest on the // screen: its close button puts it away rather than ends anything /// /// Closing the window closes it. The tray it used to hide into belongs to another /// process now, and there is nothing shared left to keep alive. /// [Fact] public void Closing_the_window_really_closes_it() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { window.Close(); Assert.False(window.IsVisible); Assert.Throws(window.Show); }); }); } [Fact] public void The_window_hooks_up_to_the_placement() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); var placement = new MainWindowPlacement(); Open(viewModel, new FakeThemeService(), placement, window => { // The placement works off the window creation event: the window // has to end up on a monitor rather than beyond its edges Assert.True(window.Left > -10_000); Assert.True(window.Top > -10_000); }); }); } private static SettingsViewModel CreateViewModel( AppSettings? settings = null, ILocalizationService? localization = null, IStartupService? startup = null, Version? version = null) => new(settings ?? new AppSettings(), localization ?? new FakeLocalizationService(), startup ?? new FakeStartupService(), version ?? new Version(1, 0, 0, 0)); private static void Open(SettingsViewModel viewModel, Action check) => Open(viewModel, new FakeThemeService(), new MainWindowPlacement(), check); private static void Open(SettingsViewModel viewModel, IThemeService theme, Action check) => Open(viewModel, theme, new MainWindowPlacement(), check); private static void Open( SettingsViewModel viewModel, IThemeService theme, MainWindowPlacement placement, Action check) { var window = new MainWindow(viewModel, theme, placement) { // The window is needed alive, but not in sight Opacity = 0, ShowInTaskbar = false, ShowActivated = false, }; try { window.Show(); window.UpdateLayout(); check(window); } finally { window.Close(); } } private static IEnumerable