From ad78fb996ed447bcbe64da651e01c94d6957c29d Mon Sep 17 00:00:00 2001 From: Zhang Dian <54255897+zdpcdt@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:33:32 +0800 Subject: [PATCH] docs: transport headless docs. --- .../current.json | 4 + .../concepts/headless/headless-custom.md | 81 +++++++++ .../concepts/headless/headless-nunit.md | 91 ++++++++++ .../concepts/headless/headless-xunit.md | 91 ++++++++++ .../current/concepts/headless/index.md | 163 ++++++++++++++++++ 5 files changed, 430 insertions(+) create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-custom.md create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-nunit.md create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-xunit.md create mode 100644 i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/index.md diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json b/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json index c4b878a40..b81756729 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current.json @@ -119,6 +119,10 @@ "message": "数据模板", "description": "The label for category Data Templates in sidebar documentationSidebar" }, + "sidebar.documentationSidebar.category.Headless": { + "message": "Headless", + "description": "The label for category Headless Custom in sidebar documentationSidebar" + }, "sidebar.documentationSidebar.category.Input": { "message": "输入", "description": "The label for category Input in sidebar documentationSidebar" diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-custom.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-custom.md new file mode 100644 index 000000000..9e9dcd84d --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-custom.md @@ -0,0 +1,81 @@ +--- +id: headless-custom +title: Manual Setup of Headless Platform +--- + +:::warning +This page explains an advanced usage scenario with the Headless platform. +We recommend using the [XUnit](headless-xunit.md) or [NUnit](headless-nunit.md) testing frameworks instead. +::: + +## Install Packages + +To set up the Headless platform, you need to install two packages: +- [Avalonia.Headless](https://www.nuget.org/packages/Avalonia.Headless), which also includes Avalonia. +- [Avalonia.Themes.Fluent](https://www.nuget.org/packages/Avalonia.Themes.Fluent), as even headless controls need a theme. + +:::tip +The Headless platform doesn't require any specific theme, and it is possible to swap FluentTheme with any other. +::: + +## Setup Application + +As in any other Avalonia app, an `Application` instance needs to be created, and themes need to be applied. When using the Headless platform, the setup is not much different from a regular Avalonia app and can mostly be reused. + +```xml title=App.axaml + + + + + +``` + +And the code: + +```csharp title=App.axaml.cs +using Avalonia; +using Avalonia.Headless; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } +} +``` + +## Run Headless Session + +```csharp title=Program.cs +using Avalonia.Controls; +using Avalonia.Headless; + +// Start Headless session passing Application type. +using var session = HeadlessUnitTestSession.StartNew(typeof(App)); + +// Since the Headless session has its own thread internally, we need to dispatch actions there: +await session.Dispatch(() => +{ + // Setup controls: + var textBox = new TextBox(); + var window = new Window { Content = textBox }; + + // Open window: + window.Show(); + + // Focus text box: + textBox.Focus(); + + // Simulate text input: + window.KeyTextInput("Hello World"); + + // Assert: + if (textBox.Text != "Hello World") + { + throw new Exception("Assert"); + } +}, CancellationToken.None); +``` \ No newline at end of file diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-nunit.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-nunit.md new file mode 100644 index 000000000..e534218b6 --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-nunit.md @@ -0,0 +1,91 @@ +--- +id: headless-nunit +title: Headless Testing with NUnit +--- + +## Preparation + +This page assumes that NUnit project was already created. +If not, please follow NUnit "Getting Started" and "Installation" here https://docs.nunit.org/articles/nunit/getting-started/installation.html. + +## Install packages + +Aside from NUnit packages, we need to install two more packages: +- [Avalonia.Headless.NUnit](https://www.nuget.org/packages/Avalonia.Headless.NUnit) which also includes Avalonia. +- [Avalonia.Themes.Fluent](https://www.nuget.org/packages/Avalonia.Themes.Fluent) as even headless controls need a theme + +:::tip +Headless platform doesn't require any specific theme, and it is possible to swap FluentTheme with any other. +::: + +## Setup Application +As in any other Avalonia app, an `Application` instance needs to be created, and themes need to be applied. When using the Headless platform, the setup is not much different from a regular Avalonia app and can mostly be reused. + +```xml title=App.axaml + + + + + +``` + +And the code: + +```csharp title=App.axaml.cs +using Avalonia; +using Avalonia.Headless; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } +} +``` + +:::note +Usually, the `BuildAvaloniaApp` method is defined in the Program.cs file, but NUnit/XUnit tests don't have it, so it is defined in the `App` file instead. +::: + +## Initialize NUnit Tests + +The `[AvaloniaTestApplication]` attribute wires the tests in the current project with the specific application. It needs to be defined once per project in any file. + +```csharp +[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] + +public class TestAppBuilder +{ + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()); +} +``` + +## Test Example + +```csharp +[AvaloniaTest] +public void Should_Type_Text_Into_TextBox() +{ + // Setup controls: + var textBox = new TextBox(); + var window = new Window { Content = textBox }; + + // Open window: + window.Show(); + + // Focus text box: + textBox.Focus(); + + // Simulate text input: + window.KeyTextInput("Hello World"); + + // Assert: + Assert.AreEqual("Hello World", textBox.Text); +} +``` + +Instead of the typical `[Test]` attribute, we need to use `[AvaloniaTest]` as it sets up the UI thread. Similarly, instead of `[Theory]`, there is a `[AvaloniaTheory]` attribute. diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-xunit.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-xunit.md new file mode 100644 index 000000000..07e4859f9 --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/headless-xunit.md @@ -0,0 +1,91 @@ +--- +id: headless-xunit +title: Headless Testing with XUnit +--- + +## Preparation + +This page assumes that XUnit project was already created. +If not, please follow XUnit "Getting Started" and "Installation" here https://xunit.net/docs/getting-started/netfx/visual-studio. + +## Install packages + +Aside from XUnit packages, we need to install two more packages: +- [Avalonia.Headless.XUnit](https://www.nuget.org/packages/Avalonia.Headless.XUnit) which also includes Avalonia. +- [Avalonia.Themes.Fluent](https://www.nuget.org/packages/Avalonia.Themes.Fluent) as even headless controls need a theme + +:::tip +Headless platform doesn't require any specific theme, and it is possible to swap FluentTheme with any other. +::: + +## Setup Application +As in any other Avalonia app, an `Application` instance needs to be created, and themes need to be applied. When using the Headless platform, the setup is not much different from a regular Avalonia app and can mostly be reused. + +```xml title=App.axaml + + + + + +``` + +And the code: + +```csharp title=App.axaml.cs +using Avalonia; +using Avalonia.Headless; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } +} +``` + +:::note +Usually, the `BuildAvaloniaApp` method is defined in the Program.cs file, but NUnit/XUnit tests don't have it, so it is defined in the `App` file instead. +::: + +## Initialize XUnit Tests + +The `[AvaloniaTestApplication]` attribute wires the tests in the current project with the specific application. It needs to be defined once per project in any file. + +```csharp +[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] + +public class TestAppBuilder +{ + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()); +} +``` + +## Test Example + +```csharp +[AvaloniaFact] +public void Should_Type_Text_Into_TextBox() +{ + // Setup controls: + var textBox = new TextBox(); + var window = new Window { Content = textBox }; + + // Open window: + window.Show(); + + // Focus text box: + textBox.Focus(); + + // Simulate text input: + window.KeyTextInput("Hello World"); + + // Assert: + Assert.Equal("Hello World", textBox.Text); +} +``` + +Instead of the typical `[Fact]` attribute, we need to use `[AvaloniaFact]` as it sets up the UI thread. Similarly, instead of `[Theory]`, there is a `[AvaloniaTheory]` attribute. diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/index.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/index.md new file mode 100644 index 000000000..bf04e50f0 --- /dev/null +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/concepts/headless/index.md @@ -0,0 +1,163 @@ +--- +id: index +title: Headless Platform +--- + +## Introduction +The headless platform in AvaloniaUI provides the capability to run Avalonia applications without a visible graphical user interface (GUI). This allows for testing and automation scenarios on systems that lack a graphical environment, such as servers or continuous integration/continuous deployment (CI/CD) environments. + +By utilizing the headless platform, you can perform UI testing, execute application scenarios, and validate functionality in a headless environment, saving time and resources compared to manual testing. + +## Getting Started + +While the Headless platform can be initialized without any dependencies, for convenience, we have created integration packages for common unit testing platforms: + +- [Headless Testing with XUnit](./headless-xunit) +- [Headless Testing with NUnit](./headless-nunit) +- If you are using another platform or need more control: [Manual setup of the Headless platform](./headless-custom) + +## Simulating User Input + +As the headless platform doesn't have any real input, every event needs to be raised from the unit test. The Avalonia.Headless package is shipped with a number of helper methods that can be used: + +#### `Window.KeyPress(Key key, RawInputModifiers modifiers)` + +Simulates a keyboard press on the headless window/toplevel. + +#### `Window.KeyRelease(Key key, RawInputModifiers modifiers)` + +Simulates a keyboard release on the headless window/toplevel. + +#### `Window.KeyTextInput(string text)` + +Simulates a text input event on the headless window/toplevel. + +:::note +This event is independent of KeyPress and KeyRelease. If you need to simulate text input to a TextBox or a similar control, please use KeyTextInput. +::: + +#### `Window.MouseDown(Point point, MouseButton button, RawInputModifiers modifiers)` + +Simulates a mouse down event on the headless window/toplevel. + +:::note +In the headless platform, there is a single mouse pointer. There are no helper methods for touch or pen input. +::: + +#### `Window.MouseMove(Point point, MouseButton button, RawInputModifiers modifiers)` + +Simulates a mouse move event on the headless window/toplevel. + +#### `Window.MouseUp(Point point, MouseButton button, RawInputModifiers modifiers)` + +Simulates a mouse up event on the headless window/toplevel. + +#### `Window.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers)` + +Simulates a mouse wheel event on the headless window/toplevel. + +#### `Window.DragDrop(Point point, RawDragEventType type, DataObject data, DragDropEffects effects, RawInputModifiers modifiers)` + +Simulates a drag and drop target event on the headless window/toplevel. This event simulates a user moving files from another app to the current app. + +A simple button click test is a typical example where these methods can be used: + +```csharp +// Create window and button: +var button = new Button +{ + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch +}; +var window = new Window +{ + Width = 100, + Height = 100, + Content = button +}; + +// Subscribe to the button click event: +var buttonClicked = false; +button.Click += (_, _) => buttonClicked = true; + +// Show the window: +window.Show(); + +// Simulate mouse events with a click (mouse down + up): +window.MouseDown(new Point(50, 50), MouseButton.Left); +window.MouseUp(new Point(50, 50), MouseButton.Left); + +// Assert that the button was clicked: +Assert.True(buttonClicked); +``` + +:::tip +Just like in any other Avalonia application, it's also possible to raise events directly. For example, with button click `button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent))`. This can be more convenient for most use cases but lacks some flexibility with input parameters. +::: + +## Capturing the Last Rendered Frame + +By default, the Headless platform doesn't render anything and instead has a fake/headless drawing backend enabled. However, it is possible to enable the Skia renderer and use it to capture the last rendered frame, which can be compared with an expected image or used in another way. + +To enable the Skia renderer, adjust the AppBuilder code as follows: + +```csharp title=App.axaml.cs +public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseSkia() // enable Skia renderer + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = false // disable headless drawing + }); +``` + +With the real renderer enabled, you can use the `Window.CaptureRenderedFrame` helper method: + +```csharp +var window = new Window +{ + Content = new TextBlock + { + Text = "Hello World" + } +}; +window.Show(); + +var frame = window.CaptureRenderedFrame(); +frame.Save("file.png"); +``` + +:::tip +The `CaptureRenderedFrame` method returns a WriteableBitmap, allowing you to lock it and read pixel data that can be compared in memory. +::: + +## Simulating User Delay + +In real UI applications, there is always some delay between user interactions, which gives the operating system time to run various tasks in the application. These tasks are often delayed and might include operations such as window resize, which on most platforms (including Headless), is asynchronous. + +This delay can result in unexpected behavior when some operations have not yet executed. + +Obvious solution would be to add `Task.Delay` between these operations, but a more efficient option in Avalonia would be to use the `Dispatcher` API, which allows flushing the jobs queue directly: + +```csharp +var window = new Window(); +window.Show(); + +window.Width = 100; +window.Height = 100; + +// highlight-start +Dispatcher.UIThread.RunJobs(); +// highlight-end + +Assert.AreEqual(new Size(100, 100), window.ClientSize); +``` + +Additionally, the headless platform provides a method to force the render timer to tick, which can be useful in some scenarios: + +```csharp +AvaloniaHeadlessPlatform.ForceRenderTimerTick(); +``` + +:::tip +All input helper methods and `CaptureRenderedFrame` internally use these user delay methods, so there is no need to run them twice! +::: \ No newline at end of file