Uno平台测试技能 dotnet-uno-testing

本技能专注于测试Uno Platform应用程序,涵盖WASM头部的Playwright浏览器自动化、平台特定测试模式以及跨平台测试基础设施。关键词:Uno Platform, 测试, Playwright, 跨平台, WASM, 自动化测试。

测试 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-uno-testing description: “测试Uno Platform应用程序。用于WASM的Playwright,平台特定模式,运行时头部。” user-invocable: false

dotnet-uno-testing

跨目标头部(WASM、桌面、移动)测试Uno Platform应用程序。涵盖基于Playwright的浏览器自动化用于Uno WASM应用程序、不同运行时头部的平台特定测试模式,以及跨平台Uno项目的测试基础设施。

版本假设: .NET 8.0+ 基线,Uno Platform 5.x+,用于WASM测试的Playwright 1.40+。Uno Platform使用具有多个目标框架的单项目结构。

范围

  • 用于Uno WASM应用程序的基于Playwright的浏览器自动化
  • 不同运行时头部的平台特定测试模式
  • 跨平台Uno项目的测试基础设施

范围外

  • 共享UI测试模式(页面对象模型、选择器、等待策略) – 参见 [skill:dotnet-ui-testing-core]
  • Playwright基础(安装、CI缓存、跟踪查看器) – 参见 [skill:dotnet-playwright]
  • 测试项目脚手架 – 参见 [skill:dotnet-add-testing]

先决条件: 配置了WASM头部的Uno Platform应用程序。对于WASM测试:安装了Playwright浏览器(参见 [skill:dotnet-playwright])。对于移动测试:配置了平台SDK(Android SDK、Xcode)。

交叉引用: [skill:dotnet-ui-testing-core] 用于页面对象模型和选择器策略, [skill:dotnet-playwright] 用于Playwright安装、CI缓存和跟踪查看器, [skill:dotnet-uno-platform] 用于Uno扩展、MVUX、工具包和主题指导, [skill:dotnet-uno-targets] 用于每个目标的部署和平台特定陷阱。


按头部划分的Uno测试策略

Uno Platform应用程序在多个头部上运行(WASM、桌面/Skia、iOS、Android、Windows)。每个头部有不同的测试工具和权衡。

头部 测试方法 工具 速度 保真度
WASM 浏览器自动化 Playwright 中等 高 – 真实浏览器渲染
桌面 (Skia/GTK, WPF) UI自动化 Appium / WinAppDriver 中等 高 – 真实桌面渲染
iOS 模拟器自动化 Appium + XCUITest 最高 – 真实iOS渲染
Android 模拟器自动化 Appium + UIAutomator2 最高 – 真实Android渲染
单元 (共享逻辑) 内存中 xUnit (无UI) N/A – 仅逻辑

推荐优先级: 首先用单元测试测试共享业务逻辑。使用Playwright针对WASM头部进行UI验证 – 这是最快的UI测试路径,覆盖最广。仅对头部之间不同的行为添加平台特定的Appium测试。


用于Uno WASM的Playwright

WASM头部在浏览器中渲染Uno应用程序,使Playwright成为UI测试的自然选择。

测试基础设施

// NuGet: Microsoft.Playwright
public class UnoWasmFixture : IAsyncLifetime
{
    public IPlaywright Playwright { get; private set; } = null!;
    public IBrowser Browser { get; private set; } = null!;
    public IPage Page { get; private set; } = null!;

    private Process? _serverProcess;

    public async ValueTask InitializeAsync()
    {
        // 启动WASM应用程序(dotnet run或服务发布的输出)
        _serverProcess = await StartWasmServerAsync();

        Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
        Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
        Page = await Browser.NewPageAsync();

        // 等待Uno WASM应用程序完全加载
        await Page.GotoAsync("http://localhost:5000");
        await WaitForUnoAppReadyAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await Page.CloseAsync();
        await Browser.CloseAsync();
        Playwright.Dispose();

        _serverProcess?.Kill(entireProcessTree: true);
        _serverProcess?.Dispose();
    }

    private async Task WaitForUnoAppReadyAsync()
    {
        // Uno WASM应用程序显示加载启动画面;等待应用程序根元素出现
        await Page.WaitForSelectorAsync(
            "[data-testid='app-root'], #uno-body",
            new() { State = WaitForSelectorState.Visible, Timeout = 30_000 });

        // 额外等待Uno运行时初始化
        await Page.WaitForFunctionAsync(
            "() => document.querySelector('#uno-body')?.children.length > 0",
            null,
            new() { Timeout = 15_000 });
    }

