更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
@@ -0,0 +1,201 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using FlaUI.Core.Definitions;
using FlaUI.Core.Input;
using FlaUI.Core.WindowsAPI;
#pragma warning disable IDE0008 // Use explicit type instead of 'var'
#pragma warning disable SA1512 // Single-line comments should not be followed by blank line
namespace Wpf.Ui.Gallery.IntegrationTests;
public sealed class ContentDialogAutomationTests : UiTest
{
[Fact]
public async Task ContentDialog_Should_Return_Correct_Text()
{
// Give the test app a moment to display the window before starting UI automation interactions
await Wait(2, TestContext.Current.CancellationToken);
// Navigate to the ContentDialog page explicitly: click parent then child nav items
var parentNav = FindFirst(c => c.ByText("Dialogs & flyouts"));
parentNav.Should().NotBeNull("because the Dialogs & flyouts navigation item should be present");
parentNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
var childNav = FindFirst(c => c.ByText("ContentDialog"));
if (childNav == null)
{
// If the child item is not immediately visible, try toggling the parent to expand it and retry
parentNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
childNav = FindFirst(c => c.ByText("ContentDialog"));
}
childNav.Should().NotBeNull("because the ContentDialog navigation item should be present as a child");
childNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
var showButton = FindFirst(c => c.ByText("Show"));
showButton
.Should()
.NotBeNull("because the ContentDialog page must contain a Show button to open the dialog");
showButton.AsButton().Click();
await Wait(1, TestContext.Current.CancellationToken);
// Now exercise each dialog button (Primary, Secondary, Close) and verify
// the page TextBlock updates according to ContentDialogViewModel.
// Primary -> "Save" -> expect "User saved their work"
await OpenDialog();
await ClickButtonMatching(["Save"]);
await WaitForText("User saved their work");
// Secondary -> "Don't Save" -> expect "User did not save their work"
await OpenDialog();
await ClickButtonMatching(["Don't Save", "Do not Save", "Dont Save"]);
await WaitForText("User did not save their work");
// Close/Cancel -> "Cancel" -> expect "User cancelled the dialog"
await OpenDialog();
await ClickButtonMatching(["Cancel", "Close"]);
await WaitForText("User cancelled the dialog");
}
[Fact]
public async Task ContentDialog_CtrlF_DoesNotFocus_NavigationAutoSuggestBox()
{
await Wait(2, TestContext.Current.CancellationToken);
// Open ContentDialog page and show dialog
var parentNav = FindFirst(c => c.ByText("Dialogs & flyouts"));
parentNav.Should().NotBeNull();
parentNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
var childNav = FindFirst(c => c.ByText("ContentDialog"));
if (childNav == null)
{
parentNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
childNav = FindFirst(c => c.ByText("ContentDialog"));
}
childNav.Should().NotBeNull();
childNav.Click();
await Wait(1, TestContext.Current.CancellationToken);
var showButton = FindFirst(c => c.ByText("Show"));
showButton.Should().NotBeNull();
showButton.AsButton().Click();
await Wait(1, TestContext.Current.CancellationToken);
// Send Ctrl+F and ensure background autosuggest does not get focused
Keyboard.Press(VirtualKeyShort.CONTROL);
Keyboard.Type(VirtualKeyShort.KEY_F);
Keyboard.Release(VirtualKeyShort.CONTROL);
global::FlaUI.Core.Input.Wait.UntilInputIsProcessed();
// "Find the element with keyboard focus within the main window."
var focusedElement = MainWindow
?.FindAllDescendants()
.FirstOrDefault(e => e.Properties.HasKeyboardFocus.ValueOrDefault);
// Assert that a focused element is found.
focusedElement.Should().NotBeNull("there should be a focused element after sending keys");
// Get and assert that the AutomationId is not the background autosuggest's id.
var focusedAutomationId = focusedElement.Properties.AutomationId.ValueOrDefault as string;
focusedAutomationId
.Should()
.NotBe(
"NavigationAutoSuggestBox",
"because Ctrl+F should not focus the background autosuggest while the dialog is open"
);
}
private Task OpenDialog()
{
// ensure dialog opened by clicking Show
var showButton = FindFirst(c => c.ByText("Show"));
if (showButton == null)
{
// If Show button is not found, assume dialog is already open; give UI a moment to stabilize.
return Wait(1, TestContext.Current.CancellationToken);
}
showButton.AsButton().Click();
return Wait(1, TestContext.Current.CancellationToken);
}
/// <summary>
/// Polls the UI until the specified text appears or a retry limit is reached.
/// Useful for waiting for view-model-driven UI updates after dialog interactions.
/// </summary>
/// <param name="text">The text to wait for.</param>
/// <param name="retries">Number of polling attempts.</param>
/// <param name="delaySeconds">Delay in seconds between attempts (uses test cancellation token).</param>
/// <returns>A task that completes when the text is found or throws an assertion if not found.</returns>
private async Task WaitForText(string text, int retries = 10, int delaySeconds = 1)
{
for (var i = 0; i < retries; i++)
{
if (FindFirst(c => c.ByText(text)) != null)
{
return;
}
await Wait(delaySeconds, TestContext.Current.CancellationToken);
}
// Final assertion to fail test with clear message if text never appeared
FindFirst(c => c.ByText(text))
.Should()
.NotBeNull($"Expected text '{text}' to appear within timeout");
}
/// <summary>
/// Finds and clicks a button matching one of the provided candidate texts.
/// If no direct match is found, falls back to clicking the last available button in the window.
/// </summary>
/// <param name="candidates">Array of acceptable button texts (first match is used).</param>
/// <returns>A task that waits a short time after clicking to allow UI to settle.</returns>
private Task ClickButtonMatching(string[] candidates)
{
AutomationElement? btn = null;
foreach (var txt in candidates)
{
btn = FindFirst(c => c.ByText(txt));
if (btn != null)
{
break;
}
}
if (btn == null)
{
var buttons = MainWindow?.FindAllDescendants(cf => cf.ByControlType(ControlType.Button));
if (buttons is { Length: > 0 })
{
btn = buttons.Last();
}
}
btn.Should().NotBeNull($"expected one of: {string.Join(',', candidates)}");
btn.AsButton().Click();
return Wait(1, TestContext.Current.CancellationToken);
}
}
#pragma warning restore IDE0008 // Use explicit type instead of 'var'
#pragma warning restore SA1512 // Single-line comments should not be followed by blank line
@@ -0,0 +1,85 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using FlaUI.Core;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
namespace Wpf.Ui.Gallery.IntegrationTests.Fixtures;
/// <summary>
/// Class managing the lifecycle of the tested application implementing <see cref="IAsyncLifetime"/>.
/// Uses <see cref="UIA3Automation"/> for UI automation.
/// </summary>
public sealed class TestedApplication : IAsyncLifetime
{
private const string ExecutableName = "Wpf.Ui.Gallery.exe";
private readonly AutomationBase automation = new UIA3Automation();
private Application? app;
private Window? mainWindow;
/// <summary>
/// Gets the wrapper for an application which should be automated.
/// </summary>
public Application? Application => app;
/// <summary>
/// Gets the main window of the applications process.
/// </summary>
public Window? MainWindow => mainWindow ??= app?.GetMainWindow(automation);
/// <inheritdoc />
public ValueTask InitializeAsync()
{
if (app is not null)
{
app.Close();
app.Dispose();
}
string path = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
ExecutableName
);
if (!File.Exists(path))
{
throw new InvalidOperationException(
$"Unable to find the application executable at path \"{path}\"."
);
}
app = Application.Launch(path);
app.WaitWhileMainHandleIsMissing(TimeSpan.FromMinutes(1));
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask DisposeAsync()
{
if (app is not null)
{
if (!app.HasExited)
{
app.Close();
}
// ReSharper disable once AccessToDisposedClosure
Retry.WhileFalse(() => app?.HasExited ?? true, TimeSpan.FromSeconds(2), ignoreException: true);
app.Dispose();
app = null;
}
automation?.Dispose();
return ValueTask.CompletedTask;
}
}
@@ -0,0 +1,96 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System.Threading;
using FlaUI.Core;
using FlaUI.Core.Conditions;
using FlaUI.Core.Input;
using FlaUI.Core.WindowsAPI;
namespace Wpf.Ui.Gallery.IntegrationTests.Fixtures;
/// <summary>
/// Base class for UI tests implementing <see cref="Xunit.IAsyncLifetime"/> to manage <see cref="FlaUI.Core.Application"/> lifecycle.
/// </summary>
public abstract class UiTest : IAsyncLifetime
{
private readonly TestedApplication app = new();
/// <summary>
/// Gets the wrapper for an application which should be automated.
/// </summary>
internal Application? Application => app.Application;
/// <summary>
/// Gets the main window of the applications process.
/// </summary>
internal Window? MainWindow => app.MainWindow;
/// <inheritdoc />
public ValueTask InitializeAsync() => app.InitializeAsync();
/// <inheritdoc />
public ValueTask DisposeAsync() => app.DisposeAsync();
/// <summary>
/// Finds the first descendant with the given automation id.
/// </summary>
/// <param name="automationId">The automation id.</param>
/// <returns>The found element or null if no element was found.</returns>
protected AutomationElement? FindFirst(string automationId) =>
app.MainWindow?.FindFirstDescendant(automationId);
/// <summary>Finds the first descendant with the condition.</summary>
/// <param name="conditionFunc">The condition method.</param>
/// <returns>The found element or null if no element was found.</returns>
protected AutomationElement? FindFirst(Func<ConditionFactory, ConditionBase> conditionFunc) =>
app.MainWindow?.FindFirstDescendant(conditionFunc);
/// <summary>
/// Creates a Task that will complete after a time delay.
/// </summary>
/// <param name="seconds">The time delay in seconds.</param>
/// <param name="cancellationToken">An optional cancellation token to cancel the delay.</param>
/// <returns>A Task that represents the time delay.</returns>
/// <remarks>
/// After the specified time delay, the Task is completed in RanToCompletion state. If the <paramref name="cancellationToken"/>
/// is cancelled, the returned task will be cancelled and an <see cref="OperationCanceledException"/> will be thrown.
/// </remarks>
protected Task Wait(int seconds, CancellationToken cancellationToken = default) =>
Task.Delay(TimeSpan.FromSeconds(seconds), cancellationToken);
/// <summary>
/// Simulate typing in text. This is slower than setting <see cref="P:FlaUI.Core.AutomationElements.TextBox.Text" /> but raises more events.
/// </summary>
protected void Enter(string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
string[] source = value.Replace("\r\n", "\n").Split('\n');
Keyboard.Type(source[0]);
foreach (string text in ((IEnumerable<string>)source).Skip<string>(1))
{
Keyboard.Type(VirtualKeyShort.RETURN);
Keyboard.Type(text);
}
global::FlaUI.Core.Input.Wait.UntilInputIsProcessed();
}
/// <summary>
/// Type the given key.
/// </summary>
protected void Press(VirtualKeyShort virtualKey)
{
Keyboard.Type(virtualKey);
global::FlaUI.Core.Input.Wait.UntilInputIsProcessed();
}
}
@@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
global using System.Reflection;
global using AwesomeAssertions;
global using FlaUI.Core.AutomationElements;
global using Wpf.Ui.FlaUI;
global using Wpf.Ui.Gallery.IntegrationTests.Fixtures;
@@ -0,0 +1,46 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using FlaUI.Core.Definitions;
using FlaUI.Core.WindowsAPI;
namespace Wpf.Ui.Gallery.IntegrationTests;
public sealed class NavigationTests : UiTest
{
[Fact]
public async Task Settings_ShouldBeAvailable_ThroughAutoSuggestBox()
{
AutomationElement? autoSuggestBox = FindFirst("NavigationAutoSuggestBox");
autoSuggestBox
.Should()
.NotBeNull("because NavigationAutoSuggestBox should be present in the main window");
autoSuggestBox.As<AutoSuggestBox>().Enter("Settings");
await Wait(1);
FindFirst(c => c.ByText("About"))
.Should()
.NotBeNull("because Settings page should be displayed after clicking the Settings button");
}
[Fact]
public async Task Settings_ShouldBeAvailable_ThroughNavigation()
{
AutomationElement? settingsButton = FindFirst("NavigationFooterItems")
?.FindFirstDescendant(c => c.ByText("Settings"));
settingsButton.Should().NotBeNull("because NavigationView should be present in the main window");
settingsButton.Click();
await Wait(1);
FindFirst(c => c.ByText("About"))
.Should()
.NotBeNull("because Settings page should be displayed after clicking the Settings button");
}
}
@@ -0,0 +1,67 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System.Windows.Automation;
using WindowVisualState = FlaUI.Core.Definitions.WindowVisualState;
namespace Wpf.Ui.Gallery.IntegrationTests;
public sealed class TitleBarTests : UiTest
{
[Fact]
public async Task CloseButton_ShouldCloseWindow_WhenClicked()
{
Button? closeButton = FindFirst("TitleBarCloseButton").AsButton();
closeButton.Should().NotBeNull("because CloseButton should be present in the main window title bar");
closeButton.Click(moveMouse: false);
await Wait(2);
Application
?.HasExited.Should()
.BeTrue("because the main window should be closed after clicking the close button");
}
[Fact]
public async Task MinimizeButton_ShouldHideWindow_WhenClicked()
{
Button? minimizeButton = FindFirst("TitleBarMinimizeButton").AsButton();
minimizeButton
.Should()
.NotBeNull("because MinimizeButton should be present in the main window title bar");
minimizeButton.Click(moveMouse: false);
await Wait(2);
MainWindow
.Patterns.Window.Pattern.WindowVisualState.ValueOrDefault.Should()
.Be(
WindowVisualState.Minimized,
"because the main window should be minimized after clicking the minimize button"
);
}
[Fact]
public async Task MaximizeButton_ShouldExpandWindow_WhenClicked()
{
Button? maximizeButton = FindFirst("TitleBarMaximizeButton").AsButton();
maximizeButton
.Should()
.NotBeNull("because MaximizeButton should be present in the main window title bar");
maximizeButton.Click(moveMouse: false);
await Wait(2);
MainWindow
.Patterns.Window.Pattern.WindowVisualState.ValueOrDefault.Should()
.Be(
WindowVisualState.Maximized,
"because the main window should be maximized after clicking the maximize button"
);
}
}
@@ -0,0 +1,17 @@
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
namespace Wpf.Ui.Gallery.IntegrationTests;
public sealed class WindowTests() : UiTest
{
[Fact]
public void WindowTitle_ShouldMatchPredefinedOne()
{
string? title = MainWindow?.Title;
title.Should().Be("WPF UI Gallery", "because the main window title should match the predefined one");
}
}
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="FlaUI.UIA3" />
<PackageReference Include="xunit.v3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Wpf.Ui.FlaUI\Wpf.Ui.FlaUI.csproj" />
<ProjectReference Include="..\..\src\Wpf.Ui.Gallery\Wpf.Ui.Gallery.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="AwesomeAssertions" />
<Using Include="NSubstitute" />
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"culture": "invariant",
"parallelizeTestCollections": false,
"diagnosticMessages": true
}