namespace Zcbot.WindowsNode.Tests; public sealed class SoftwarePageViewModelTests { [Fact] public async Task Refresh_PopulatesSoftwareCardsAndExecutionState() { var service = new FakeSoftwareManagementService( Snapshot("origin", "Origin", available: true), Snapshot("ansys", "ANSYS", available: false, active: true)); using var viewModel = new SoftwarePageViewModel(service); await viewModel.RefreshAsync(); Assert.Equal(2, viewModel.Cards.Count); Assert.True(viewModel.Cards[0].InstallRuntimeCommand.CanExecute(null)); Assert.False(viewModel.Cards[1].InstallRuntimeCommand.CanExecute(null)); Assert.Equal("检测完成", viewModel.Message); } [Fact] public async Task ChooseAndClearLocation_UseSoftwareServiceThenRefresh() { var service = new FakeSoftwareManagementService( Snapshot("origin", "Origin", available: false)); using var viewModel = new SoftwarePageViewModel(service); viewModel.ChooseLocationRequested += (_, _, _) => @"C:\Origin\Origin.exe"; await viewModel.RefreshAsync(); var card = Assert.Single(viewModel.Cards); card.ChooseLocationCommand.Execute(null); await WaitUntilAsync(() => service.SavedPath is not null); await WaitUntilAsync(() => !viewModel.IsBusy); Assert.Equal(("origin", @"C:\Origin\Origin.exe"), service.SavedPath); Assert.Equal("应用位置已保存", viewModel.Message); card.ClearLocationCommand.Execute(null); await WaitUntilAsync(() => service.ClearedId is not null); await WaitUntilAsync(() => !viewModel.IsBusy); Assert.Equal("origin", service.ClearedId); } [Fact] public async Task InstallRuntime_RequiresConfirmationAndReportsResult() { var service = new FakeSoftwareManagementService( Snapshot("blender", "Blender", available: true)); using var viewModel = new SoftwarePageViewModel(service); viewModel.ConfirmInstallRequested += _ => true; await viewModel.RefreshAsync(); viewModel.Cards[0].InstallRuntimeCommand.Execute(null); await WaitUntilAsync(() => service.InstallCount == 1); await WaitUntilAsync(() => !viewModel.IsBusy); Assert.Contains("安装完成", viewModel.Message); Assert.Equal("blender", service.InstalledId); } [Fact] public async Task CancelInstall_CancelsTheInFlightRuntimeOperation() { var service = new FakeSoftwareManagementService( Snapshot("origin", "Origin", available: true)) { BlockInstall = true, }; using var viewModel = new SoftwarePageViewModel(service); viewModel.ConfirmInstallRequested += _ => true; await viewModel.RefreshAsync(); viewModel.Cards[0].InstallRuntimeCommand.Execute(null); await service.InstallStarted.Task.WaitAsync(TimeSpan.FromSeconds(2)); Assert.True(viewModel.CancelInstallCommand.CanExecute(null)); viewModel.CancelInstallCommand.Execute(null); await WaitUntilAsync(() => !viewModel.IsBusy); Assert.Equal("安装已取消,原运行环境保持不变。", viewModel.Message); } private static SoftwareManagementSnapshot Snapshot( string id, string displayName, bool available, bool active = false) => new( id, displayName, SoftwarePathKind.Executable, "*.exe", available, available ? $@"C:\{displayName}\app.exe" : "未检测到应用位置", available ? "运行环境已安装" : "运行环境尚未安装", RuntimeInstalled: available, HasActiveJobs: active); private static async Task WaitUntilAsync(Func condition) { var deadline = DateTime.UtcNow.AddSeconds(2); while (!condition() && DateTime.UtcNow < deadline) { await Task.Delay(10); } Assert.True(condition()); } private sealed class FakeSoftwareManagementService( params SoftwareManagementSnapshot[] snapshots) : ISoftwareManagementService { internal (string Id, string Path)? SavedPath { get; private set; } internal string? ClearedId { get; private set; } internal string? InstalledId { get; private set; } internal int InstallCount { get; private set; } internal bool BlockInstall { get; init; } internal TaskCompletionSource InstallStarted { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public Task> RefreshAsync( CancellationToken cancellationToken) => Task.FromResult>(snapshots); public void SaveLocation(string softwareId, string path) => SavedPath = (softwareId, path); public void ClearLocation(string softwareId) => ClearedId = softwareId; public async Task InstallRuntimeAsync( string softwareId, IProgress progress, CancellationToken cancellationToken) { InstallCount++; InstalledId = softwareId; InstallStarted.TrySetResult(); progress.Report("正在安装…"); if (BlockInstall) { await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); } return new RuntimeInstallResult(@"C:\runtime\python.exe", "安装完成"); } } }