    private static async Task<Process> StartWasmServerAsync()
    {
        var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "dotnet",
                Arguments = "run --project src/MyApp/MyApp.Wasm.csproj --no-build",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            }
        };
        process.Start();

        // 通过探测健康端点等待服务器准备就绪
        using var httpClient = new HttpClient();
        var deadline = DateTime.UtcNow.AddSeconds(30);
        while (DateTime.UtcNow < deadline)
        {
            try
            {
                var resp = await httpClient.GetAsync("http://localhost:5000");
                if (resp.IsSuccessStatusCode) break;
            }
            catch (HttpRequestException)
            {
                // 服务器尚未准备就绪
            }
            await Task.Delay(500);
        }
        return process;
    }
}

WASM UI测试

public class UnoNavigationTests : IClassFixture<UnoWasmFixture>
{
    private readonly IPage _page;

    public UnoNavigationTests(UnoWasmFixture fixture)
    {
        _page = fixture.Page;
    }

    [Fact]
    public async Task MainPage_LoadsSuccessfully()
    {
        // Uno WASM将XAML控件渲染为HTML元素
        // 使用AutomationProperties.AutomationId进行选择器
        var title = _page.Locator("[data-testid='main-title']");

        await Expect(title).ToBeVisibleAsync();
        await Expect(title).ToHaveTextAsync("Welcome");
    }

    [Fact]
    public async Task Navigation_ClickSettings_ShowsSettingsPage()
    {
        await _page.ClickAsync("[data-testid='nav-settings']");

        var settingsHeader = _page.Locator("[data-testid='settings-header']");
        await Expect(settingsHeader).ToBeVisibleAsync();
        await Expect(settingsHeader).ToHaveTextAsync("Settings");
    }
}

表单交互测试

[Fact]
public async Task LoginForm_SubmitValid_NavigatesToDashboard()
{
    await _page.FillAsync("[data-testid='username-input']", "testuser");
    await _page.FillAsync("[data-testid='password-input']", "P@ssw0rd!");
    await _page.ClickAsync("[data-testid='login-button']");

    // 登录后等待导航
    var dashboard = _page.Locator("[data-testid='dashboard-title']");
    await Expect(dashboard).ToBeVisibleAsync(
        new() { Timeout = 10_000 });
}

[Fact]
public async Task TodoList_AddItem_AppearsInList()
{
    await _page.FillAsync("[data-testid='todo-input']", "Buy groceries");
    await _page.ClickAsync("[data-testid='add-todo-btn']");

    var items = _page.Locator("[data-testid='todo-item']");
    await Expect(items).ToHaveCountAsync(1);
    await Expect(items.First).ToContainTextAsync("Buy groceries");
}

平台特定测试

用于跨平台选择器的AutomationProperties

Uno将 AutomationProperties.AutomationId 映射到每个平台的原生标识符:

<!-- Uno XAML -- 适用于所有头部 -->
<Page x:Class="MyApp.Views.LoginPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <StackPanel>
        <TextBox AutomationProperties.AutomationId="username-input"
                 PlaceholderText="Username" />

        <PasswordBox AutomationProperties.AutomationId="password-input"
                     PlaceholderText="Password" />

        <Button AutomationProperties.AutomationId="login-button"
                Content="Log In" />

        <TextBlock AutomationProperties.AutomationId="error-message"
                   Foreground="Red" />
    </StackPanel>
</Page>

