Major changes across the client stack: - Settings persistence (connection, subscriptions, alarm source) - Deferred OPC UA SDK init for instant startup - Array/status code formatting, write value popup, alarm acknowledgment - Severity-colored alarm rows, condition dedup on server side - DateTimeRangePicker control with preset buttons and UTC text input - Historian queries use wwTimezone=UTC and OPCQuality column - Recursive subscribe from tree, multi-select remove - Connection panel with expander, folder chooser for cert path - Dynamic tab headers showing subscription/alarm counts - Client.UI.md documentation with headless-rendered screenshots Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
5.2 KiB
C#
171 lines
5.2 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.LmxOpcUa.Client.UI.Controls;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Screenshots;
|
|
|
|
public class DateTimeRangePickerScreenshot
|
|
{
|
|
private static readonly object Lock = new();
|
|
private static bool _initialized;
|
|
|
|
private static void EnsureInitialized()
|
|
{
|
|
lock (Lock)
|
|
{
|
|
if (_initialized) return;
|
|
_initialized = true;
|
|
|
|
AppBuilder.Configure<ScreenshotTestApp>()
|
|
.UseSkia()
|
|
.UseHeadless(new AvaloniaHeadlessPlatformOptions
|
|
{
|
|
UseHeadlessDrawing = false
|
|
})
|
|
.SetupWithoutStarting();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TextBoxes_ShowValues_WhenSetBeforeLoad()
|
|
{
|
|
EnsureInitialized();
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var picker = new DateTimeRangePicker
|
|
{
|
|
StartDateTime = new DateTimeOffset(2026, 3, 31, 8, 0, 0, TimeSpan.Zero),
|
|
EndDateTime = new DateTimeOffset(2026, 3, 31, 14, 0, 0, TimeSpan.Zero)
|
|
};
|
|
|
|
var window = new Window
|
|
{
|
|
Content = picker,
|
|
Width = 700, Height = 70
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var startInput = picker.FindControl<TextBox>("StartInput");
|
|
var endInput = picker.FindControl<TextBox>("EndInput");
|
|
|
|
startInput.ShouldNotBeNull();
|
|
endInput.ShouldNotBeNull();
|
|
startInput!.Text.ShouldBe("2026-03-31 08:00:00");
|
|
endInput!.Text.ShouldBe("2026-03-31 14:00:00");
|
|
|
|
window.Close();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void TextBoxes_ShowValues_WhenSetAfterLoad()
|
|
{
|
|
EnsureInitialized();
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var picker = new DateTimeRangePicker();
|
|
|
|
var window = new Window
|
|
{
|
|
Content = picker,
|
|
Width = 700, Height = 70
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
// Set values after the control is loaded
|
|
picker.StartDateTime = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
|
|
picker.EndDateTime = new DateTimeOffset(2026, 1, 15, 18, 45, 0, TimeSpan.Zero);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var startInput = picker.FindControl<TextBox>("StartInput");
|
|
var endInput = picker.FindControl<TextBox>("EndInput");
|
|
|
|
startInput!.Text.ShouldBe("2026-01-15 10:30:00");
|
|
endInput!.Text.ShouldBe("2026-01-15 18:45:00");
|
|
|
|
window.Close();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void PresetButtons_SetCorrectRange()
|
|
{
|
|
EnsureInitialized();
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var picker = new DateTimeRangePicker();
|
|
|
|
var window = new Window
|
|
{
|
|
Content = picker,
|
|
Width = 700, Height = 70
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
// Click the 1h preset
|
|
var lastHourBtn = picker.FindControl<Button>("LastHourBtn");
|
|
lastHourBtn.ShouldNotBeNull();
|
|
lastHourBtn!.RaiseEvent(new Avalonia.Interactivity.RoutedEventArgs(Button.ClickEvent));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
picker.StartDateTime.ShouldNotBeNull();
|
|
picker.EndDateTime.ShouldNotBeNull();
|
|
|
|
var span = picker.EndDateTime!.Value - picker.StartDateTime!.Value;
|
|
span.TotalMinutes.ShouldBe(60, 1); // ~1 hour
|
|
|
|
// Verify text boxes are populated
|
|
var startInput = picker.FindControl<TextBox>("StartInput");
|
|
var endInput = picker.FindControl<TextBox>("EndInput");
|
|
startInput!.Text.ShouldNotBeNullOrEmpty();
|
|
endInput!.Text.ShouldNotBeNullOrEmpty();
|
|
|
|
window.Close();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Capture_DateTimeRangePicker_Screenshot()
|
|
{
|
|
EnsureInitialized();
|
|
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var picker = new DateTimeRangePicker
|
|
{
|
|
StartDateTime = new DateTimeOffset(2026, 3, 31, 8, 0, 0, TimeSpan.Zero),
|
|
EndDateTime = new DateTimeOffset(2026, 3, 31, 14, 0, 0, TimeSpan.Zero)
|
|
};
|
|
|
|
var window = new Window
|
|
{
|
|
Content = new Border { Padding = new Thickness(16), Background = Brushes.White, Child = picker },
|
|
Width = 700, Height = 70, Background = Brushes.White
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var bitmap = window.CaptureRenderedFrame();
|
|
Assert.NotNull(bitmap);
|
|
|
|
var outputDir = Path.Combine(
|
|
Path.GetDirectoryName(typeof(DateTimeRangePickerScreenshot).Assembly.Location)!, "screenshots");
|
|
Directory.CreateDirectory(outputDir);
|
|
bitmap!.Save(Path.Combine(outputDir, "datetimerangepicker.png"));
|
|
|
|
window.Close();
|
|
});
|
|
}
|
|
}
|