added tests to project
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using CursorLang.Models;
|
||||
using CursorLang.Views;
|
||||
|
||||
namespace CursorLang.Tests.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The binding converters: they decide what the settings window shows and what
|
||||
/// it keeps out of sight.
|
||||
/// </summary>
|
||||
public sealed class ConvertersTests
|
||||
{
|
||||
private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
|
||||
|
||||
[Fact]
|
||||
public void A_match_with_the_single_listed_value_shows_the_element()
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(
|
||||
Visibility.Visible,
|
||||
converter.Convert(PopupPlacementMode.AtCursor, typeof(Visibility), "AtCursor", Culture));
|
||||
}
|
||||
|
||||
// The anchor settings suit two placement modes at once
|
||||
[Fact]
|
||||
public void A_match_with_one_of_the_listed_values_shows_the_element()
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(
|
||||
Visibility.Visible,
|
||||
converter.Convert(PopupPlacementMode.AtCaret, typeof(Visibility), "AtCursor, AtCaret", Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_match_hides_the_element()
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(
|
||||
Visibility.Collapsed,
|
||||
converter.Convert(PopupPlacementMode.FixedPoint, typeof(Visibility), "AtCursor, AtCaret", Culture));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "AtCursor")]
|
||||
[InlineData(PopupPlacementMode.AtCursor, null)]
|
||||
[InlineData(PopupPlacementMode.AtCursor, "")]
|
||||
[InlineData(PopupPlacementMode.AtCursor, ",,")]
|
||||
[InlineData(null, null)]
|
||||
public void Without_a_value_or_without_a_list_the_element_is_hidden(object? value, string? parameter)
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(Visibility.Collapsed, converter.Convert(value, typeof(Visibility), parameter, Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extra_spaces_in_the_list_do_not_get_in_the_way()
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(
|
||||
Visibility.Visible,
|
||||
converter.Convert(ScreenPosition.Center, typeof(Visibility), " Top , Center ", Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Visibility_does_not_convert_back()
|
||||
{
|
||||
var converter = new EnumToVisibilityConverter();
|
||||
|
||||
Assert.Equal(
|
||||
Binding.DoNothing,
|
||||
converter.ConvertBack(Visibility.Visible, typeof(PopupPlacementMode), "AtCursor", Culture));
|
||||
}
|
||||
|
||||
// The alpha is not shown: transparency is a setting of its own
|
||||
[Fact]
|
||||
public void A_colour_is_shown_with_six_digits()
|
||||
{
|
||||
var converter = new ColorToHexConverter();
|
||||
|
||||
Assert.Equal("#0A1B2C", converter.Convert(Color.FromRgb(0x0A, 0x1B, 0x2C), typeof(string), null, Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_semi_transparent_colour_is_shown_without_its_alpha()
|
||||
{
|
||||
var converter = new ColorToHexConverter();
|
||||
|
||||
Assert.Equal(
|
||||
"#102030",
|
||||
converter.Convert(Color.FromArgb(0x80, 0x10, 0x20, 0x30), typeof(string), null, Culture));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("not a colour")]
|
||||
[InlineData(42)]
|
||||
public void Anything_that_is_not_a_colour_shows_as_an_empty_string(object? value)
|
||||
{
|
||||
var converter = new ColorToHexConverter();
|
||||
|
||||
Assert.Equal(string.Empty, converter.Convert(value, typeof(string), null, Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_colour_notation_does_not_convert_back()
|
||||
{
|
||||
var converter = new ColorToHexConverter();
|
||||
|
||||
Assert.Equal(Binding.DoNothing, converter.ConvertBack("#102030", typeof(Color), null, Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_colour_turns_into_a_brush()
|
||||
{
|
||||
var converter = new ColorToBrushConverter();
|
||||
Color color = Color.FromRgb(0x10, 0x20, 0x30);
|
||||
|
||||
var brush = Assert.IsType<SolidColorBrush>(converter.Convert(color, typeof(Brush), null, Culture));
|
||||
|
||||
Assert.Equal(color, brush.Color);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("not a colour")]
|
||||
public void Anything_that_is_not_a_colour_turns_into_a_transparent_brush(object? value)
|
||||
{
|
||||
var converter = new ColorToBrushConverter();
|
||||
|
||||
Assert.Same(Brushes.Transparent, converter.Convert(value, typeof(Brush), null, Culture));
|
||||
}
|
||||
|
||||
// Picking a swatch in the list sends the colour back into the settings
|
||||
[Fact]
|
||||
public void A_brush_converts_back_into_a_colour()
|
||||
{
|
||||
var converter = new ColorToBrushConverter();
|
||||
Color color = Color.FromRgb(0x10, 0x20, 0x30);
|
||||
|
||||
Assert.Equal(color, converter.ConvertBack(new SolidColorBrush(color), typeof(Color), null, Culture));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("not a brush")]
|
||||
public void Anything_that_is_not_a_brush_does_not_convert_back(object? value)
|
||||
{
|
||||
var converter = new ColorToBrushConverter();
|
||||
|
||||
Assert.Equal(Binding.DoNothing, converter.ConvertBack(value, typeof(Color), null, Culture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_converters_are_fit_for_bindings()
|
||||
{
|
||||
Assert.IsAssignableFrom<IValueConverter>(new EnumToVisibilityConverter());
|
||||
Assert.IsAssignableFrom<IValueConverter>(new ColorToHexConverter());
|
||||
Assert.IsAssignableFrom<IValueConverter>(new ColorToBrushConverter());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Models;
|
||||
using CursorLang.Services;
|
||||
using CursorLang.Tests.Infrastructure;
|
||||
using CursorLang.ViewModels;
|
||||
using CursorLang.Views;
|
||||
|
||||
namespace CursorLang.Tests.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip window itself: where it ends up and how Windows sees it.
|
||||
/// </summary>
|
||||
public sealed class LayoutPopupWindowTests
|
||||
{
|
||||
private const int GwlExstyle = -20;
|
||||
private const int WsExNoactivate = 0x08000000;
|
||||
private const int WsExToolwindow = 0x00000080;
|
||||
|
||||
[Fact]
|
||||
public void The_window_is_created_before_the_first_show()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var popup = Popup.Create(new AppSettings());
|
||||
|
||||
// The handle is needed to set the window bounds before the show:
|
||||
// otherwise the window flashes at its default size for a moment
|
||||
Assert.NotEqual(IntPtr.Zero, popup.Handle);
|
||||
Assert.False(popup.Window.IsVisible);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_shows_what_the_view_model_gave_it()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
using var popup = Popup.Create(settings);
|
||||
|
||||
Assert.Same(popup.ViewModel, popup.Window.DataContext);
|
||||
|
||||
popup.ViewModel.ShortName = "RU";
|
||||
popup.Window.ShowPopup();
|
||||
|
||||
Assert.True(popup.Window.IsVisible);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_does_what_the_popup_service_expects_of_it()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var popup = Popup.Create(new AppSettings());
|
||||
|
||||
Assert.IsAssignableFrom<ILayoutPopupWindow>(popup.Window);
|
||||
});
|
||||
}
|
||||
|
||||
// The tooltip pops up over other applications and must neither take the
|
||||
// focus nor turn up in Alt+Tab
|
||||
[Fact]
|
||||
public void The_window_takes_no_focus_and_stays_out_of_the_switcher()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var popup = Popup.Create(new AppSettings());
|
||||
|
||||
int style = GetWindowLong(popup.Handle, GwlExstyle);
|
||||
|
||||
Assert.Equal(WsExNoactivate, style & WsExNoactivate);
|
||||
Assert.Equal(WsExToolwindow, style & WsExToolwindow);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_window_stays_on_top_and_out_of_the_mouses_way()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var popup = Popup.Create(new AppSettings());
|
||||
|
||||
Assert.True(popup.Window.Topmost);
|
||||
Assert.False(popup.Window.ShowInTaskbar);
|
||||
Assert.False(popup.Window.ShowActivated);
|
||||
Assert.False(popup.Window.IsHitTestVisible);
|
||||
Assert.False(popup.Window.Focusable);
|
||||
Assert.Equal(WindowStyle.None, popup.Window.WindowStyle);
|
||||
Assert.True(popup.Window.AllowsTransparency);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_opacity_comes_from_the_settings()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings { Opacity = 0.42 };
|
||||
using var popup = Popup.Create(settings);
|
||||
popup.Window.ShowPopup();
|
||||
|
||||
Assert.Equal(0.42, popup.Window.Opacity, precision: 3);
|
||||
|
||||
settings.Opacity = 0.75;
|
||||
Assert.Equal(0.75, popup.Window.Opacity, precision: 3);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ScreenPosition.TopLeft)]
|
||||
[InlineData(ScreenPosition.Top)]
|
||||
[InlineData(ScreenPosition.TopRight)]
|
||||
[InlineData(ScreenPosition.Center)]
|
||||
[InlineData(ScreenPosition.BottomLeft)]
|
||||
[InlineData(ScreenPosition.Bottom)]
|
||||
[InlineData(ScreenPosition.BottomRight)]
|
||||
public void In_the_fixed_point_mode_the_window_lands_where_it_was_computed(ScreenPosition position)
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings
|
||||
{
|
||||
PlacementMode = PopupPlacementMode.FixedPoint,
|
||||
ScreenPosition = position,
|
||||
ScreenMargin = 24,
|
||||
};
|
||||
|
||||
using var popup = Popup.Create(settings);
|
||||
popup.ViewModel.ShortName = "RU";
|
||||
|
||||
(PopupWindowNative.Rect before, _) = PopupWindowNative.GetActiveMonitorWorkArea();
|
||||
popup.Window.ShowPopup();
|
||||
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
||||
|
||||
if (!before.Equals(work))
|
||||
{
|
||||
Assert.Skip("The active monitor changed while the check was running");
|
||||
}
|
||||
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||
|
||||
int width = bounds.Right - bounds.Left;
|
||||
int height = bounds.Bottom - bounds.Top;
|
||||
PopupWindowNative.Point expected = PopupLayout.OnScreen(
|
||||
work, position, PopupLayout.ToPixels(settings.ScreenMargin, scale), width, height);
|
||||
|
||||
Assert.Equal(expected.X, bounds.Left);
|
||||
Assert.Equal(expected.Y, bounds.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void At_the_cursor_the_window_lands_next_to_it()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings
|
||||
{
|
||||
PlacementMode = PopupPlacementMode.AtCursor,
|
||||
CursorSide = AnchorSide.BottomRight,
|
||||
CursorOffset = 16,
|
||||
};
|
||||
|
||||
using var popup = Popup.Create(settings);
|
||||
popup.ViewModel.ShortName = "RU";
|
||||
|
||||
// The place is computed from where the cursor was at the moment of
|
||||
// the show. If it was moving right then, show it once more
|
||||
PopupWindowNative.Point before = default;
|
||||
|
||||
for (int attempt = 0; attempt < 10; attempt++)
|
||||
{
|
||||
before = PopupWindowNative.GetCursorPosition();
|
||||
popup.Window.ShowPopup();
|
||||
PopupWindowNative.Point after = PopupWindowNative.GetCursorPosition();
|
||||
|
||||
if (before.X == after.X && before.Y == after.Y)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (attempt == 9)
|
||||
{
|
||||
Assert.Skip("The cursor kept moving the whole time");
|
||||
}
|
||||
}
|
||||
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||
double scale = PopupWindowNative.GetScaleAt(before);
|
||||
|
||||
PopupWindowNative.Point expected = PopupLayout.NearAnchor(
|
||||
PopupLayout.AsAnchor(before),
|
||||
AnchorSide.BottomRight,
|
||||
PopupLayout.ToPixels(settings.CursorOffset, scale),
|
||||
bounds.Right - bounds.Left,
|
||||
bounds.Bottom - bounds.Top);
|
||||
|
||||
Assert.Equal(expected.X, bounds.Left);
|
||||
Assert.Equal(expected.Y, bounds.Top);
|
||||
});
|
||||
}
|
||||
|
||||
// There is no caret in the test environment, and the tooltip has to fall
|
||||
// back to the cursor
|
||||
[Fact]
|
||||
public void Without_a_caret_the_window_lands_at_the_cursor()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings
|
||||
{
|
||||
PlacementMode = PopupPlacementMode.AtCaret,
|
||||
CaretSide = AnchorSide.BottomRight,
|
||||
CaretOffset = 8,
|
||||
};
|
||||
|
||||
using var popup = Popup.Create(settings);
|
||||
popup.ViewModel.ShortName = "RU";
|
||||
|
||||
PopupWindowNative.Point before = PopupWindowNative.GetCursorPosition();
|
||||
popup.Window.ShowPopup();
|
||||
|
||||
PopupWindowNative.Rect bounds = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||
|
||||
// A gentle check: a window holding the input focus may still have a caret
|
||||
Assert.True(bounds.Right > bounds.Left);
|
||||
Assert.True(bounds.Bottom > bounds.Top);
|
||||
Assert.NotEqual(default, before);
|
||||
});
|
||||
}
|
||||
|
||||
// The window size equals the size of the text: the tooltip has no frame
|
||||
[Fact]
|
||||
public void The_window_size_follows_the_size_of_the_caption()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var small = new AppSettings { FontSize = 12, PlacementMode = PopupPlacementMode.FixedPoint };
|
||||
var large = new AppSettings { FontSize = 48, PlacementMode = PopupPlacementMode.FixedPoint };
|
||||
|
||||
using var smallPopup = Popup.Create(small);
|
||||
using var largePopup = Popup.Create(large);
|
||||
|
||||
smallPopup.ViewModel.ShortName = "RU";
|
||||
largePopup.ViewModel.ShortName = "RU";
|
||||
|
||||
smallPopup.Window.ShowPopup();
|
||||
largePopup.Window.ShowPopup();
|
||||
|
||||
PopupWindowNative.Rect smallBounds = WindowPlacementNative.TryGetBounds(smallPopup.Handle)!.Value;
|
||||
PopupWindowNative.Rect largeBounds = WindowPlacementNative.TryGetBounds(largePopup.Handle)!.Value;
|
||||
|
||||
Assert.True(largeBounds.Right - largeBounds.Left > smallBounds.Right - smallBounds.Left);
|
||||
Assert.True(largeBounds.Bottom - largeBounds.Top > smallBounds.Bottom - smallBounds.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Showing_again_moves_the_window_to_its_new_place()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
var settings = new AppSettings
|
||||
{
|
||||
PlacementMode = PopupPlacementMode.FixedPoint,
|
||||
ScreenPosition = ScreenPosition.TopLeft,
|
||||
};
|
||||
|
||||
using var popup = Popup.Create(settings);
|
||||
popup.ViewModel.ShortName = "RU";
|
||||
popup.Window.ShowPopup();
|
||||
|
||||
PopupWindowNative.Rect topLeft = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||
|
||||
settings.ScreenPosition = ScreenPosition.BottomRight;
|
||||
popup.Window.ShowPopup();
|
||||
|
||||
PopupWindowNative.Rect bottomRight = WindowPlacementNative.TryGetBounds(popup.Handle)!.Value;
|
||||
|
||||
Assert.True(bottomRight.Left > topLeft.Left);
|
||||
Assert.True(bottomRight.Top > topLeft.Top);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_hidden_window_stays_alive()
|
||||
{
|
||||
Sta.Run(() =>
|
||||
{
|
||||
using var popup = Popup.Create(new AppSettings());
|
||||
|
||||
popup.Window.ShowPopup();
|
||||
popup.Window.Hide();
|
||||
|
||||
Assert.False(popup.Window.IsVisible);
|
||||
Assert.NotEqual(IntPtr.Zero, popup.Handle);
|
||||
|
||||
popup.Window.ShowPopup();
|
||||
Assert.True(popup.Window.IsVisible);
|
||||
});
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
/// <summary>The tooltip window together with everything it needs to work.</summary>
|
||||
private sealed class Popup : IDisposable
|
||||
{
|
||||
private Popup(LayoutPopupWindow window, LayoutPopupViewModel viewModel)
|
||||
{
|
||||
Window = window;
|
||||
ViewModel = viewModel;
|
||||
Handle = new WindowInteropHelper(window).Handle;
|
||||
}
|
||||
|
||||
internal LayoutPopupWindow Window { get; }
|
||||
|
||||
internal LayoutPopupViewModel ViewModel { get; }
|
||||
|
||||
internal IntPtr Handle { get; }
|
||||
|
||||
internal static Popup Create(AppSettings settings)
|
||||
{
|
||||
var viewModel = new LayoutPopupViewModel(settings);
|
||||
return new Popup(new LayoutPopupWindow(viewModel, settings), viewModel);
|
||||
}
|
||||
|
||||
public void Dispose() => Window.Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The settings window as a whole: the markup, the bindings and the hook-up
|
||||
/// to the theme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<ComboBox> boxes = [.. FindAll<ComboBox>(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<string> displays =
|
||||
[
|
||||
.. FindAll<ComboBox>(window)
|
||||
.SelectMany(box => box.Items.OfType<object>())
|
||||
.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<TextBlock>(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<TextBlock>(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)));
|
||||
});
|
||||
}
|
||||
|
||||
[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<MainWindow> check) =>
|
||||
Open(viewModel, new FakeThemeService(), new MainWindowPlacement(), check);
|
||||
|
||||
private static void Open(SettingsViewModel viewModel, IThemeService theme, Action<MainWindow> check) =>
|
||||
Open(viewModel, theme, new MainWindowPlacement(), check);
|
||||
|
||||
private static void Open(
|
||||
SettingsViewModel viewModel,
|
||||
IThemeService theme,
|
||||
MainWindowPlacement placement,
|
||||
Action<MainWindow> 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 UpdateViewModel CreateUpdates(IUpdateService updates) =>
|
||||
new(updates, new FakeLocalizationService(), new AppSettings(), new UpdateOptions());
|
||||
|
||||
private static IEnumerable<Button> FindButtonsBoundTo(DependencyObject root, string path) =>
|
||||
FindAll<Button>(root).Where(button =>
|
||||
BindingOperations.GetBinding(button, ButtonBase.CommandProperty)?.Path.Path == path);
|
||||
|
||||
private static IEnumerable<CheckBox> FindStartupCheckBoxes(DependencyObject root) =>
|
||||
FindCheckBoxesBoundTo(root, nameof(SettingsViewModel.RunAtStartup));
|
||||
|
||||
// An element is found by what it is bound to: the markup gives them no names
|
||||
private static IEnumerable<CheckBox> FindCheckBoxesBoundTo(DependencyObject root, string path) =>
|
||||
FindAll<CheckBox>(root).Where(box =>
|
||||
BindingOperations.GetBinding(box, ToggleButton.IsCheckedProperty)?.Path.Path == path);
|
||||
|
||||
// Walking the element tree: the window markup is large, and things have to be searched for
|
||||
private static IEnumerable<TElement> FindAll<TElement>(DependencyObject root)
|
||||
where TElement : DependencyObject
|
||||
{
|
||||
int count = VisualTreeHelper.GetChildrenCount(root);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(root, i);
|
||||
|
||||
if (child is TElement found)
|
||||
{
|
||||
yield return found;
|
||||
}
|
||||
|
||||
foreach (TElement nested in FindAll<TElement>(child))
|
||||
{
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user