平台映射:

  • WASM: 渲染为 data-testid 属性(Playwright选择器)
  • Android: 映射到 content-desc(Appium AccessibilityId
  • iOS: 映射到 accessibilityIdentifier(Appium AccessibilityId
  • Windows: 映射到 AutomationId(WinAppDriver AccessibilityId

测试平台特定代码

对于因平台而异的代码,使用条件编译和单独的测试类:

// 共享测试 -- 在所有平台上运行
[Fact]
public async Task Settings_ChangeTheme_UpdatesUI()
{
    await _page.ClickAsync("[data-testid='theme-toggle']");

    var body = _page.Locator("[data-testid='app-root']");
    await Expect(body).ToHaveAttributeAsync("data-theme", "dark");
}

// 平台特定测试
[Fact]
[Trait("Platform", "WASM")]
public async Task FileUpload_BrowserDialog_AcceptsFiles()
{
    // WASM使用浏览器文件选择器 -- 使用Playwright文件选择器测试
    var fileChooserTask = _page.WaitForFileChooserAsync();
    await _page.ClickAsync("[data-testid='upload-btn']");
    var fileChooser = await fileChooserTask;
    await fileChooser.SetFilesAsync("testdata/sample.pdf");

    var fileName = _page.Locator("[data-testid='file-name']");
    await Expect(fileName).ToHaveTextAsync("sample.pdf");
}

运行时头部验证

使用共享测试逻辑和平台特定驱动验证相同的UI逻辑在不同Uno运行时头部上正确工作。

共享测试逻辑模式

/// <summary>
/// 定义UI测试一次的抽象基类。具体子类提供
/// 每个平台的驱动(WASM的Playwright,移动的Appium)。
/// </summary>
public abstract class LoginTestsBase
{
    protected abstract Task FillFieldAsync(string automationId, string value);
    protected abstract Task ClickAsync(string automationId);
    protected abstract Task<string> GetTextAsync(string automationId);
    protected abstract Task WaitForElementAsync(string automationId, int timeoutMs = 5000);

    [Fact]
    public async Task Login_ValidCredentials_ShowsDashboard()
    {
        await FillFieldAsync("username-input", "alice");
        await FillFieldAsync("password-input", "P@ssw0rd!");
        await ClickAsync("login-button");

        await WaitForElementAsync("dashboard-title");
        var title = await GetTextAsync("dashboard-title");
        Assert.Equal("Dashboard", title);
    }

    [Fact]
    public async Task Login_EmptyPassword_ShowsValidationError()
    {
        await FillFieldAsync("username-input", "alice");
        await ClickAsync("login-button");

        await WaitForElementAsync("error-message");
        var error = await GetTextAsync("error-message");
        Assert.Contains("required", error, StringComparison.OrdinalIgnoreCase);
    }
}

// WASM实现
public class LoginTestsWasm : LoginTestsBase, IClassFixture<UnoWasmFixture>
{
    private readonly IPage _page;
    public LoginTestsWasm(UnoWasmFixture fixture) => _page = fixture.Page;

    protected override async Task FillFieldAsync(string automationId, string value) =>
        await _page.FillAsync($"[data-testid='{automationId}']", value);

    protected override async Task ClickAsync(string automationId) =>
        await _page.ClickAsync($"[data-testid='{automationId}']");

    protected override async Task<string> GetTextAsync(string automationId) =>
        await _page.Locator($"[data-testid='{automationId}']").TextContentAsync() ?? "";

    protected override async Task WaitForElementAsync(string automationId, int timeoutMs = 5000) =>
        await _page.WaitForSelectorAsync(
            $"[data-testid='{automationId}']",
            new() { Timeout = timeoutMs });
}

关键原则

  • 首先用单元测试测试共享逻辑。 Uno的MVVM模式意味着大多数业务逻辑可以在没有任何UI框架的情况下测试。
  • 使用Playwright + WASM作为主要UI测试路径。 它比移动模拟器更快,并在浏览器中提供真实的渲染保真度。
  • 在所有可测试控件上使用 AutomationProperties.AutomationId 这是唯一在所有Uno头部上相同工作的选择器策略。
  • 将共享测试与平台特定测试分开。 使用抽象基类进行共享测试逻辑,每个平台的具体子类。
  • 仅对平台分歧行为添加平台特定测试。 文件选择器、硬件按钮、手势和通知在不同平台上不同;单独测试这些。

代理陷阱

  1. 不要假设Uno WASM应用程序立即加载。 Uno运行时必须初始化(mono WASM引导),这需要几秒钟。在交互之前始终等待应用程序根元素。
  2. 不要对Uno WASM元素使用CSS选择器。 Uno生成自己的DOM结构;CSS类是内部且不稳定的。专门使用 data-testid(来自 AutomationProperties.AutomationId)。
  3. 不要忘记在运行Playwright测试之前构建WASM头部。 dotnet run 按需构建,但 dotnet publish 需要用于类似生产的测试。过时的构建会导致混淆的测试失败。
  4. 不要在WASM头部测试移动特定功能。 文件系统访问、推送通知、生物识别和NFC在浏览器中不可用。在WASM测试中跳过或模拟这些。
  5. 不要在所有平台测试中运行单个CI作业。 每个平台需要自己的SDK(Android SDK、Xcode、WinAppDriver)。使用每个平台的单独CI作业与适当的运行器。

参考