Files
cursor-lang/CursorLang.Core.Tests/Models/AppSettingsTests.cs
T
alex 5e11b16758 changed settings saving (#3)
Reviewed-on: #3
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-12 17:15:12 +00:00

320 lines
11 KiB
C#

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(500, settings.DurationMilliseconds);
Assert.Equal(300, settings.CapsLockHoldMilliseconds);
}
[Fact]
public void Every_mode_starts_out_looking_the_same()
{
var settings = new AppSettings();
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
Assert.Equal(16, settings.AtCursor.Offset);
Assert.Equal(CaretSide.Right, settings.AtCaret.Side);
Assert.Equal(16, settings.AtCaret.Offset);
// The middle of the monitor, where there is no edge to stand off from
Assert.Equal(ScreenPosition.Center, settings.FixedPoint.Position);
Assert.Equal(0, settings.FixedPoint.Offset);
PopupModeSettings[] modes = [settings.AtCursor, settings.AtCaret, settings.FixedPoint];
Assert.All(modes, mode =>
{
Assert.Equal(20, mode.FontSize);
Assert.Equal(0.9, mode.Opacity);
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), mode.BackgroundColor);
Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), mode.ForegroundColor);
});
// Three modes and three sets of settings: none of them is shared
Assert.Equal(3, modes.Distinct().Count());
}
[Theory]
[InlineData(PopupPlacementMode.AtCursor)]
[InlineData(PopupPlacementMode.AtCaret)]
[InlineData(PopupPlacementMode.FixedPoint)]
public void The_chosen_mode_is_the_one_handed_out(PopupPlacementMode mode)
{
var settings = new AppSettings { PlacementMode = mode };
PopupModeSettings expected = mode switch
{
PopupPlacementMode.AtCaret => settings.AtCaret,
PopupPlacementMode.FixedPoint => settings.FixedPoint,
_ => settings.AtCursor,
};
Assert.Same(expected, settings.Current);
}
/// <summary>
/// The settings of a mode are its own: setting one up leaves the others alone.
/// </summary>
/// <remarks>
/// This is the whole point of keeping them per mode. A look shared by the modes
/// meant setting it up again after every switch, and switching modes to see what
/// they do undid what had just been set up.
/// </remarks>
[Fact]
public void Setting_one_mode_up_leaves_the_others_where_they_were()
{
var settings = new AppSettings();
settings.AtCaret.FontSize = 12;
settings.AtCaret.Side = CaretSide.Left;
settings.AtCaret.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
Assert.Equal(20, settings.AtCursor.FontSize);
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
Assert.Equal(20, settings.FixedPoint.FontSize);
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.FixedPoint.BackgroundColor);
}
// The mode in force decides what the look means, so a switch of it is a change of
// everything the window shows through the current mode
[Fact]
public void A_switch_of_the_mode_is_announced_as_a_change_of_the_current_one()
{
var settings = new AppSettings();
List<string?> changed = [];
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
settings.PlacementMode = PopupPlacementMode.FixedPoint;
Assert.Contains(nameof(AppSettings.PlacementMode), changed);
Assert.Contains(nameof(AppSettings.Current), changed);
}
/// <summary>
/// A change inside a mode is passed on as a change of the settings.
/// </summary>
/// <remarks>
/// The service that writes the file listens to the settings alone. Without this, a
/// font size dragged in the window would never reach the disk.
/// </remarks>
[Fact]
public void A_change_inside_a_mode_is_announced_by_the_settings()
{
var settings = new AppSettings();
List<string?> changed = [];
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
settings.AtCaret.FontSize = 42;
settings.FixedPoint.Position = ScreenPosition.Bottom;
settings.AtCursor.Offset = 8;
Assert.Contains("AtCaret.FontSize", changed);
Assert.Contains("FixedPoint.Position", changed);
Assert.Contains("AtCursor.Offset", changed);
}
// 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<string?> 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<string?> changed = [];
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
property.SetValue(settings, value);
Assert.Empty(changed);
}
/// <summary>
/// Pouring another instance in fills the modes in place rather than replacing them.
/// </summary>
/// <remarks>
/// This is the agent's side of the connection: the fresh values arrive as a freshly
/// parsed instance. Replacing the mode objects would leave the popup bound to the
/// old ones.
/// </remarks>
[Fact]
public void Taking_other_settings_over_fills_the_modes_that_are_already_there()
{
var settings = new AppSettings();
CursorModeSettings cursor = settings.AtCursor;
var other = new AppSettings
{
Language = "ru",
Theme = AppTheme.Dark,
PlacementMode = PopupPlacementMode.FixedPoint,
DurationMilliseconds = 900,
UseCapsLockHotkey = true,
CapsLockHoldMilliseconds = 450,
};
other.AtCursor.Side = AnchorSide.TopLeft;
other.AtCursor.Offset = 5;
other.AtCursor.FontSize = 11;
other.AtCaret.Side = CaretSide.Left;
other.AtCaret.ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C);
other.FixedPoint.Position = ScreenPosition.Top;
other.FixedPoint.Offset = 64;
settings.CopyFrom(other);
Assert.Same(cursor, settings.AtCursor);
Assert.Equal("ru", settings.Language);
Assert.Equal(AppTheme.Dark, settings.Theme);
Assert.Equal(PopupPlacementMode.FixedPoint, settings.PlacementMode);
Assert.Equal(900, settings.DurationMilliseconds);
Assert.True(settings.UseCapsLockHotkey);
Assert.Equal(450, settings.CapsLockHoldMilliseconds);
Assert.Equal(AnchorSide.TopLeft, settings.AtCursor.Side);
Assert.Equal(5, settings.AtCursor.Offset);
Assert.Equal(11, settings.AtCursor.FontSize);
Assert.Equal(CaretSide.Left, settings.AtCaret.Side);
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), settings.AtCaret.ForegroundColor);
Assert.Equal(ScreenPosition.Top, settings.FixedPoint.Position);
Assert.Equal(64, settings.FixedPoint.Offset);
}
// Derived values and the modes handed out for convenience are computed from what
// is stored and have no business being in the file themselves
[Fact]
public void Derived_values_stay_out_of_the_file()
{
using JsonDocument document = JsonSerializer.SerializeToDocument(new AppSettings());
List<string> names = [.. document.RootElement.EnumerateObject().Select(property => property.Name)];
Assert.DoesNotContain(nameof(AppSettings.Duration), names);
Assert.DoesNotContain(nameof(AppSettings.CapsLockHoldDelay), names);
Assert.DoesNotContain(nameof(AppSettings.Current), 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);
Assert.Contains(nameof(AppSettings.AtCursor), names);
Assert.Contains(nameof(AppSettings.AtCaret), names);
Assert.Contains(nameof(AppSettings.FixedPoint), names);
}
[Fact]
public void Settings_report_changes_as_INotifyPropertyChanged()
{
Assert.IsAssignableFrom<INotifyPropertyChanged>(new AppSettings());
}
public static TheoryData<string> WritableProperties()
{
var data = new TheoryData<string>();
foreach (string name in WritablePropertyNames())
{
data.Add(name);
}
return data;
}
/// <summary>Names of the settings the user is able to change.</summary>
/// <remarks>
/// The modes are handed out rather than assigned — they are filled in place — so
/// they are not among these. What is inside them is checked by
/// <see cref="PopupModeSettingsTests"/>.
/// </remarks>
internal static IEnumerable<string> 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");
}
}