380 lines
14 KiB
C#
380 lines
14 KiB
C#
using System.Reflection;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Threading;
|
|
|
|
namespace Zcbot.WindowsNode.Tests;
|
|
|
|
public sealed class PresentationCommandTests
|
|
{
|
|
[Fact]
|
|
public void MainWindow_BindingsKeepOnlyTheSelectedPageVisible()
|
|
{
|
|
Exception? failure = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
var application = new System.Windows.Application
|
|
{
|
|
ShutdownMode = ShutdownMode.OnExplicitShutdown,
|
|
};
|
|
application.Resources.MergedDictionaries.Add(new ResourceDictionary
|
|
{
|
|
Source = new Uri(
|
|
"pack://application:,,,/Zcbot.WindowsNode;component/Presentation/Themes/LightTheme.xaml",
|
|
UriKind.Absolute),
|
|
});
|
|
using var viewModel = CreateViewModel();
|
|
var window = new MainWindow(viewModel);
|
|
window.Measure(new Size(1040, 720));
|
|
window.Arrange(new Rect(0, 0, 1040, 720));
|
|
window.Dispatcher.Invoke(() => { }, DispatcherPriority.DataBind);
|
|
|
|
Assert.Equal(Visibility.Visible, FindPage(window, "OverviewPage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "SoftwarePage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "JobsPage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "SettingsPage").Visibility);
|
|
|
|
viewModel.ShowJobsCommand.Execute(null);
|
|
window.Dispatcher.Invoke(() => { }, DispatcherPriority.DataBind);
|
|
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "OverviewPage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "SoftwarePage").Visibility);
|
|
Assert.Equal(Visibility.Visible, FindPage(window, "JobsPage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "SettingsPage").Visibility);
|
|
Assert.Equal(
|
|
"本机任务",
|
|
FindElement<TextBlock>(window, "PageTitleText").Text);
|
|
|
|
viewModel.ShowSettingsCommand.Execute(null);
|
|
window.Dispatcher.Invoke(() => { }, DispatcherPriority.DataBind);
|
|
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "OverviewPage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "SoftwarePage").Visibility);
|
|
Assert.Equal(Visibility.Collapsed, FindPage(window, "JobsPage").Visibility);
|
|
Assert.Equal(Visibility.Visible, FindPage(window, "SettingsPage").Visibility);
|
|
Assert.Equal(
|
|
"运行设置",
|
|
FindElement<TextBlock>(window, "PageTitleText").Text);
|
|
Assert.Equal(
|
|
@"C:\node-data",
|
|
FindElement<TextBox>(window, "DataRootText").Text);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
failure = exception;
|
|
}
|
|
});
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
|
|
Assert.True(thread.Join(TimeSpan.FromSeconds(5)), "WPF binding smoke test timed out.");
|
|
if (failure is not null) throw failure;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(typeof(MainWindowViewModel), "PageTitle")]
|
|
[InlineData(typeof(MainWindowViewModel), "StatusText")]
|
|
[InlineData(typeof(MainWindowViewModel), "IsOverviewPage")]
|
|
[InlineData(typeof(MainWindowViewModel), "IsSoftwarePage")]
|
|
[InlineData(typeof(MainWindowViewModel), "IsJobsPage")]
|
|
[InlineData(typeof(MainWindowViewModel), "IsSettingsPage")]
|
|
[InlineData(typeof(MainWindowViewModel), "ShowOverviewCommand")]
|
|
[InlineData(typeof(MainWindowViewModel), "ShowSoftwareCommand")]
|
|
[InlineData(typeof(MainWindowViewModel), "ShowJobsCommand")]
|
|
[InlineData(typeof(MainWindowViewModel), "ShowSettingsCommand")]
|
|
[InlineData(typeof(MainWindowViewModel), "SoftwarePage")]
|
|
[InlineData(typeof(MainWindowViewModel), "JobPage")]
|
|
[InlineData(typeof(MainWindowViewModel), "AcceptancePage")]
|
|
[InlineData(typeof(MainWindowViewModel), "DataRootPage")]
|
|
[InlineData(typeof(MainWindowViewModel), "CopyDiagnosticsCommand")]
|
|
[InlineData(typeof(SoftwarePageViewModel), "Cards")]
|
|
[InlineData(typeof(SoftwarePageViewModel), "Message")]
|
|
[InlineData(typeof(SoftwareCardViewModel), "DisplayName")]
|
|
[InlineData(typeof(SoftwareCardViewModel), "LocationText")]
|
|
[InlineData(typeof(SoftwareCardViewModel), "InstallRuntimeCommand")]
|
|
[InlineData(typeof(JobPageViewModel), "Jobs")]
|
|
[InlineData(typeof(JobPageViewModel), "SelectedJob")]
|
|
[InlineData(typeof(JobPageViewModel), "Summary")]
|
|
[InlineData(typeof(JobPageViewModel), "RefreshCommand")]
|
|
[InlineData(typeof(LocalJobItemViewModel), "StageText")]
|
|
[InlineData(typeof(LocalJobItemViewModel), "DetailText")]
|
|
[InlineData(typeof(AnsysAcceptanceViewModel), "IsAvailable")]
|
|
[InlineData(typeof(AnsysAcceptanceViewModel), "Message")]
|
|
[InlineData(typeof(AnsysAcceptanceViewModel), "RunCommand")]
|
|
[InlineData(typeof(AnsysAcceptanceViewModel), "EnableGateCommand")]
|
|
[InlineData(typeof(DataRootPageViewModel), "CurrentRoot")]
|
|
[InlineData(typeof(DataRootPageViewModel), "Status")]
|
|
[InlineData(typeof(DataRootPageViewModel), "ChangeCommand")]
|
|
[InlineData(typeof(DataRootPageViewModel), "OpenCommand")]
|
|
public void WpfBindingSources_ArePublicProperties(Type type, string propertyName)
|
|
{
|
|
var property = type.GetProperty(propertyName);
|
|
|
|
Assert.NotNull(property);
|
|
Assert.True(property!.GetMethod?.IsPublic);
|
|
}
|
|
|
|
[Fact]
|
|
public void MainWindowViewModel_MapsStatusAndDisablesExitDuringShutdown()
|
|
{
|
|
var viewModel = CreateViewModel();
|
|
|
|
var config = new NodeConfig(
|
|
new Uri("https://zcbot.example/"),
|
|
Guid.NewGuid(),
|
|
Guid.NewGuid(),
|
|
"lab-node",
|
|
"secret",
|
|
30,
|
|
["origin.plot@v2"]);
|
|
viewModel.ApplyStatus(NodeStatus.Create(NodeState.Online, "已连接"), config);
|
|
|
|
Assert.Equal("节点在线", viewModel.StatusText);
|
|
Assert.Equal("success", viewModel.StatusTone);
|
|
Assert.StartsWith("已连接", viewModel.StatusDetail);
|
|
Assert.True(viewModel.IsRegistered);
|
|
Assert.Contains("lab-node", viewModel.IdentityText);
|
|
Assert.True(viewModel.HideCommand.CanExecute(null));
|
|
Assert.True(viewModel.ExitCommand.CanExecute(null));
|
|
|
|
viewModel.BeginExit();
|
|
|
|
Assert.True(viewModel.IsExiting);
|
|
Assert.Equal("正在退出", viewModel.StatusText);
|
|
Assert.Equal("neutral", viewModel.StatusTone);
|
|
Assert.False(viewModel.HideCommand.CanExecute(null));
|
|
Assert.False(viewModel.ExitCommand.CanExecute(null));
|
|
}
|
|
|
|
[Fact]
|
|
public void MainWindowViewModel_ForwardsShellCommands()
|
|
{
|
|
var viewModel = CreateViewModel();
|
|
var copied = false;
|
|
var hidden = false;
|
|
viewModel.ApplyStatus(NodeStatus.Create(NodeState.Online, "online"), CreateConfig());
|
|
viewModel.CopyDiagnosticsRequested += text => copied = text == "diagnostic";
|
|
viewModel.HideRequested += () => hidden = true;
|
|
|
|
viewModel.CopyDiagnosticsCommand.Execute(null);
|
|
viewModel.HideCommand.Execute(null);
|
|
|
|
Assert.True(copied);
|
|
Assert.True(hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void MainWindowViewModel_NavigatesBetweenMigratedPages()
|
|
{
|
|
var viewModel = CreateViewModel();
|
|
|
|
viewModel.ShowSettingsCommand.Execute(null);
|
|
|
|
Assert.True(viewModel.IsSettingsPage);
|
|
Assert.Equal("运行设置", viewModel.PageTitle);
|
|
|
|
viewModel.ShowJobsCommand.Execute(null);
|
|
|
|
Assert.True(viewModel.IsJobsPage);
|
|
Assert.Equal("本机任务", viewModel.PageTitle);
|
|
|
|
viewModel.ShowOverviewCommand.Execute(null);
|
|
|
|
Assert.True(viewModel.IsOverviewPage);
|
|
Assert.Equal("节点概览", viewModel.PageTitle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MainWindowViewModel_ValidatesAndForwardsRegistration()
|
|
{
|
|
var viewModel = CreateViewModel();
|
|
var received = new TaskCompletionSource<EnrollOptions>(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
viewModel.RegisterRequested += options =>
|
|
{
|
|
received.SetResult(options);
|
|
return Task.CompletedTask;
|
|
};
|
|
viewModel.ServerUrl = "https://zcbot.example/base";
|
|
viewModel.NodeName = "lab-node";
|
|
viewModel.EnrollmentCode = "ZCN-123456789012";
|
|
|
|
Assert.True(viewModel.RegisterCommand.CanExecute(null));
|
|
viewModel.RegisterCommand.Execute(null);
|
|
|
|
var options = await received.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
|
await WaitUntilAsync(() => !viewModel.IsBusy);
|
|
Assert.Equal("https://zcbot.example/base/", options.ServerUrl.AbsoluteUri);
|
|
Assert.Equal("lab-node", options.NodeName);
|
|
Assert.Equal("ZCN-123456789012", options.EnrollmentCode);
|
|
Assert.Equal("注册成功,正在连接节点", viewModel.OperationMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void MainWindowViewModel_AppliesAndRollsBackStartupSetting()
|
|
{
|
|
var startup = new FakeStartupRegistration();
|
|
var viewModel = CreateViewModel(startup);
|
|
|
|
viewModel.IsStartupEnabled = true;
|
|
|
|
Assert.True(startup.IsEnabled);
|
|
Assert.True(viewModel.IsStartupEnabled);
|
|
Assert.True(viewModel.HasOperationMessage);
|
|
|
|
startup.Failure = new UnauthorizedAccessException("denied");
|
|
viewModel.IsStartupEnabled = false;
|
|
|
|
Assert.True(viewModel.IsStartupEnabled);
|
|
Assert.Contains("denied", viewModel.OperationMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AsyncCommand_PreventsReentryAndRestoresAvailability()
|
|
{
|
|
var release = new TaskCompletionSource(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var finished = new TaskCompletionSource(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var callCount = 0;
|
|
var command = new AsyncCommand(async () =>
|
|
{
|
|
callCount++;
|
|
await release.Task;
|
|
finished.SetResult();
|
|
});
|
|
|
|
command.Execute(null);
|
|
command.Execute(null);
|
|
|
|
Assert.Equal(1, callCount);
|
|
Assert.True(command.IsRunning);
|
|
Assert.False(command.CanExecute(null));
|
|
|
|
release.SetResult();
|
|
await finished.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
|
await WaitUntilAsync(() => !command.IsRunning);
|
|
|
|
Assert.True(command.CanExecute(null));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AsyncCommand_ReportsExecutionFailure()
|
|
{
|
|
var failure = new InvalidOperationException("failed");
|
|
var reported = new TaskCompletionSource<Exception>(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var command = new AsyncCommand(() => Task.FromException(failure));
|
|
command.Failed += reported.SetResult;
|
|
|
|
command.Execute(null);
|
|
|
|
Assert.Same(failure, await reported.Task.WaitAsync(TimeSpan.FromSeconds(2)));
|
|
await WaitUntilAsync(() => !command.IsRunning);
|
|
Assert.True(command.CanExecute(null));
|
|
}
|
|
|
|
private static async Task WaitUntilAsync(Func<bool> condition)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddSeconds(2);
|
|
while (!condition() && DateTime.UtcNow < deadline)
|
|
{
|
|
await Task.Delay(10);
|
|
}
|
|
|
|
Assert.True(condition());
|
|
}
|
|
|
|
private static FrameworkElement FindPage(MainWindow window, string name) =>
|
|
FindElement<FrameworkElement>(window, name);
|
|
|
|
private static T FindElement<T>(MainWindow window, string name)
|
|
where T : FrameworkElement =>
|
|
Assert.IsAssignableFrom<T>(window.FindName(name));
|
|
|
|
private static MainWindowViewModel CreateViewModel(
|
|
FakeStartupRegistration? startup = null) =>
|
|
new(
|
|
startup ?? new FakeStartupRegistration(),
|
|
new FakeDiagnosticService(),
|
|
new SoftwarePageViewModel(new EmptySoftwareManagementService()),
|
|
new JobPageViewModel(new EmptyJobMonitorService()),
|
|
new AnsysAcceptanceViewModel(new EmptyAnsysAcceptanceService()),
|
|
new DataRootPageViewModel(new EmptyDataRootManagementService()));
|
|
|
|
private static NodeConfig CreateConfig() => new(
|
|
new Uri("https://zcbot.example/"),
|
|
Guid.NewGuid(),
|
|
Guid.NewGuid(),
|
|
"lab-node",
|
|
"secret",
|
|
30,
|
|
["origin.plot@v2"]);
|
|
|
|
private sealed class FakeStartupRegistration : IStartupRegistration
|
|
{
|
|
internal Exception? Failure { get; set; }
|
|
|
|
public bool IsEnabled { get; private set; }
|
|
|
|
public void SetEnabled(bool enabled)
|
|
{
|
|
if (Failure is not null) throw Failure;
|
|
IsEnabled = enabled;
|
|
}
|
|
}
|
|
|
|
private sealed class FakeDiagnosticService : IDiagnosticService
|
|
{
|
|
public string Build(NodeConfig config) => "diagnostic";
|
|
}
|
|
|
|
private sealed class EmptySoftwareManagementService : ISoftwareManagementService
|
|
{
|
|
public Task<IReadOnlyList<SoftwareManagementSnapshot>> RefreshAsync(
|
|
CancellationToken cancellationToken) =>
|
|
Task.FromResult<IReadOnlyList<SoftwareManagementSnapshot>>([]);
|
|
|
|
public void SaveLocation(string softwareId, string path) { }
|
|
|
|
public void ClearLocation(string softwareId) { }
|
|
|
|
public Task<RuntimeInstallResult> InstallRuntimeAsync(
|
|
string softwareId,
|
|
IProgress<string> progress,
|
|
CancellationToken cancellationToken) =>
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
private sealed class EmptyJobMonitorService : IJobMonitorService
|
|
{
|
|
public IReadOnlyList<JobDisplaySnapshot> ReadSnapshots(int limit = 50) => [];
|
|
}
|
|
|
|
private sealed class EmptyAnsysAcceptanceService : IAnsysAcceptanceService
|
|
{
|
|
public bool IsAvailable => false;
|
|
public bool IsGateEnabled => false;
|
|
|
|
public Task<AdapterAcceptanceResult> RunAsync(
|
|
string workRoot,
|
|
IProgress<string> progress,
|
|
CancellationToken cancellationToken) =>
|
|
throw new NotSupportedException();
|
|
|
|
public void EnableGate(string reportPath) => throw new NotSupportedException();
|
|
}
|
|
|
|
private sealed class EmptyDataRootManagementService : IDataRootManagementService
|
|
{
|
|
public NodeDataRootSelection ReadSelection() =>
|
|
new(@"C:\node-data", IsEnvironmentManaged: false, "default");
|
|
|
|
public bool HasPendingJobs => false;
|
|
|
|
public string Normalize(string path) => path;
|
|
}
|
|
}
|