mcpField
D
dotnet-uno-testing

dotnet-uno-testing

github.com/wshaddixテスト・品質8666 回インストール35 週間前に更新
リポジトリ

dotnet-uno-testing について

WASM、デスクトップ、モバイルを対象とする Uno Platform 開発者向けのテストスキルです。Playwright によるブラウザテスト、共通の識別子、再利用可能なテストロジックを扱います。

主な機能

  1. 01実行ターゲット別のテスト方式
  2. 02Playwright による WASM テスト
  3. 03AutomationId によるコントロール特定
  4. 04複数プラットフォームでのテスト再利用
  5. 05共通テストと個別テストの分離

活用シーン

  1. 01Uno WASM の画面操作を検証
  2. 02各ターゲットで共通する動作を確認
  3. 03特定プラットフォーム固有の機能をテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-uno-testing

インストール後、Claude Code で/dotnet-uno-testingを入力して読み込まれたか確認します。

詳細

dotnet-uno-testing

ターゲットヘッド(WASM、デスクトップ、モバイル)全体にわたる Uno Platform アプリケーションのテストを行います。Uno WASM アプリ向けの Playwright ベースのブラウザー自動化、さまざまなランタイムヘッドに対応するプラットフォーム固有のテストパターン、クロスプラットフォーム Uno プロジェクト向けのテストインフラストラクチャを扱います。

バージョンの前提: ベースラインは .NET 8.0 以降、Uno Platform 5.x 以降、WASM テストには Playwright 1.40 以降です。Uno Platform は、複数のターゲットフレームワークを持つ単一プロジェクト構造を採用しています。

対象外: 共通の 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 Extensions、MVUX、Toolkit、テーマのガイダンス、[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 なし)速い該当なし -- ロジックのみ

推奨する優先順位: まずユニットテストで共有ビジネスロジックをテストします。UI の検証には WASM ヘッドで Playwright を使用します。これは最も速く、幅広い範囲をカバーできる UI テスト方法です。ヘッド間で動作が異なる部分に限り、プラットフォーム固有の Appium テストを追加します。


Uno WASM 向け Playwright

WASM ヘッドでは Uno アプリがブラウザー内にレンダリングされるため、UI テストには Playwright が自然な選択肢です。

テストインフラストラクチャ
// 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()
    {
        // Start the WASM app (dotnet run or serve the published output)
        _serverProcess = await StartWasmServerAsync();

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

        // Wait for Uno WASM app to fully load
        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 apps show a loading splash; wait for the app root to appear
        await Page.WaitForSelectorAsync(
            "[data-testid='app-root'], #uno-body",
            new() { State = WaitForSelectorState.Visible, Timeout = 30_000 });

        // Additional wait for Uno runtime initialization
        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();

        // Wait for server to be ready by probing the health endpoint
        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)
            {
                // Server not ready yet
            }
            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 renders XAML controls as HTML elements
        // Use AutomationProperties.AutomationId for selectors
        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']");

    // Wait for navigation after login
    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 を各プラットフォームのネイティブ識別子にマッピングします。


<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)
プラットフォーム固有コードのテスト

プラットフォームによって異なるコードには、条件付きコンパイルと個別のテストクラスを使用します。

// Shared test -- runs on all platforms
[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");
}

// Platform-specific test
[Fact]
[Trait("Platform", "WASM")]
public async Task FileUpload_BrowserDialog_AcceptsFiles()
{
    // WASM uses browser file picker -- test with Playwright file chooser
    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");
}

ランタイムヘッドの検証

プラットフォーム固有のドライバーと共有テストロジックを使用して、異なる Uno ランタイムヘッド間で同じ UI ロジックが正しく動作することを検証します。

共有テストロジックのパターン
/// <summary>
/// Abstract base that defines UI tests once. Concrete subclasses provide
/// the driver for each platform (Playwright for WASM, Appium for mobile).
/// </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 implementation
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 フレームワークなしでテストできます。
  • UI テストの主要な手段として Playwright + WASM を使用します。 モバイルエミュレーターより高速で、ブラウザー上で実際のレンダリングを忠実に再現できます。
  • テスト可能なすべてのコントロールに AutomationProperties.AutomationId を使用します。 すべての Uno ヘッドで同じように機能する唯一のセレクター戦略です。
  • 共有テストとプラットフォーム固有のテストを分けます。 共有テストロジックには抽象基底クラスを使用し、プラットフォームごとに具象サブクラスを作成します。
  • プラットフォームによって動作が異なる場合にのみ、プラットフォーム固有のテストを追加します。 ファイルピッカー、ハードウェアボタン、ジェスチャー、通知はプラットフォームごとに異なるため、個別にテストします。

エージェントが注意すべき点

  1. Uno WASM アプリがすぐに読み込まれると思い込まないでください。 Uno ランタイムの初期化(mono WASM ブートストラップ)には数秒かかります。操作する前に、必ずアプリのルート要素が表示されるまで待ちます。
  2. Uno WASM 要素には CSS セレクターを使用しないでください。 Uno は独自の DOM 構造を生成します。CSS クラスは内部実装であり、安定していません。AutomationProperties.AutomationId による data-testid のみを使用します。
  3. Playwright テストの実行前に WASM ヘッドをビルドするのを忘れないでください。 dotnet run は必要に応じてビルドしますが、本番環境に近いテストには dotnet publish が必要です。古いビルドが原因で、分かりにくいテスト失敗が発生します。
  4. WASM ヘッドでモバイル固有の機能をテストしないでください。 ファイルシステムへのアクセス、プッシュ通知、生体認証、NFC はブラウザーでは利用できません。WASM テストでは、これらをスキップするかモックします。
  5. すべてのプラットフォームのテストを単一の CI ジョブで実行しないでください。 プラットフォームごとに専用の SDK(Android SDK、Xcode、WinAppDriver)が必要です。適切なランナーを使用し、プラットフォームごとに CI ジョブを分けます。

参考資料

コメント