changed settings saving
Pull request / build (pull_request) Successful in 39s

This commit is contained in:
2026-08-12 21:31:42 +05:00
parent 68b3d544d2
commit 56a398f055
19 changed files with 1169 additions and 150 deletions
+170 -12
View File
@@ -16,18 +16,115 @@ public sealed class AppSettingsTests
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);
}
[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
@@ -82,8 +179,60 @@ public sealed class AppSettingsTests
Assert.Empty(changed);
}
// Duration and CapsLockHoldDelay are derived from other settings and have
// no business being in the file
/// <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()
{
@@ -93,10 +242,14 @@ public sealed class AppSettingsTests
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]
@@ -118,6 +271,11 @@ public sealed class AppSettingsTests
}
/// <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)
@@ -0,0 +1,289 @@
using System.ComponentModel;
using System.Drawing;
using System.Reflection;
using System.Text.Json;
using CursorLang.Core.Models;
namespace CursorLang.Core.Tests.Models;
/// <summary>
/// The settings of a single placement mode: what the popup looks like there and how
/// far from its anchor it sits.
/// </summary>
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);
}
/// <summary>
/// Next to the caret the popup goes beside it, and to the right by default.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<CaretSide>());
}
[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<string?> changed = [];
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
mode.Side = CaretSide.Left;
Assert.Contains(nameof(CaretModeSettings.Anchor), changed);
}
/// <summary>
/// The fixed point starts out in the middle of the monitor, where there is no edge
/// to stand off from.
/// </summary>
[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<string?> 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<string?> 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<string?> 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<string> caret = Stored(new CaretModeSettings());
List<string> 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<INotifyPropertyChanged>(new CursorModeSettings());
Assert.IsAssignableFrom<INotifyPropertyChanged>(new CaretModeSettings());
Assert.IsAssignableFrom<INotifyPropertyChanged>(new FixedPointModeSettings());
}
public static TheoryData<Type, string> WritableProperties(Type type)
{
var data = new TheoryData<Type, string>();
foreach (string name in WritablePropertyNames(type))
{
data.Add(type, name);
}
return data;
}
/// <summary>Names of the settings of a mode the user is able to change.</summary>
internal static IEnumerable<string> 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<string> Stored<TMode>(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<object>()
.First(other => !Equals(other, value)),
var other => throw new NotSupportedException($"Unknown kind of setting: {other?.GetType()}"),
};
property.SetValue(mode, next);
}
}
@@ -108,6 +108,11 @@ public sealed class StringsTests
data.Add(key);
}
foreach (string key in EnumKeysOf<CaretSide>())
{
data.Add(key);
}
foreach (string key in EnumKeysOf<ScreenPosition>())
{
data.Add(key);
@@ -71,7 +71,7 @@ public sealed class CapsLockSwitchCoordinatorTests
int startsBefore = hotkey.StartCalls;
int stopsBefore = hotkey.StopCalls;
settings.FontSize = 44;
settings.Current.FontSize = 44;
settings.CapsLockHoldMilliseconds = 700;
Assert.Equal(startsBefore, hotkey.StartCalls);
@@ -30,7 +30,7 @@ public sealed class SettingsServiceTests
});
Assert.Equal(AppTheme.System, settings.Theme);
Assert.Equal(20, settings.FontSize);
Assert.Equal(20, settings.Current.FontSize);
}
[Theory]
@@ -72,10 +72,10 @@ public sealed class SettingsServiceTests
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.FontSize = 42;
settings.Theme = AppTheme.Dark;
settings.PlacementMode = PopupPlacementMode.AtCaret;
settings.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
settings.Current.FontSize = 42;
settings.Current.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
settings.UseCapsLockHotkey = true;
service.Save();
@@ -87,10 +87,10 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(42, restored.FontSize);
Assert.Equal(42, restored.Current.FontSize);
Assert.Equal(AppTheme.Dark, restored.Theme);
Assert.Equal(PopupPlacementMode.AtCaret, restored.PlacementMode);
Assert.Equal(Color.FromArgb(0x11, 0x22, 0x33), restored.BackgroundColor);
Assert.Equal(Color.FromArgb(0x11, 0x22, 0x33), restored.Current.BackgroundColor);
Assert.True(restored.UseCapsLockHotkey);
}
@@ -104,7 +104,7 @@ public sealed class SettingsServiceTests
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.Theme = AppTheme.Dark;
settings.BackgroundColor = Color.FromArgb(0x20, 0x20, 0x20);
settings.Current.BackgroundColor = Color.FromArgb(0x20, 0x20, 0x20);
service.Save();
});
@@ -130,7 +130,7 @@ public sealed class SettingsServiceTests
AppSettings settings = service.Load();
service.TrackChanges();
settings.FontSize = 33;
settings.Current.FontSize = 33;
// Right after the edit there is nothing on disk yet: the write is deferred
Assert.False(File.Exists(path));
@@ -156,7 +156,7 @@ public sealed class SettingsServiceTests
for (int i = 0; i < 10; i++)
{
settings.Opacity = 0.5 + (i * 0.01);
settings.Current.Opacity = 0.5 + (i * 0.01);
Assert.False(File.Exists(path));
Pump.Pause(TimeSpan.FromMilliseconds(10));
}
@@ -188,7 +188,7 @@ public sealed class SettingsServiceTests
service.TrackChanges();
AppSettings settings = service.Load();
settings.FontSize = 29;
settings.Current.FontSize = 29;
Pump.WaitFor(() => File.Exists(path), "the settings were written by the timer");
});
@@ -230,11 +230,11 @@ public sealed class SettingsServiceTests
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
File.WriteAllText(path, """{"FontSize": 31, "BackgroundColor": "#FF102030"}""");
File.WriteAllText(path, """{"AtCursor": {"FontSize": 31, "BackgroundColor": "#FF102030"}}""");
service.Reload();
Assert.Equal(31, settings.FontSize);
Assert.Equal(Color.FromArgb(0x10, 0x20, 0x30), settings.BackgroundColor);
Assert.Equal(31, settings.Current.FontSize);
Assert.Equal(Color.FromArgb(0x10, 0x20, 0x30), settings.Current.BackgroundColor);
});
}
@@ -250,12 +250,12 @@ public sealed class SettingsServiceTests
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.FontSize = 44;
settings.Current.FontSize = 44;
File.WriteAllText(path, "not json at all");
service.Reload();
Assert.Equal(44, settings.FontSize);
Assert.Equal(44, settings.Current.FontSize);
});
}
@@ -269,7 +269,7 @@ public sealed class SettingsServiceTests
SettingsService service = Create(folder);
AppSettings settings = service.Load();
service.TrackChanges();
settings.FontSize = 27;
settings.Current.FontSize = 27;
service.Dispose();
});
@@ -295,7 +295,7 @@ public sealed class SettingsServiceTests
string afterDispose = File.ReadAllText(path);
settings.FontSize = 99;
settings.Current.FontSize = 99;
Pump.Pause(TimeSpan.FromMilliseconds(60));
Assert.Equal(afterDispose, File.ReadAllText(path));
@@ -309,7 +309,7 @@ public sealed class SettingsServiceTests
string inherited = folder.File("inherited.json");
string own = folder.File("settings.json");
File.WriteAllText(inherited, """{"FontSize": 31, "Language": "ru"}""");
File.WriteAllText(inherited, """{"AtCursor": {"FontSize": 31}, "Language": "ru"}""");
AppSettings settings = Pump.Run(() =>
{
@@ -317,7 +317,7 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(31, settings.FontSize);
Assert.Equal(31, settings.Current.FontSize);
Assert.Equal("ru", settings.Language);
// What was taken over is pinned to its new place at once rather than on the first edit
@@ -331,7 +331,7 @@ public sealed class SettingsServiceTests
{
using var folder = new TempFolder();
string inherited = folder.File("inherited.json");
string original = """{"FontSize": 31}""";
string original = """{"AtCursor": {"FontSize": 31}}""";
File.WriteAllText(inherited, original);
@@ -351,8 +351,8 @@ public sealed class SettingsServiceTests
string own = folder.File("settings.json");
string inherited = folder.File("inherited.json");
File.WriteAllText(own, """{"FontSize": 12}""");
File.WriteAllText(inherited, """{"FontSize": 31}""");
File.WriteAllText(own, """{"AtCursor": {"FontSize": 12}}""");
File.WriteAllText(inherited, """{"AtCursor": {"FontSize": 31}}""");
AppSettings settings = Pump.Run(() =>
{
@@ -360,7 +360,7 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(12, settings.FontSize);
Assert.Equal(12, settings.Current.FontSize);
}
// Outside a package both paths are the same, so there is nothing to take over
@@ -392,14 +392,14 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(20, settings.FontSize);
Assert.Equal(20, settings.Current.FontSize);
}
[Fact]
public void Settings_with_unknown_fields_are_still_read()
{
using var folder = new TempFolder();
File.WriteAllText(folder.File("settings.json"), """{"FontSize": 15, "SomethingNew": true}""");
File.WriteAllText(folder.File("settings.json"), """{"AtCursor": {"FontSize": 15}, "SomethingNew": true}""");
AppSettings settings = Pump.Run(() =>
{
@@ -407,7 +407,7 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(15, settings.FontSize);
Assert.Equal(15, settings.Current.FontSize);
}
[Theory]
@@ -417,7 +417,7 @@ public sealed class SettingsServiceTests
public void A_colour_is_read_from_its_usual_notation(string stored, byte r, byte g, byte b)
{
using var folder = new TempFolder();
File.WriteAllText(folder.File("settings.json"), $$"""{"BackgroundColor": {{stored}}}""");
File.WriteAllText(folder.File("settings.json"), $$"""{"AtCursor": {"BackgroundColor": {{stored}} } }""");
AppSettings settings = Pump.Run(() =>
{
@@ -425,7 +425,7 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(Color.FromArgb(r, g, b), settings.BackgroundColor);
Assert.Equal(Color.FromArgb(r, g, b), settings.Current.BackgroundColor);
}
[Theory]
@@ -435,7 +435,7 @@ public sealed class SettingsServiceTests
public void An_unintelligible_colour_becomes_black(string stored)
{
using var folder = new TempFolder();
File.WriteAllText(folder.File("settings.json"), $$"""{"BackgroundColor": {{stored}}}""");
File.WriteAllText(folder.File("settings.json"), $$"""{"AtCursor": {"BackgroundColor": {{stored}} } }""");
AppSettings settings = Pump.Run(() =>
{
@@ -443,7 +443,7 @@ public sealed class SettingsServiceTests
return service.Load();
});
Assert.Equal(Color.Black, settings.BackgroundColor);
Assert.Equal(Color.Black, settings.Current.BackgroundColor);
}
// The service creates the settings folder itself
@@ -478,7 +478,7 @@ public sealed class SettingsServiceTests
using SettingsService service = new(path, folder.File("inherited.json"), SaveDelay);
AppSettings settings = service.Load();
settings.FontSize = 18;
settings.Current.FontSize = 18;
service.Save();
});
@@ -534,6 +534,107 @@ public sealed class SettingsServiceTests
}
}
// The modes are sections of their own, and every setting of a mode has to reach the
// file inside its own section
[Theory]
[InlineData(nameof(AppSettings.AtCursor), typeof(CursorModeSettings))]
[InlineData(nameof(AppSettings.AtCaret), typeof(CaretModeSettings))]
[InlineData(nameof(AppSettings.FixedPoint), typeof(FixedPointModeSettings))]
public void Every_setting_of_a_mode_reaches_its_section_of_the_file(string section, Type mode)
{
using var folder = new TempFolder();
Pump.Run(() =>
{
using SettingsService service = Create(folder);
_ = service.Load();
service.Save();
});
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(folder.File("settings.json")));
Assert.True(document.RootElement.TryGetProperty(section, out JsonElement stored));
foreach (string name in PopupModeSettingsTests.WritablePropertyNames(mode))
{
Assert.True(stored.TryGetProperty(name, out _), $"{section}.{name}");
}
}
/// <summary>
/// The modes are stored apart: what is set up in one is still there after a trip
/// through the file and the other two.
/// </summary>
[Fact]
public void Each_mode_keeps_its_own_settings()
{
using var folder = new TempFolder();
Pump.Run(() =>
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.AtCursor.Side = AnchorSide.TopLeft;
settings.AtCursor.FontSize = 14;
settings.AtCaret.Side = CaretSide.Left;
settings.AtCaret.FontSize = 28;
settings.AtCaret.ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C);
settings.FixedPoint.Position = ScreenPosition.Top;
settings.FixedPoint.Offset = 96;
settings.FixedPoint.Opacity = 0.4;
service.Save();
});
AppSettings restored = Pump.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(AnchorSide.TopLeft, restored.AtCursor.Side);
Assert.Equal(14, restored.AtCursor.FontSize);
Assert.Equal(CaretSide.Left, restored.AtCaret.Side);
Assert.Equal(28, restored.AtCaret.FontSize);
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), restored.AtCaret.ForegroundColor);
Assert.Equal(ScreenPosition.Top, restored.FixedPoint.Position);
Assert.Equal(96, restored.FixedPoint.Offset);
Assert.Equal(0.4, restored.FixedPoint.Opacity);
// What was left alone stays at its default rather than following a neighbour
Assert.Equal(0.9, restored.AtCursor.Opacity);
}
// A change inside a mode is a change of the settings: the file has to follow the
// sliders of the look as well as the ones above them
[Fact]
public void A_change_inside_a_mode_saves_itself()
{
using var folder = new TempFolder();
string path = folder.File("settings.json");
Pump.Run(() =>
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
service.TrackChanges();
settings.AtCaret.FontSize = 37;
Pump.WaitFor(() => File.Exists(path), "the settings were written by the timer");
});
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
Assert.Equal(
37,
document.RootElement
.GetProperty(nameof(AppSettings.AtCaret))
.GetProperty(nameof(PopupModeSettings.FontSize))
.GetDouble());
}
// An ordinary run picks the storage place itself: a package keeps settings
// of its own, a separate install keeps them in the user profile
[Fact]