mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.完善了时钟轻应用,为启动器提供了多语言支持
This commit is contained in:
@@ -79,6 +79,22 @@ public sealed class AirAppLauncherServiceTests
|
||||
key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSingleInstanceKey_UsesGlobalClockSuiteForWorldClock()
|
||||
{
|
||||
var analogKey = AirAppLauncherService.BuildSingleInstanceKey(
|
||||
AirAppLauncherService.WorldClockAppId,
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"analog-placement");
|
||||
var worldKey = AirAppLauncherService.BuildSingleInstanceKey(
|
||||
AirAppLauncherService.WorldClockAppId,
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"world-placement");
|
||||
|
||||
Assert.Equal("world-clock:clock-suite:global", analogKey);
|
||||
Assert.Equal(analogKey, worldKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBrokerStartInfo_UsesAirAppBrokerCommandAndRequesterPid()
|
||||
{
|
||||
|
||||
176
LanMountainDesktop.Tests/ClockAirAppMvpTests.cs
Normal file
176
LanMountainDesktop.Tests/ClockAirAppMvpTests.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
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)}'.");
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,32 @@ public sealed class LauncherAirAppLifecycleServiceTests
|
||||
Assert.Equal(first.Instance!.InstanceKey, second.Instance!.InstanceKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsync_ReusesGlobalClockSuiteAcrossClockComponents()
|
||||
{
|
||||
var starter = new TestAirAppProcessStarter(Process.GetCurrentProcess());
|
||||
var service = new LauncherAirAppLifecycleService(starter);
|
||||
|
||||
var first = await service.OpenAsync(new AirAppOpenRequest(
|
||||
"world-clock",
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"analog-placement",
|
||||
Environment.ProcessId));
|
||||
var second = await service.OpenAsync(new AirAppOpenRequest(
|
||||
"world-clock",
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"world-placement",
|
||||
Environment.ProcessId));
|
||||
|
||||
Assert.True(first.Accepted);
|
||||
Assert.True(second.Accepted);
|
||||
Assert.Equal("started", first.Code);
|
||||
Assert.Equal("activated_existing", second.Code);
|
||||
Assert.Equal("world-clock:clock-suite:global", first.Instance!.InstanceKey);
|
||||
Assert.Equal(first.Instance.InstanceKey, second.Instance!.InstanceKey);
|
||||
Assert.Equal(1, starter.StartCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsync_PrunesExitedRegisteredInstanceBeforeRestart()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,22 @@ public sealed class WindowLayerIsolationTests
|
||||
Assert.DoesNotContain("Topmost=true", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindow_UsesFluentAvaloniaChromeInsteadOfHandRolledTitleBar()
|
||||
{
|
||||
var xaml = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml");
|
||||
var source = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml.cs");
|
||||
|
||||
Assert.Contains("<faWindowing:FAAppWindow", xaml);
|
||||
Assert.Contains(": FAAppWindow", source);
|
||||
Assert.Contains("ShowAsDialog", source);
|
||||
Assert.Contains("TitleBar.ExtendsContentIntoTitleBar", source);
|
||||
Assert.DoesNotContain("OnTitleBarPointerPressed", source);
|
||||
Assert.DoesNotContain("BeginMoveDrag", source);
|
||||
Assert.DoesNotContain("OnCloseClick", source);
|
||||
Assert.DoesNotContain("PointerPressed=\"OnTitleBarPointerPressed\"", xaml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindowDescriptor_DefinesSupportedChromeModes()
|
||||
{
|
||||
@@ -36,12 +52,29 @@ public sealed class WindowLayerIsolationTests
|
||||
|
||||
Assert.Contains("AirAppLaunchOptions.WorldClockAppId", source);
|
||||
Assert.Contains("AirAppWindowChromeMode.Standard", source);
|
||||
Assert.Contains("width: 360", source);
|
||||
Assert.Contains("height: 220", source);
|
||||
Assert.Contains("width: 780", source);
|
||||
Assert.Contains("height: 560", source);
|
||||
Assert.Contains("minWidth: 680", source);
|
||||
Assert.Contains("minHeight: 480", source);
|
||||
Assert.Contains("canResize: true", source);
|
||||
Assert.Contains("showAsDialog: false", source);
|
||||
Assert.Contains("AirAppLaunchOptions.WhiteboardAppId", source);
|
||||
Assert.Contains("AirAppWindowChromeMode.FullScreen", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirAppWindow_LoadsClockSuiteForWorldClockApp()
|
||||
{
|
||||
var source = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "AirAppWindow.axaml.cs");
|
||||
var viewXaml = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "ClockAirAppView.axaml");
|
||||
var projectFile = ReadRepositoryFile("LanMountainDesktop.AirAppHost", "LanMountainDesktop.AirAppHost.csproj");
|
||||
|
||||
Assert.Contains("new ClockAirAppView(_options)", source);
|
||||
Assert.Contains("clock-suite:global", source);
|
||||
Assert.Contains("ClockAirAppView", viewXaml);
|
||||
Assert.Contains("Localization\\*.json", projectFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DesktopComponentHost_DoesNotInterceptLivePointerInputForAirApps()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user