using System.ComponentModel; using System.Drawing; using System.Reflection; using System.Text.Json; using CursorLang.Core.Models; namespace CursorLang.Core.Tests.Models; public sealed class AppSettingsTests { [Fact] public void Default_values_describe_a_tooltip_at_the_cursor() { var settings = new AppSettings(); Assert.Equal("en", settings.Language); Assert.Equal(AppTheme.System, settings.Theme); Assert.Equal(PopupPlacementMode.AtCursor, settings.PlacementMode); Assert.Equal(AnchorSide.BottomRight, settings.CursorSide); Assert.Equal(16, settings.CursorOffset); Assert.Equal(AnchorSide.BottomRight, settings.CaretSide); Assert.Equal(16, settings.CaretOffset); Assert.Equal(ScreenPosition.BottomRight, settings.ScreenPosition); Assert.Equal(24, settings.ScreenMargin); Assert.Equal(20, settings.FontSize); Assert.Equal(0.9, settings.Opacity); Assert.Equal(500, settings.DurationMilliseconds); Assert.Equal(300, settings.CapsLockHoldMilliseconds); Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.BackgroundColor); Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), settings.ForegroundColor); } // The app must not change how the system behaves until it is asked to [Fact] public void Caps_Lock_interception_is_off_by_default() { Assert.False(new AppSettings().UseCapsLockHotkey); } [Fact] public void The_time_on_screen_is_derived_from_milliseconds() { var settings = new AppSettings { DurationMilliseconds = 1250 }; Assert.Equal(TimeSpan.FromMilliseconds(1250), settings.Duration); } [Fact] public void The_hold_threshold_is_derived_from_milliseconds() { var settings = new AppSettings { CapsLockHoldMilliseconds = 400 }; Assert.Equal(TimeSpan.FromMilliseconds(400), settings.CapsLockHoldDelay); } [Theory] [MemberData(nameof(WritableProperties))] public void A_changed_setting_is_announced_to_subscribers(string propertyName) { var settings = new AppSettings(); List changed = []; settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName); SetDifferentValue(settings, propertyName); Assert.Contains(propertyName, changed); } [Theory] [MemberData(nameof(WritableProperties))] public void Writing_the_same_value_leaves_subscribers_alone(string propertyName) { var settings = new AppSettings(); PropertyInfo property = typeof(AppSettings).GetProperty(propertyName)!; object? value = property.GetValue(settings); List changed = []; settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName); property.SetValue(settings, value); Assert.Empty(changed); } // Duration and CapsLockHoldDelay are derived from other settings and have // no business being in the file [Fact] public void Derived_values_stay_out_of_the_file() { using JsonDocument document = JsonSerializer.SerializeToDocument(new AppSettings()); List names = [.. document.RootElement.EnumerateObject().Select(property => property.Name)]; Assert.DoesNotContain(nameof(AppSettings.Duration), names); Assert.DoesNotContain(nameof(AppSettings.CapsLockHoldDelay), names); // What they are derived from, on the other hand, has to be stored Assert.Contains(nameof(AppSettings.DurationMilliseconds), names); Assert.Contains(nameof(AppSettings.CapsLockHoldMilliseconds), names); } [Fact] public void Settings_report_changes_as_INotifyPropertyChanged() { Assert.IsAssignableFrom(new AppSettings()); } public static TheoryData WritableProperties() { var data = new TheoryData(); foreach (string name in WritablePropertyNames()) { data.Add(name); } return data; } /// Names of the settings the user is able to change. internal static IEnumerable WritablePropertyNames() => typeof(AppSettings).GetProperties() .Where(property => property.CanWrite) .Select(property => property.Name); // A value guaranteed to differ from the current one: each kind of setting // has its own way of differing private static void SetDifferentValue(AppSettings settings, string propertyName) { PropertyInfo property = typeof(AppSettings).GetProperty(propertyName)!; object? current = property.GetValue(settings); object next = current switch { string text => text + "-other", double number => number + 1, bool flag => !flag, Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B), Enum value => NextEnumValue(value), _ => throw new NotSupportedException($"Unknown kind of setting: {property.PropertyType}"), }; property.SetValue(settings, next); } private static object NextEnumValue(Enum current) { Array values = Enum.GetValues(current.GetType()); foreach (object? value in values) { if (!Equals(value, current)) { return value!; } } throw new NotSupportedException($"{current.GetType()} has a single value"); } }