mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
177 lines
6.3 KiB
C#
177 lines
6.3 KiB
C#
|
|
using System.Globalization;
|
||
|
|
using System.Text.Json;
|
||
|
|
using LanMountainDesktop.Services.ClockAirApp;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace LanMountainDesktop.Tests;
|
||
|
|
|
||
|
|
public sealed class ClockAirAppMvpTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void SettingsSnapshot_DefaultsMatchClockSuiteMvp()
|
||
|
|
{
|
||
|
|
var snapshot = ClockAirAppSettingsSnapshot.Normalize(null);
|
||
|
|
|
||
|
|
Assert.Equal(ClockAirAppTimeFormatMode.System, snapshot.TimeFormatMode);
|
||
|
|
Assert.True(snapshot.ShowSeconds);
|
||
|
|
Assert.Equal(ClockAirAppTabIds.Last, snapshot.StartupTab);
|
||
|
|
Assert.Equal(ClockAirAppTabIds.WorldClock, snapshot.LastSelectedTab);
|
||
|
|
Assert.True(snapshot.ActivateOnTimerFinished);
|
||
|
|
Assert.Equal(4, snapshot.WorldClockTimeZoneIds.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SettingsStore_LoadsDefaultsWhenJsonIsBroken()
|
||
|
|
{
|
||
|
|
var directory = CreateTempDirectory();
|
||
|
|
var path = Path.Combine(directory, "settings.json");
|
||
|
|
File.WriteAllText(path, "{ broken json");
|
||
|
|
|
||
|
|
var store = new ClockAirAppSettingsStore(path);
|
||
|
|
var snapshot = store.Load();
|
||
|
|
|
||
|
|
Assert.Equal(ClockAirAppTimeFormatMode.System, snapshot.TimeFormatMode);
|
||
|
|
Assert.Equal(4, snapshot.WorldClockTimeZoneIds.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void SettingsStore_SavesAndLoadsIndependentClockSettings()
|
||
|
|
{
|
||
|
|
var directory = CreateTempDirectory();
|
||
|
|
var path = Path.Combine(directory, "settings.json");
|
||
|
|
var store = new ClockAirAppSettingsStore(path);
|
||
|
|
|
||
|
|
store.Save(new ClockAirAppSettingsSnapshot
|
||
|
|
{
|
||
|
|
TimeFormatMode = ClockAirAppTimeFormatMode.TwelveHour,
|
||
|
|
ShowSeconds = false,
|
||
|
|
StartupTab = ClockAirAppTabIds.Timer,
|
||
|
|
LastSelectedTab = ClockAirAppTabIds.Stopwatch,
|
||
|
|
ActivateOnTimerFinished = false,
|
||
|
|
WorldClockTimeZoneIds = ["UTC"]
|
||
|
|
});
|
||
|
|
|
||
|
|
var loaded = store.Load();
|
||
|
|
Assert.Equal(ClockAirAppTimeFormatMode.TwelveHour, loaded.TimeFormatMode);
|
||
|
|
Assert.False(loaded.ShowSeconds);
|
||
|
|
Assert.Equal(ClockAirAppTabIds.Timer, loaded.StartupTab);
|
||
|
|
Assert.Equal(ClockAirAppTabIds.Stopwatch, loaded.LastSelectedTab);
|
||
|
|
Assert.False(loaded.ActivateOnTimerFinished);
|
||
|
|
Assert.Single(loaded.WorldClockTimeZoneIds);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void TimeFormatter_FormatsTimeAndOffsets()
|
||
|
|
{
|
||
|
|
var time = new DateTime(2026, 5, 18, 21, 7, 9);
|
||
|
|
var settings = new ClockAirAppSettingsSnapshot
|
||
|
|
{
|
||
|
|
TimeFormatMode = ClockAirAppTimeFormatMode.TwentyFourHour,
|
||
|
|
ShowSeconds = true
|
||
|
|
};
|
||
|
|
|
||
|
|
Assert.Equal("21:07:09", ClockAirAppTimeFormatter.FormatTime(time, settings, CultureInfo.GetCultureInfo("en-US")));
|
||
|
|
Assert.Equal("UTC+08:30", ClockAirAppTimeFormatter.FormatUtcOffset(TimeSpan.FromMinutes(510)));
|
||
|
|
Assert.Equal("UTC-05:00", ClockAirAppTimeFormatter.FormatUtcOffset(TimeSpan.FromHours(-5)));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void StopwatchState_StartPauseLapAndReset()
|
||
|
|
{
|
||
|
|
var state = new ClockAirAppStopwatchState();
|
||
|
|
var start = DateTimeOffset.Parse("2026-05-18T12:00:00Z", CultureInfo.InvariantCulture);
|
||
|
|
|
||
|
|
state.StartOrResume(start);
|
||
|
|
Assert.True(state.IsRunning);
|
||
|
|
Assert.Equal(TimeSpan.FromSeconds(5), state.GetElapsed(start.AddSeconds(5)));
|
||
|
|
|
||
|
|
var lap = state.AddLap(start.AddSeconds(6));
|
||
|
|
Assert.Equal(TimeSpan.FromSeconds(6), lap);
|
||
|
|
Assert.Single(state.Laps);
|
||
|
|
|
||
|
|
state.Pause(start.AddSeconds(8));
|
||
|
|
Assert.False(state.IsRunning);
|
||
|
|
Assert.Equal(TimeSpan.FromSeconds(8), state.GetElapsed(start.AddSeconds(20)));
|
||
|
|
|
||
|
|
state.Reset();
|
||
|
|
Assert.Equal(TimeSpan.Zero, state.GetElapsed(start.AddSeconds(30)));
|
||
|
|
Assert.Empty(state.Laps);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void TimerState_StartPauseAndComplete()
|
||
|
|
{
|
||
|
|
var state = new ClockAirAppTimerState();
|
||
|
|
var start = DateTimeOffset.Parse("2026-05-18T12:00:00Z", CultureInfo.InvariantCulture);
|
||
|
|
|
||
|
|
state.SetDuration(TimeSpan.FromSeconds(10));
|
||
|
|
state.StartOrResume(start);
|
||
|
|
Assert.True(state.IsRunning);
|
||
|
|
Assert.Equal(TimeSpan.FromSeconds(6), state.GetRemaining(start.AddSeconds(4)));
|
||
|
|
|
||
|
|
state.Pause(start.AddSeconds(4));
|
||
|
|
Assert.False(state.IsRunning);
|
||
|
|
Assert.Equal(TimeSpan.FromSeconds(6), state.GetRemaining(start.AddSeconds(20)));
|
||
|
|
|
||
|
|
state.StartOrResume(start.AddSeconds(20));
|
||
|
|
Assert.False(state.Update(start.AddSeconds(25)));
|
||
|
|
Assert.True(state.Update(start.AddSeconds(26)));
|
||
|
|
Assert.True(state.IsCompleted);
|
||
|
|
Assert.Equal(TimeSpan.Zero, state.GetRemaining(start.AddSeconds(26)));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void LocalizationFiles_ContainClockAirAppKeys()
|
||
|
|
{
|
||
|
|
var requiredKeys = new[]
|
||
|
|
{
|
||
|
|
"clockairapp.title",
|
||
|
|
"clockairapp.tab.world",
|
||
|
|
"clockairapp.tab.stopwatch",
|
||
|
|
"clockairapp.tab.timer",
|
||
|
|
"clockairapp.tab.settings",
|
||
|
|
"clockairapp.settings.time_format.24h"
|
||
|
|
};
|
||
|
|
|
||
|
|
foreach (var language in new[] { "zh-CN", "en-US", "ja-JP", "ko-KR" })
|
||
|
|
{
|
||
|
|
var json = ReadRepositoryFile("LanMountainDesktop", "Localization", $"{language}.json");
|
||
|
|
var table = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||
|
|
Assert.NotNull(table);
|
||
|
|
foreach (var key in requiredKeys)
|
||
|
|
{
|
||
|
|
Assert.True(table!.ContainsKey(key), $"{language} is missing {key}.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string CreateTempDirectory()
|
||
|
|
{
|
||
|
|
var directory = Path.Combine(Path.GetTempPath(), "LanMountainDesktop.Tests", Guid.NewGuid().ToString("N"));
|
||
|
|
Directory.CreateDirectory(directory);
|
||
|
|
return directory;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string ReadRepositoryFile(params string[] segments)
|
||
|
|
{
|
||
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||
|
|
while (directory is not null)
|
||
|
|
{
|
||
|
|
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
|
||
|
|
if (File.Exists(candidate))
|
||
|
|
{
|
||
|
|
return File.ReadAllText(candidate);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
directory = directory.Parent;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new FileNotFoundException($"Could not locate repository file '{Path.Combine(segments)}'.");
|
||
|
|
}
|
||
|
|
}
|