Files
cursor-lang/CursorLang.Tests/Services/SettingsServiceTests.cs
T
2026-08-09 18:31:43 +05:00

455 lines
14 KiB
C#

using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Windows.Media;
using CursorLang.Models;
using CursorLang.Services;
using CursorLang.Tests.Infrastructure;
using CursorLang.Tests.Models;
namespace CursorLang.Tests.Services;
/// <summary>
/// Keeping the settings in a file. Everything happens in a temporary folder:
/// the tests have no business touching the user's own settings.
/// </summary>
public sealed class SettingsServiceTests
{
/// <summary>The deferred write delay in tests: half a second is not worth waiting for.</summary>
private static readonly TimeSpan SaveDelay = TimeSpan.FromMilliseconds(20);
[Fact]
public void Without_a_file_the_defaults_are_handed_out()
{
using var folder = new TempFolder();
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(AppTheme.System, settings.Theme);
Assert.Equal(20, settings.FontSize);
}
[Theory]
[InlineData("ru", "ru")]
[InlineData("ru-RU", "ru")]
[InlineData("en-US", "en")]
[InlineData("de-DE", "en")]
[InlineData("fr", "en")]
public void The_default_language_follows_the_language_of_Windows(string uiCulture, string expected)
{
using var folder = new TempFolder();
AppSettings settings = Sta.Run(() =>
{
CultureInfo previous = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(uiCulture);
using SettingsService service = Create(folder);
return service.Load();
}
finally
{
CultureInfo.CurrentUICulture = previous;
}
});
Assert.Equal(expected, settings.Language);
}
[Fact]
public void Saved_settings_are_read_back()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.FontSize = 42;
settings.Theme = AppTheme.Dark;
settings.PlacementMode = PopupPlacementMode.AtCaret;
settings.BackgroundColor = Color.FromRgb(0x11, 0x22, 0x33);
settings.UseCapsLockHotkey = true;
service.Save();
});
AppSettings restored = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(42, restored.FontSize);
Assert.Equal(AppTheme.Dark, restored.Theme);
Assert.Equal(PopupPlacementMode.AtCaret, restored.PlacementMode);
Assert.Equal(Color.FromRgb(0x11, 0x22, 0x33), restored.BackgroundColor);
Assert.True(restored.UseCapsLockHotkey);
}
[Fact]
public void The_settings_land_in_the_file_in_a_readable_form()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.Theme = AppTheme.Dark;
settings.BackgroundColor = Color.FromRgb(0x20, 0x20, 0x20);
service.Save();
});
string json = File.ReadAllText(folder.File("settings.json"));
// The theme as a word rather than a number; the colour in its usual notation
Assert.Contains("\"Theme\": \"Dark\"", json, StringComparison.Ordinal);
Assert.Contains("#FF202020", json, StringComparison.Ordinal);
// And all of it across lines: the file is sometimes edited by hand
Assert.Contains('\n', json);
}
[Fact]
public void A_changed_setting_saves_itself()
{
using var folder = new TempFolder();
string path = folder.File("settings.json");
Sta.Run(() =>
{
using SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.FontSize = 33;
// Right after the edit there is nothing on disk yet: the write is deferred
Assert.False(File.Exists(path));
Sta.WaitFor(() => File.Exists(path), "the settings were written by the timer");
});
Assert.Contains("\"FontSize\": 33", File.ReadAllText(path), StringComparison.Ordinal);
}
// A slider changes its value continuously, and writing every move to disk is pointless
[Fact]
public void A_run_of_edits_defers_the_write_until_a_pause()
{
using var folder = new TempFolder();
string path = folder.File("settings.json");
Sta.Run(() =>
{
using SettingsService service = new(path, folder.File("inherited.json"), TimeSpan.FromMilliseconds(150));
AppSettings settings = service.Load();
for (int i = 0; i < 10; i++)
{
settings.Opacity = 0.5 + (i * 0.01);
Assert.False(File.Exists(path));
Sta.Pause(TimeSpan.FromMilliseconds(10));
}
Sta.WaitFor(() => File.Exists(path), "the write happened after the pause in edits");
});
}
[Fact]
public void Closing_the_service_saves_the_latest_edits()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
SettingsService service = Create(folder);
AppSettings settings = service.Load();
settings.FontSize = 27;
service.Dispose();
});
Assert.Contains(
"\"FontSize\": 27",
File.ReadAllText(folder.File("settings.json")),
StringComparison.Ordinal);
}
[Fact]
public void After_closing_edits_no_longer_reach_the_disk()
{
using var folder = new TempFolder();
string path = folder.File("settings.json");
Sta.Run(() =>
{
SettingsService service = Create(folder);
AppSettings settings = service.Load();
service.Dispose();
string afterDispose = File.ReadAllText(path);
settings.FontSize = 99;
Sta.Pause(TimeSpan.FromMilliseconds(60));
Assert.Equal(afterDispose, File.ReadAllText(path));
});
}
[Fact]
public void Settings_of_a_previous_install_are_taken_over()
{
using var folder = new TempFolder();
string inherited = folder.File("inherited.json");
string own = folder.File("settings.json");
File.WriteAllText(inherited, """{"FontSize": 31, "Language": "ru"}""");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = new(own, inherited, SaveDelay);
return service.Load();
});
Assert.Equal(31, settings.FontSize);
Assert.Equal("ru", settings.Language);
// What was taken over is pinned to its new place at once rather than on the first edit
Assert.True(File.Exists(own));
Assert.Contains("\"FontSize\": 31", File.ReadAllText(own), StringComparison.Ordinal);
}
// Both builds may be installed side by side: the other one keeps its settings
[Fact]
public void The_previous_install_does_not_lose_its_settings()
{
using var folder = new TempFolder();
string inherited = folder.File("inherited.json");
string original = """{"FontSize": 31}""";
File.WriteAllText(inherited, original);
Sta.Run(() =>
{
using SettingsService service = new(folder.File("settings.json"), inherited, SaveDelay);
_ = service.Load();
});
Assert.Equal(original, File.ReadAllText(inherited));
}
[Fact]
public void Own_settings_outweigh_those_of_a_previous_install()
{
using var folder = new TempFolder();
string own = folder.File("settings.json");
string inherited = folder.File("inherited.json");
File.WriteAllText(own, """{"FontSize": 12}""");
File.WriteAllText(inherited, """{"FontSize": 31}""");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = new(own, inherited, SaveDelay);
return service.Load();
});
Assert.Equal(12, settings.FontSize);
}
// Outside a package both paths are the same, so there is nothing to take over
[Fact]
public void Without_a_package_no_settings_are_taken_over()
{
using var folder = new TempFolder();
string path = folder.File("settings.json");
Sta.Run(() =>
{
using SettingsService service = new(path, path, SaveDelay);
_ = service.Load();
// No file appeared: there was nothing to take over and nowhere to take it from
Assert.False(File.Exists(path));
});
}
[Fact]
public void A_broken_settings_file_does_not_bring_the_app_down()
{
using var folder = new TempFolder();
File.WriteAllText(folder.File("settings.json"), "{this is not json");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(20, settings.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}""");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(15, settings.FontSize);
}
[Theory]
[InlineData("\"#FF102030\"", 0x10, 0x20, 0x30)]
[InlineData("\"#102030\"", 0x10, 0x20, 0x30)]
[InlineData("\"Red\"", 0xFF, 0x00, 0x00)]
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}}}""");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(Color.FromRgb(r, g, b), settings.BackgroundColor);
}
[Theory]
[InlineData("\"\"")]
[InlineData("\" \"")]
[InlineData("\"not a colour\"")]
public void An_unintelligible_colour_becomes_black(string stored)
{
using var folder = new TempFolder();
File.WriteAllText(folder.File("settings.json"), $$"""{"BackgroundColor": {{stored}}}""");
AppSettings settings = Sta.Run(() =>
{
using SettingsService service = Create(folder);
return service.Load();
});
Assert.Equal(Colors.Black, settings.BackgroundColor);
}
// The service creates the settings folder itself
[Fact]
public void The_settings_folder_is_created_on_write()
{
using var folder = new TempFolder();
string nested = Path.Combine(folder.Path, "a", "b", "settings.json");
Sta.Run(() =>
{
using SettingsService service = new(nested, folder.File("inherited.json"), SaveDelay);
_ = service.Load();
service.Save();
});
Assert.True(File.Exists(nested));
}
// Settings are not the kind of thing worth bringing the app down for
[Fact]
public void A_path_that_cannot_be_written_does_not_bring_the_app_down()
{
using var folder = new TempFolder();
// A folder sits where the settings file should be: writing there will not work
string path = folder.File("settings.json");
Directory.CreateDirectory(path);
Sta.Run(() =>
{
using SettingsService service = new(path, folder.File("inherited.json"), SaveDelay);
AppSettings settings = service.Load();
settings.FontSize = 18;
service.Save();
});
Assert.True(Directory.Exists(path));
}
[Fact]
public void Saving_without_loading_writes_nothing()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
using SettingsService service = Create(folder);
service.Save();
});
Assert.False(File.Exists(folder.File("settings.json")));
}
[Fact]
public void Closing_without_loading_passes_without_consequence()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
SettingsService service = Create(folder);
service.Dispose();
});
Assert.False(File.Exists(folder.File("settings.json")));
}
[Fact]
public void Every_setting_of_the_app_reaches_the_file()
{
using var folder = new TempFolder();
Sta.Run(() =>
{
using SettingsService service = Create(folder);
_ = service.Load();
service.Save();
});
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(folder.File("settings.json")));
List<string> stored = [.. document.RootElement.EnumerateObject().Select(property => property.Name)];
foreach (string name in AppSettingsTests.WritablePropertyNames())
{
Assert.Contains(name, stored);
}
}
// 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]
public void The_storage_place_is_chosen_on_its_own()
{
Sta.Run(() =>
{
// Nothing is read and nothing is written: only the fact that a path
// gets chosen without error is under test
using var service = new SettingsService();
});
}
private static SettingsService Create(TempFolder folder) =>
new(folder.File("settings.json"), folder.File("inherited.json"), SaveDelay);
}