dotnet-playwright について
dotnet-playwrightは、Playwrightでブラウザー自動化とエンドツーエンドテストを行う.NET開発者向けのスキルです。操作やアサーションに加え、CIキャッシュ、トレースによる調査、コード生成後のテスト改善を扱います。
主な機能
- 01ブラウザー管理とページ操作
- 02アサーションとネットワーク傍受
- 03Chromium、Firefox、WebKitのテスト
- 04CIでのブラウザーバイナリーキャッシュ
- 05Trace Viewerによる失敗調査
- 06Codegenとテストのひな形
活用シーン
- 01.NETアプリのエンドツーエンドテスト作成
- 02API応答をモックしてページを検証
- 03CIで複数ブラウザーのテスト実行
- 04実行トレースによるテスト失敗の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-playwrightインストール後、Claude Code で/dotnet-playwrightを入力して読み込まれたか確認します。
詳細
dotnet-playwright
Playwright for .NET:ブラウザーの自動化とエンドツーエンドテストに対応します。ブラウザーのライフサイクル管理、ページ操作、アサーション、CI でのブラウザーバイナリのキャッシュ、失敗のデバッグに使うトレースビューアー、テストのひな形をすばやく作成する codegen を扱います。
バージョンの前提: Playwright 1.40+ for .NET、.NET 8.0+ を基準とします。Playwright は Chromium、Firefox、WebKit ブラウザーに対応しています。
対象外: UI テストの共通パターン(ページオブジェクトモデル、セレクター、待機方法)は [skill:dotnet-ui-testing-core] で扱います。テスト戦略(E2E、単体テスト、統合テストの使い分け)は [skill:dotnet-testing-strategy] で扱います。テストプロジェクトのひな形作成は [skill:dotnet-add-testing] が担当します。
前提条件: [skill:dotnet-add-testing] を使ってテストプロジェクトのひな形を作成し、Playwright パッケージを参照していること。pwsh bin/Debug/net8.0/playwright.ps1 install または dotnet tool run playwright install でブラウザーをインストールします。
相互参照:[skill:dotnet-ui-testing-core] ではページオブジェクトモデルとセレクター戦略を、[skill:dotnet-testing-strategy] では E2E テストが適切かどうかの判断方法を扱います。
パッケージのセットアップ
<PackageReference Include="Microsoft.Playwright" Version="1.*" />
<PackageReference Include="Microsoft.Playwright.Xunit" Version="1.*" />
ブラウザーのインストール
Playwright でテストを実行するには、事前にブラウザーバイナリをダウンロードする必要があります。
# After building the test project:
pwsh bin/Debug/net8.0/playwright.ps1 install
# Or install specific browsers:
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
pwsh bin/Debug/net8.0/playwright.ps1 install firefox
# Using dotnet tool:
dotnet tool install --global Microsoft.Playwright.CLI
playwright install
基本的なテスト構造
Playwright の xUnit 基底クラスを使う場合
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;
// PageTest provides Page, Browser, BrowserContext, and Playwright properties
public class HomePageTests : PageTest
{
[Fact]
public async Task HomePage_Title_ContainsAppName()
{
await Page.GotoAsync("https://localhost:5001");
await Expect(Page).ToHaveTitleAsync(new Regex("My App"));
}
[Fact]
public async Task HomePage_NavLinks_AreVisible()
{
await Page.GotoAsync("https://localhost:5001");
var nav = Page.Locator("nav");
await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "Home" }))
.ToBeVisibleAsync();
await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "About" }))
.ToBeVisibleAsync();
}
}
手動でセットアップする場合(基底クラスを使わない場合)
public class ManualSetupTests : IAsyncLifetime
{
private IPlaywright _playwright = null!;
private IBrowser _browser = null!;
private IBrowserContext _context = null!;
private IPage _page = null!;
public async ValueTask InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true
});
_context = await _browser.NewContextAsync(new BrowserNewContextOptions
{
ViewportSize = new ViewportSize { Width = 1280, Height = 720 },
Locale = "en-US"
});
_page = await _context.NewPageAsync();
}
public async ValueTask DisposeAsync()
{
await _page.CloseAsync();
await _context.CloseAsync();
await _browser.CloseAsync();
_playwright.Dispose();
}
[Fact]
public async Task Login_ValidUser_RedirectsToDashboard()
{
await _page.GotoAsync("https://localhost:5001/login");
await _page.FillAsync("[data-testid='email']", "[email protected]");
await _page.FillAsync("[data-testid='password']", "P@ssw0rd!");
await _page.ClickAsync("[data-testid='login-btn']");
await Expect(_page).ToHaveURLAsync(new Regex("/dashboard"));
}
}
ロケーターと操作
推奨されるロケーター戦略
// BEST: Role-based (accessible and semantic)
var submitBtn = Page.GetByRole(AriaRole.Button, new() { Name = "Submit Order" });
// GOOD: Test ID (stable, explicit)
var emailInput = Page.Locator("[data-testid='email-input']");
// GOOD: Label text (user-visible, accessible)
var nameField = Page.GetByLabel("Full Name");
// GOOD: Placeholder (user-visible)
var searchBox = Page.GetByPlaceholder("Search products...");
// AVOID: CSS class (fragile, changes with styling)
var card = Page.Locator(".card-primary");
// AVOID: XPath (brittle, hard to read)
var cell = Page.Locator("//table/tbody/tr[1]/td[2]");
よく使う操作
// Text input
await Page.FillAsync("[data-testid='name']", "Alice Johnson");
// Click
await Page.ClickAsync("[data-testid='submit']");
// Select dropdown
await Page.SelectOptionAsync("[data-testid='country']", "US");
// Checkbox / radio
await Page.CheckAsync("[data-testid='agree-terms']");
// File upload
await Page.SetInputFilesAsync("[data-testid='avatar']", "testdata/photo.jpg");
// Keyboard
await Page.Keyboard.PressAsync("Enter");
await Page.Keyboard.TypeAsync("search query");
// Hover (for dropdowns, tooltips)
await Page.HoverAsync("[data-testid='user-menu']");
アサーション(Expect API)
Playwright のアサーションは、条件が満たされるかタイムアウトするまで自動で再試行します。
// Element visibility
await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
await Expect(Page.Locator("[data-testid='spinner']")).ToBeHiddenAsync();
// Text content
await Expect(Page.Locator("[data-testid='total']")).ToHaveTextAsync("$99.99");
await Expect(Page.Locator("[data-testid='status']")).ToContainTextAsync("Completed");
// Attribute
await Expect(Page.Locator("[data-testid='submit']")).ToBeEnabledAsync();
await Expect(Page.Locator("[data-testid='email']")).ToHaveValueAsync("[email protected]");
// Page-level
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));
await Expect(Page).ToHaveTitleAsync("Order Details - My App");
// Count
await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(5);
ネットワークのインターセプト
API レスポンスのモック
[Fact]
public async Task OrderList_WithMockedApi_DisplaysOrders()
{
// Intercept API calls and return mock data
await Page.RouteAsync("**/api/orders", async route =>
{
var json = JsonSerializer.Serialize(new[]
{
new { Id = 1, CustomerName = "Alice", Total = 99.99 },
new { Id = 2, CustomerName = "Bob", Total = 149.50 }
});
await route.FulfillAsync(new RouteFulfillOptions
{
Status = 200,
ContentType = "application/json",
Body = json
});
});
await Page.GotoAsync("https://localhost:5001/orders");
await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(2);
}
ネットワークリクエストの待機
[Fact]
public async Task CreateOrder_SubmitForm_WaitsForApiResponse()
{
await Page.GotoAsync("https://localhost:5001/orders/new");
await Page.FillAsync("[data-testid='customer']", "Alice");
await Page.FillAsync("[data-testid='amount']", "99.99");
// Wait for the API call triggered by form submission
var responseTask = Page.WaitForResponseAsync(
response => response.Url.Contains("/api/orders") && response.Status == 201);
await Page.ClickAsync("[data-testid='submit']");
var response = await responseTask;
Assert.Equal(201, response.Status);
}
CI でのブラウザーキャッシュ
CI の実行ごとにブラウザーバイナリをダウンロードすると時間がかかります(500MB 以上)。キャッシュを使ってビルドを高速化しましょう。
GitHub Actions のキャッシュ
# .github/workflows/e2e-tests.yml
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build
run: dotnet build tests/MyApp.E2E/
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('tests/MyApp.E2E/MyApp.E2E.csproj') }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Install Playwright system deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps
- name: Run E2E tests
run: dotnet test tests/MyApp.E2E/
Azure DevOps のキャッシュ
# azure-pipelines.yml
steps:
- task: Cache@2
inputs:
key: 'playwright | "$(Agent.OS)" | tests/MyApp.E2E/MyApp.E2E.csproj'
path: $(HOME)/.cache/ms-playwright
restoreKeys: |
playwright | "$(Agent.OS)"
cacheHitVar: PLAYWRIGHT_CACHE_RESTORED
displayName: Cache Playwright browsers
- script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps
condition: ne(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
displayName: Install Playwright browsers
- script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps
condition: eq(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
displayName: Install Playwright system deps (cached browsers)
- script: dotnet test tests/MyApp.E2E/
displayName: Run E2E tests
キャッシュキーの戦略
キャッシュキーには次の項目を含めます。
- OS: ブラウザーバイナリはプラットフォームごとに異なります
- プロジェクトファイルのハッシュ: Playwright のバージョンによってブラウザーのバージョンが決まるため、パッケージのバージョンを変更するとキャッシュが無効になります
- フォールバックキー: プロジェクトファイルが変更された場合も、キャッシュの一部を復元できるようにします
トレースビューアー
Playwright のトレースビューアーは、失敗のデバッグに使えるテスト実行の完全な記録を保存します。各トレースにはスクリーンショット、DOM スナップショット、ネットワークログ、コンソール出力が含まれます。
トレースを有効にする
public class TracedTests : IAsyncLifetime
{
private IPlaywright _playwright = null!;
private IBrowser _browser = null!;
private IBrowserContext _context = null!;
public IPage Page { get; private set; } = null!;
public async ValueTask InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync();
_context = await _browser.NewContextAsync();
// Start tracing before each test
await _context.Tracing.StartAsync(new TracingStartOptions
{
Screenshots = true,
Snapshots = true,
Sources = true
});
Page = await _context.NewPageAsync();
}
public async ValueTask DisposeAsync()
{
// Save trace on failure (check test result in xUnit requires custom wrapper)
await _context.Tracing.StopAsync(new TracingStopOptions
{
Path = Path.Combine("test-results", "traces",
$"trace-{DateTime.UtcNow:yyyyMMdd-HHmmss}.zip")
});
await Page.CloseAsync();
await _context.CloseAsync();
await _browser.CloseAsync();
_playwright.Dispose();
}
}
トレースを表示する
# Open trace file in browser
pwsh bin/Debug/net8.0/playwright.ps1 show-trace test-results/traces/trace-20260101-120000.zip
# Or use the online trace viewer
# Upload the .zip to https://trace.playwright.dev/
失敗時のみトレースを記録する
ストレージの使用量を抑えるため、テストが失敗した場合のみトレースを保存します。
// In a custom test class or middleware
public async Task RunWithTrace(Func<IPage, Task> testAction, string testName)
{
await _context.Tracing.StartAsync(new TracingStartOptions
{
Screenshots = true,
Snapshots = true,
Sources = true
});
try
{
await testAction(Page);
// Test passed -- discard trace
await _context.Tracing.StopAsync();
}
catch
{
// Test failed -- save trace for debugging
await _context.Tracing.StopAsync(new TracingStopOptions
{
Path = $"test-results/traces/{testName}.zip"
});
throw;
}
}
Codegen
Playwright のコードジェネレーターはブラウザー操作を記録し、テストコードを生成します。テストのひな形をすばやく作成するために使い、その後、生成されたコードを調整します。
Codegen の実行
# Open codegen with your app URL
pwsh bin/Debug/net8.0/playwright.ps1 codegen https://localhost:5001
# With specific browser
pwsh bin/Debug/net8.0/playwright.ps1 codegen --browser firefox https://localhost:5001
# With device emulation
pwsh bin/Debug/net8.0/playwright.ps1 codegen --device "iPhone 15" https://localhost:5001
# With saved authentication state
pwsh bin/Debug/net8.0/playwright.ps1 codegen --save-storage auth.json https://localhost:5001
Codegen のベストプラクティス
- codegen は最終的なテストではなく、出発点として使います。 生成されたコードでは、壊れやすいセレクターが使われ、適切なアサーションが不足していることがよくあります。
- 生成後すぐに、生成されたセレクターを
data-testidまたはロールベースのロケーターに置き換えます。 - 意味のあるアサーションを追加します。 codegen は操作を記録しますが、何を検証すべきかは判断できません。期待される結果に対する
Expect()呼び出しを追加します。 - 生成されたコードからページオブジェクトを抽出します。 関連する操作をページオブジェクトのメソッドにまとめます。
Codegen による生成前と調整後
// GENERATED by codegen (fragile, no assertions):
await page.GotoAsync("https://localhost:5001/orders");
await page.Locator("#root > div > main > div:nth-child(2) > button").ClickAsync();
await page.GetByPlaceholder("Customer name").FillAsync("Alice");
await page.GetByPlaceholder("Amount").FillAsync("99.99");
await page.Locator("form > button[type='submit']").ClickAsync();
// REFINED (stable selectors, proper assertions):
await Page.GotoAsync("https://localhost:5001/orders");
await Page.ClickAsync("[data-testid='new-order-btn']");
await Page.FillAsync("[data-testid='customer-name']", "Alice");
await Page.FillAsync("[data-testid='amount']", "99.99");
await Page.ClickAsync("[data-testid='submit-order']");
await Expect(Page.Locator("[data-testid='success-toast']"))
.ToBeVisibleAsync();
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));
複数ブラウザーでのテスト
複数のブラウザーでテストを実行する
// Using Playwright xUnit base class with environment variable
// Set BROWSER=chromium|firefox|webkit via CLI or CI config
public class CrossBrowserTests : PageTest
{
[Fact]
public async Task OrderFlow_WorksAcrossBrowsers()
{
// This test runs in whichever browser BROWSER env var specifies
await Page.GotoAsync("https://localhost:5001/orders/new");
await Page.FillAsync("[data-testid='customer']", "Alice");
await Page.ClickAsync("[data-testid='submit']");
await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
}
}
# Run tests in each browser
BROWSER=chromium dotnet test
BROWSER=firefox dotnet test
BROWSER=webkit dotnet test
CI のマトリックス戦略
# GitHub Actions matrix for multi-browser
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- name: Run E2E tests
run: dotnet test tests/MyApp.E2E/
env:
BROWSER: ${{ matrix.browser }}
主な原則
- 生の xUnit
Assertではなく、Playwright のアサーション(Expect)を使用してください。 Playwright のアサーションは設定可能なタイムアウトで自動的に再試行するため、タイミングに起因する不安定さを解消できます。 - CI でブラウザーのバイナリをキャッシュしてください。 実行ごとに 500MB 以上のブラウザーをダウンロードすると、時間と帯域幅が無駄になります。OS と Playwright のバージョンごとにキャッシュしてください。
- CI の失敗をデバッグできるよう、トレースビューアーを有効にしてください。 トレースには、テストを再実行せずに失敗を再現するために必要なすべての情報が記録されます。
- テストのたたき台には codegen を使用し、その後で調整してください。 生成されたコードを使えばすぐに始められます。手動で調整すれば、保守しやすいテストになります。
- CSS クラスや XPath よりも、ロールベースまたは
data-testidのロケーターを優先してください。 セレクターの優先順位について詳しくは、[skill:dotnet-ui-testing-core] を参照してください。
エージェントが注意すべき点
- Playwright パッケージを追加した後、ブラウザーのインストールを忘れないでください。 NuGet パッケージにはブラウザーのバイナリは含まれていません。ビルド後にインストールスクリプトを実行してください。
- 待機に
Task.Delayを使用しないでください。 Playwright の自動待機とExpectアサーションがタイミングを自動的に処理します。遅延を追加すると、テストの実行が遅くなるうえ、依然として不安定なままです。 localhostのポートをハードコードしないでください。 ベース URL には設定または環境変数を使用してください。CI 環境では、ローカル開発環境とは異なるポートが使われる場合があります。- CI で初回インストールするときは、
--with-depsを省略しないでください。 Linux 上で Playwright のブラウザーを実行するには、システムライブラリ(libgbm、libasound など)が必要です。--with-depsフラグを指定すると、それらもインストールされます。以降のキャッシュ済みの実行では、install-depsのみが必要です。 - トレースファイルをリポジトリに保存しないでください。 トレースはサイズの大きなバイナリファイルです。git の追跡対象から除外された
test-results/ディレクトリに書き込み、CI アーティファクトとしてアップロードしてください。 - テストごとに新しいブラウザーインスタンスを作成しないでください。 ブラウザーの起動には時間がかかります。
IClassFixtureまたは Playwright の xUnit 基底クラスを使って、クラス内のテストでブラウザーを共有してください。分離性を保つため、テストごとに新しいBrowserContextを作成してください。