using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Media; using CursorLang.Models; using CursorLang.Services; using CursorLang.Tests.Infrastructure; using CursorLang.ViewModels; using CursorLang.Views; namespace CursorLang.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 { 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)); }); } [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 Color chosen = viewModel.BackgroundPalette[2]; settings.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); }); }); } [Fact] public void The_updates_are_checked_by_the_button_in_the_window() { Sta.Run(() => { var updates = new FakeUpdateService(); using UpdateViewModel section = CreateUpdates(updates); using SettingsViewModel viewModel = CreateViewModel(updates: section); Open(viewModel, window => { Button check = Assert.Single(FindButtonsBoundTo(window, "Updates.CheckCommand")); Assert.True(check.IsVisible); check.Command.Execute(null); Assert.Equal(1, updates.CheckCalls); }); }); } // An app installed from the Store is updated by the Store [Fact] public void An_app_that_updates_itself_elsewhere_shows_no_updates_section() { Sta.Run(() => { using UpdateViewModel section = CreateUpdates(new FakeUpdateService { IsSupported = false }); using SettingsViewModel viewModel = CreateViewModel(updates: section); Open(viewModel, window => Assert.All(FindButtonsBoundTo(window, "Updates.CheckCommand"), button => Assert.False(button.IsVisible))); }); } // 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 [Fact] public void The_window_hooks_up_to_the_tray() { Sta.Run(() => { using SettingsViewModel viewModel = CreateViewModel(); Open(viewModel, window => { window.Close(); Assert.False(window.IsVisible); // A closed window would refuse this window.Show(); Assert.True(window.IsVisible); }); }); } [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, UpdateViewModel? updates = null) => new(settings ?? new AppSettings(), localization ?? new FakeLocalizationService(), startup ?? new FakeStartupService(), updates ?? Fake.Updates()); 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 presenter = new MainWindowPresenter(placement); var window = new MainWindow(viewModel, theme, placement, presenter) { // The window is needed alive, but not in sight Opacity = 0, ShowInTaskbar = false, ShowActivated = false, }; try { window.Show(); window.UpdateLayout(); check(window); } finally { // The window belongs to the tray now and refuses to close until // the application is on its way out presenter.AllowClose(); window.Close(); } } private static UpdateViewModel CreateUpdates(IUpdateService updates) => new(updates, new FakeLocalizationService(), new AppSettings(), new UpdateOptions()); private static IEnumerable