using System.ComponentModel; using System.Drawing; using System.Reflection; using System.Text.Json; using CursorLang.Core.Models; namespace CursorLang.Core.Tests.Models; /// /// The settings of a single placement mode: what the popup looks like there and how /// far from its anchor it sits. /// public sealed class PopupModeSettingsTests { [Fact] public void The_cursor_mode_starts_out_below_and_right_of_the_pointer() { var mode = new CursorModeSettings(); Assert.Equal(AnchorSide.BottomRight, mode.Side); Assert.Equal(16, mode.Offset); } /// /// Next to the caret the popup goes beside it, and to the right by default. /// /// /// Above or below the caret is where the next line of the text is, so those sides /// are not on offer at all — the type has the two of them and no more. /// [Fact] public void The_caret_mode_starts_out_to_the_right_of_the_caret() { var mode = new CaretModeSettings(); Assert.Equal(CaretSide.Right, mode.Side); Assert.Equal(16, mode.Offset); Assert.Equal([CaretSide.Left, CaretSide.Right], Enum.GetValues()); } [Theory] [InlineData(CaretSide.Left, AnchorSide.Left)] [InlineData(CaretSide.Right, AnchorSide.Right)] public void The_side_of_the_caret_lines_the_popup_up_with_it(CaretSide side, AnchorSide expected) { var mode = new CaretModeSettings { Side = side }; Assert.Equal(expected, mode.Anchor); } [Fact] public void A_changed_side_of_the_caret_is_a_change_of_what_it_lines_up_with() { var mode = new CaretModeSettings(); List changed = []; mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName); mode.Side = CaretSide.Left; Assert.Contains(nameof(CaretModeSettings.Anchor), changed); } /// /// The fixed point starts out in the middle of the monitor, where there is no edge /// to stand off from. /// [Fact] public void The_fixed_point_starts_out_in_the_middle_with_no_offset() { var mode = new FixedPointModeSettings(); Assert.Equal(ScreenPosition.Center, mode.Position); Assert.Equal(0, mode.Offset); Assert.False(mode.IsAtAnEdge); } // A setting that is not shown must not be one that still applies [Fact] public void Moving_the_fixed_point_to_the_middle_drops_the_offset() { var mode = new FixedPointModeSettings { Position = ScreenPosition.TopLeft, Offset = 40 }; Assert.True(mode.IsAtAnEdge); mode.Position = ScreenPosition.Center; Assert.Equal(0, mode.Offset); Assert.False(mode.IsAtAnEdge); } [Theory] [InlineData(ScreenPosition.TopLeft)] [InlineData(ScreenPosition.Top)] [InlineData(ScreenPosition.TopRight)] [InlineData(ScreenPosition.BottomLeft)] [InlineData(ScreenPosition.Bottom)] [InlineData(ScreenPosition.BottomRight)] public void Away_from_the_middle_the_offset_is_kept(ScreenPosition position) { var mode = new FixedPointModeSettings { Position = position, Offset = 40 }; Assert.Equal(40, mode.Offset); Assert.True(mode.IsAtAnEdge); } [Fact] public void A_changed_place_of_the_fixed_point_is_a_change_of_having_an_edge() { var mode = new FixedPointModeSettings(); List changed = []; mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName); mode.Position = ScreenPosition.Bottom; Assert.Contains(nameof(FixedPointModeSettings.IsAtAnEdge), changed); } [Theory] [MemberData(nameof(WritableProperties), typeof(CursorModeSettings))] [MemberData(nameof(WritableProperties), typeof(CaretModeSettings))] [MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))] public void A_changed_setting_is_announced_to_subscribers(Type type, string propertyName) { PopupModeSettings mode = Create(type); List changed = []; mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName); SetDifferentValue(mode, propertyName); Assert.Contains(propertyName, changed); } [Theory] [MemberData(nameof(WritableProperties), typeof(CursorModeSettings))] [MemberData(nameof(WritableProperties), typeof(CaretModeSettings))] [MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))] public void Writing_the_same_value_leaves_subscribers_alone(Type type, string propertyName) { PopupModeSettings mode = Create(type); PropertyInfo property = type.GetProperty(propertyName)!; List changed = []; mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName); property.SetValue(mode, property.GetValue(mode)); Assert.Empty(changed); } [Fact] public void The_cursor_mode_takes_another_one_over_whole() { var mode = new CursorModeSettings(); var other = new CursorModeSettings { Side = AnchorSide.Left, Offset = 3, FontSize = 41, Opacity = 0.25, BackgroundColor = Color.FromArgb(0x01, 0x02, 0x03), ForegroundColor = Color.FromArgb(0x04, 0x05, 0x06), }; mode.CopyFrom(other); Assert.Equal(AnchorSide.Left, mode.Side); Assert.Equal(3, mode.Offset); Assert.Equal(41, mode.FontSize); Assert.Equal(0.25, mode.Opacity); Assert.Equal(Color.FromArgb(0x01, 0x02, 0x03), mode.BackgroundColor); Assert.Equal(Color.FromArgb(0x04, 0x05, 0x06), mode.ForegroundColor); } [Fact] public void The_caret_mode_takes_another_one_over_whole() { var mode = new CaretModeSettings(); var other = new CaretModeSettings { Side = CaretSide.Left, Offset = 2, FontSize = 15 }; mode.CopyFrom(other); Assert.Equal(CaretSide.Left, mode.Side); Assert.Equal(2, mode.Offset); Assert.Equal(15, mode.FontSize); } [Fact] public void The_fixed_point_takes_another_one_over_whole() { var mode = new FixedPointModeSettings(); var other = new FixedPointModeSettings { Position = ScreenPosition.Bottom, Offset = 120, FontSize = 60, Opacity = 0.5, BackgroundColor = Color.FromArgb(0x07, 0x08, 0x09), ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C), }; mode.CopyFrom(other); Assert.Equal(ScreenPosition.Bottom, mode.Position); Assert.Equal(120, mode.Offset); Assert.Equal(60, mode.FontSize); Assert.Equal(0.5, mode.Opacity); Assert.Equal(Color.FromArgb(0x07, 0x08, 0x09), mode.BackgroundColor); Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), mode.ForegroundColor); } // Taking over a mode that sits in the middle takes its lack of an offset over too [Fact] public void Taking_over_a_fixed_point_in_the_middle_leaves_no_offset() { var mode = new FixedPointModeSettings { Position = ScreenPosition.Top, Offset = 32 }; mode.CopyFrom(new FixedPointModeSettings()); Assert.Equal(ScreenPosition.Center, mode.Position); Assert.Equal(0, mode.Offset); } // What a mode works out from what it stores is not stored itself [Fact] public void Derived_values_of_a_mode_stay_out_of_the_file() { List caret = Stored(new CaretModeSettings()); List fixedPoint = Stored(new FixedPointModeSettings()); Assert.DoesNotContain(nameof(CaretModeSettings.Anchor), caret); Assert.DoesNotContain(nameof(FixedPointModeSettings.IsAtAnEdge), fixedPoint); Assert.Contains(nameof(CaretModeSettings.Side), caret); Assert.Contains(nameof(FixedPointModeSettings.Position), fixedPoint); } [Fact] public void A_mode_reports_changes_as_INotifyPropertyChanged() { Assert.IsAssignableFrom(new CursorModeSettings()); Assert.IsAssignableFrom(new CaretModeSettings()); Assert.IsAssignableFrom(new FixedPointModeSettings()); } public static TheoryData WritableProperties(Type type) { var data = new TheoryData(); foreach (string name in WritablePropertyNames(type)) { data.Add(type, name); } return data; } /// Names of the settings of a mode the user is able to change. internal static IEnumerable WritablePropertyNames(Type type) => type.GetProperties().Where(property => property.CanWrite).Select(property => property.Name); private static PopupModeSettings Create(Type type) => (PopupModeSettings)Activator.CreateInstance(type)!; private static List Stored(TMode mode) where TMode : PopupModeSettings { using JsonDocument document = JsonSerializer.SerializeToDocument(mode); return [.. document.RootElement.EnumerateObject().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(PopupModeSettings mode, string propertyName) { PropertyInfo property = mode.GetType().GetProperty(propertyName)!; object next = property.GetValue(mode) switch { double number => number + 1, Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B), Enum value => Enum.GetValues(value.GetType()) .Cast() .First(other => !Equals(other, value)), var other => throw new NotSupportedException($"Unknown kind of setting: {other?.GetType()}"), }; property.SetValue(mode, next); } }