dotnet-uno-testing について
WASM、デスクトップ、モバイルを対象とする Uno Platform 開発者向けのテストスキルです。Playwright によるブラウザテスト、共通の識別子、再利用可能なテストロジックを扱います。
主な機能
- 01実行ターゲット別のテスト方式
- 02Playwright による WASM テスト
- 03AutomationId によるコントロール特定
- 04複数プラットフォームでのテスト再利用
- 05共通テストと個別テストの分離
活用シーン
- 01Uno WASM の画面操作を検証
- 02各ターゲットで共通する動作を確認
- 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 ヘッドで同じように機能する唯一のセレクター戦略です。 - 共有テストとプラットフォーム固有のテストを分けます。 共有テストロジックには抽象基底クラスを使用し、プラットフォームごとに具象サブクラスを作成します。
- プラットフォームによって動作が異なる場合にのみ、プラットフォーム固有のテストを追加します。 ファイルピッカー、ハードウェアボタン、ジェスチャー、通知はプラットフォームごとに異なるため、個別にテストします。
エージェントが注意すべき点
- Uno WASM アプリがすぐに読み込まれると思い込まないでください。 Uno ランタイムの初期化(mono WASM ブートストラップ)には数秒かかります。操作する前に、必ずアプリのルート要素が表示されるまで待ちます。
- Uno WASM 要素には CSS セレクターを使用しないでください。 Uno は独自の DOM 構造を生成します。CSS クラスは内部実装であり、安定していません。
AutomationProperties.AutomationIdによるdata-testidのみを使用します。 - Playwright テストの実行前に WASM ヘッドをビルドするのを忘れないでください。
dotnet runは必要に応じてビルドしますが、本番環境に近いテストにはdotnet publishが必要です。古いビルドが原因で、分かりにくいテスト失敗が発生します。 - WASM ヘッドでモバイル固有の機能をテストしないでください。 ファイルシステムへのアクセス、プッシュ通知、生体認証、NFC はブラウザーでは利用できません。WASM テストでは、これらをスキップするかモックします。
- すべてのプラットフォームのテストを単一の CI ジョブで実行しないでください。 プラットフォームごとに専用の SDK(Android SDK、Xcode、WinAppDriver)が必要です。適切なランナーを使用し、プラットフォームごとに CI ジョブを分けます。