playwright-blazor-testing
Playwrightを用いてBlazorアプリケーションのUIテストを記述し、画面遷移や要素操作、認証、Blazor特有のパターンを検証することで、アプリケーションの品質向上に貢献するSkill。
📜 元の英語説明(参考)
Write UI tests for Blazor applications (Server or WebAssembly) using Playwright. Covers navigation, interaction, authentication, selectors, and common Blazor-specific patterns.
🇯🇵 日本人クリエイター向け解説
Playwrightを用いてBlazorアプリケーションのUIテストを記述し、画面遷移や要素操作、認証、Blazor特有のパターンを検証することで、アプリケーションの品質向上に貢献するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o playwright-blazor-testing.zip https://jpskill.com/download/8719.zip && unzip -o playwright-blazor-testing.zip && rm playwright-blazor-testing.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/8719.zip -OutFile "$d\playwright-blazor-testing.zip"; Expand-Archive "$d\playwright-blazor-testing.zip" -DestinationPath $d -Force; ri "$d\playwright-blazor-testing.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
playwright-blazor-testing.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
playwright-blazor-testingフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
[Skill 名] playwright-blazor-testing
Playwright を使用した Blazor アプリケーションのテスト
この Skill を使用する場面
この Skill は、以下のような場合に使用します。
- Blazor Server または WebAssembly アプリケーションのエンドツーエンド UI テストを作成する
- インタラクティブなコンポーネント、フォーム、およびユーザーワークフローをテストする
- 認証および認可フローを検証する
- Blazor Server での SignalR ベースのリアルタイム更新をテストする
- ビジュアルリグレッションテストのためにスクリーンショットをキャプチャする
- レスポンシブデザインとモバイルエミュレーションをテストする
- ブラウザの開発者ツールで UI の問題をデバッグする
コア原則
- レンダリングを待機する - Blazor は非同期でレンダリングされます。適切な待機戦略を使用してください。
- テスト属性 - 安定したセレクターのために、
data-testまたはdata-testid属性を使用してください。 - デフォルトでヘッドレス - CI ではヘッドレスでテストを実行し、ローカルデバッグではヘッド付きで実行します。
- エラー UI を処理する - 未処理の例外をキャッチするために、常に
#blazor-error-uiを確認してください。 - ネットワーク待機状態を避ける - Blazor のナビゲーションはネットワークロードをトリガーしません。DOM の変更を待機してください。
- ブラウザチャネルを固定する - 再現性のために、特定のブラウザチャネル (msedge, chrome) を使用してください。
必要な NuGet パッケージ
<ItemGroup>
<PackageReference Include="Microsoft.Playwright" Version="*" />
<PackageReference Include="Microsoft.Playwright.MSTest" Version="*" />
<!-- OR for xUnit -->
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
</ItemGroup>
インストール
テストを実行する前に、Playwright ブラウザをインストールしてください。
pwsh -Command "playwright install --with-deps"
パターン 1: 基本的な Playwright のセットアップ
using Microsoft.Playwright;
public class PlaywrightFixture : IAsyncLifetime
{
private IPlaywright? _playwright;
private IBrowser? _browser;
public IBrowser Browser => _browser
?? throw new InvalidOperationException("Browser not initialized");
public async Task InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new()
{
Headless = true,
// For CI/debugging, you might want:
// Headless = Environment.GetEnvironmentVariable("CI") != null,
// SlowMo = 100 // Slow down actions for debugging
});
}
public async Task DisposeAsync()
{
if (_browser is not null)
await _browser.DisposeAsync();
_playwright?.Dispose();
}
}
パターン 2: Blazor アプリでのナビゲーション
初回ページロード (従来のナビゲーション)
[Fact]
public async Task InitialPageLoad()
{
var page = await _fixture.Browser.NewPageAsync();
// 最初のロードは従来の HTTP ナビゲーションです
await page.GotoAsync("https://localhost:5001");
// Blazor が初期化されるのを待ちます
await page.WaitForSelectorAsync("h1:has-text('Welcome')");
Assert.True(await page.IsVisibleAsync("h1:has-text('Welcome')"));
}
アプリ内ナビゲーション (ページリロードなし)
Blazor はクライアントサイドルーティングを使用するため、後続のナビゲーションはページリロードをトリガーしません。
[Fact]
public async Task InternalNavigation()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync("https://localhost:5001");
// 方法 1: ナビゲーションリンクをクリックする
await page.GetByRole(AriaRole.Link, new() { Name = "Counter" })
.ClickAsync();
// 新しいページコンテンツを待機します (ネットワークアイドルではありません!)
await page.WaitForSelectorAsync("h1:has-text('Counter')");
// 方法 2: プログラムによるナビゲーション (Blazor 8+)
await page.EvaluateAsync("window.Blazor.navigateTo('/fetchdata')");
await page.WaitForSelectorAsync("h1:has-text('Weather')");
// 方法 3: 直接 URL ナビゲーション (完全なリロードが発生します)
await page.GotoAsync("https://localhost:5001/counter");
await page.WaitForSelectorAsync("h1:has-text('Counter')");
}
Blazor の待機戦略
// ❌ ダメ: ネットワークアイドルを待機する (Blazor はページをリロードしません)
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// ✅ OK: 特定の DOM 要素を待機する
await page.WaitForSelectorAsync("h1:has-text('My Page')");
// ✅ OK: 要素の可視性を待機する
await page.Locator("[data-test='content']").WaitForAsync();
// ✅ OK: URL の変更を待機する
await page.WaitForURLAsync("**/counter");
パターン 3: テスト属性を使用した安定したセレクター
Blazor コンポーネント内
<!-- 安定したセレクターのために data-test 属性を追加します -->
<button data-test="submit-button" @onclick="HandleSubmit">
Submit
</button>
<input data-test="username-input" @bind="Username" />
<div data-test="result-container">
@Result
</div>
テスト内
[Fact]
public async Task FormSubmission()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync(baseUrl);
// data-test 属性を持つ要素には GetByTestId を使用します
await page.GetByTestId("username-input").FillAsync("testuser");
await page.GetByTestId("password-input").FillAsync("password123");
await page.GetByTestId("submit-button").ClickAsync();
// 結果を検証します
var result = await page.GetByTestId("result-container").TextContentAsync();
Assert.Contains("Success", result);
}
パターン 4: 認証の処理
インタラクティブログイン
[Fact]
public async Task LoginFlow()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync($"{baseUrl}/login");
// ログインフォームに入力します
await page.FillAsync("input[name='username']", "alice");
await page.FillAsync("input[name='password']", "P@ssw0rd");
await page.ClickAsync("button[type='submit']");
// ダッシュボードへのリダイレクトを待機します
await page.WaitForURLAsync("**/dashboard");
// ログインしていることを確認します
var username = await page.TextContentAsync("[data-test='user-name']");
Assert.Equal("alice", username);
}
Cookie インジェクション (より高速)
[Fact]
public async Task AuthenticatedAccess_ViaCookie()
{
var page = await _fixture.Browser.NewPageAsync();
// インジェクト
(原文がここで切り詰められています) 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Testing Blazor Applications with Playwright
When to Use This Skill
Use this skill when:
- Writing end-to-end UI tests for Blazor Server or WebAssembly applications
- Testing interactive components, forms, and user workflows
- Verifying authentication and authorization flows
- Testing SignalR-based real-time updates in Blazor Server
- Capturing screenshots for visual regression testing
- Testing responsive designs and mobile emulation
- Debugging UI issues with browser developer tools
Core Principles
- Wait for Rendering - Blazor renders asynchronously; use proper wait strategies
- Test Attributes - Use
data-testordata-testidattributes for stable selectors - Headless by Default - Run tests headless in CI, headed for local debugging
- Handle Error UI - Always check for
#blazor-error-uito catch unhandled exceptions - Avoid Network Wait States - Blazor navigation doesn't trigger network loads; wait for DOM changes
- Pin Browser Channels - Use specific browser channels (msedge, chrome) for reproducibility
Required NuGet Packages
<ItemGroup>
<PackageReference Include="Microsoft.Playwright" Version="*" />
<PackageReference Include="Microsoft.Playwright.MSTest" Version="*" />
<!-- OR for xUnit -->
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
</ItemGroup>
Installation
Before running tests, install Playwright browsers:
pwsh -Command "playwright install --with-deps"
Pattern 1: Basic Playwright Setup
using Microsoft.Playwright;
public class PlaywrightFixture : IAsyncLifetime
{
private IPlaywright? _playwright;
private IBrowser? _browser;
public IBrowser Browser => _browser
?? throw new InvalidOperationException("Browser not initialized");
public async Task InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new()
{
Headless = true,
// For CI/debugging, you might want:
// Headless = Environment.GetEnvironmentVariable("CI") != null,
// SlowMo = 100 // Slow down actions for debugging
});
}
public async Task DisposeAsync()
{
if (_browser is not null)
await _browser.DisposeAsync();
_playwright?.Dispose();
}
}
Pattern 2: Navigation in Blazor Apps
Initial Page Load (Classic Navigation)
[Fact]
public async Task InitialPageLoad()
{
var page = await _fixture.Browser.NewPageAsync();
// First load is classic HTTP navigation
await page.GotoAsync("https://localhost:5001");
// Wait for Blazor to initialize
await page.WaitForSelectorAsync("h1:has-text('Welcome')");
Assert.True(await page.IsVisibleAsync("h1:has-text('Welcome')"));
}
In-App Navigation (No Page Reload)
Blazor uses client-side routing, so subsequent navigations don't trigger page reloads:
[Fact]
public async Task InternalNavigation()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync("https://localhost:5001");
// Method 1: Click a navigation link
await page.GetByRole(AriaRole.Link, new() { Name = "Counter" })
.ClickAsync();
// Wait for the new page content (NOT network idle!)
await page.WaitForSelectorAsync("h1:has-text('Counter')");
// Method 2: Programmatic navigation (Blazor 8+)
await page.EvaluateAsync("window.Blazor.navigateTo('/fetchdata')");
await page.WaitForSelectorAsync("h1:has-text('Weather')");
// Method 3: Direct URL navigation (causes full reload)
await page.GotoAsync("https://localhost:5001/counter");
await page.WaitForSelectorAsync("h1:has-text('Counter')");
}
Wait Strategies for Blazor
// ❌ DON'T: Wait for network idle (Blazor doesn't reload pages)
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// ✅ DO: Wait for specific DOM elements
await page.WaitForSelectorAsync("h1:has-text('My Page')");
// ✅ DO: Wait for element visibility
await page.Locator("[data-test='content']").WaitForAsync();
// ✅ DO: Wait for URL change
await page.WaitForURLAsync("**/counter");
Pattern 3: Stable Selectors with Test Attributes
In Your Blazor Components
<!-- Add data-test attributes for stable selectors -->
<button data-test="submit-button" @onclick="HandleSubmit">
Submit
</button>
<input data-test="username-input" @bind="Username" />
<div data-test="result-container">
@Result
</div>
In Your Tests
[Fact]
public async Task FormSubmission()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync(baseUrl);
// Use GetByTestId for elements with data-test attributes
await page.GetByTestId("username-input").FillAsync("testuser");
await page.GetByTestId("password-input").FillAsync("password123");
await page.GetByTestId("submit-button").ClickAsync();
// Verify result
var result = await page.GetByTestId("result-container").TextContentAsync();
Assert.Contains("Success", result);
}
Pattern 4: Handling Authentication
Interactive Login
[Fact]
public async Task LoginFlow()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync($"{baseUrl}/login");
// Fill login form
await page.FillAsync("input[name='username']", "alice");
await page.FillAsync("input[name='password']", "P@ssw0rd");
await page.ClickAsync("button[type='submit']");
// Wait for redirect to dashboard
await page.WaitForURLAsync("**/dashboard");
// Verify logged in
var username = await page.TextContentAsync("[data-test='user-name']");
Assert.Equal("alice", username);
}
Cookie Injection (Faster)
[Fact]
public async Task AuthenticatedAccess_ViaCookie()
{
var page = await _fixture.Browser.NewPageAsync();
// Inject authentication cookie
await page.Context.AddCookiesAsync(new[]
{
new Cookie
{
Name = ".AspNetCore.Cookies",
Value = GenerateAuthCookie("alice"),
Url = baseUrl,
Secure = true,
HttpOnly = true
}
});
// Navigate directly to protected page
await page.GotoAsync($"{baseUrl}/dashboard");
// Already authenticated!
var username = await page.TextContentAsync("[data-test='user-name']");
Assert.Equal("alice", username);
}
private string GenerateAuthCookie(string username)
{
// Generate a valid authentication cookie
// This requires access to your app's cookie encryption keys
// OR use a test endpoint that generates valid cookies
// OR perform actual login once and reuse the cookie
}
OAuth/External Provider Mocking
// Use route interception to mock OAuth redirects
await page.RouteAsync("**/signin-microsoft", async route =>
{
// Intercept OAuth redirect and return mock response
await route.FulfillAsync(new()
{
Status = 302,
Headers = new Dictionary<string, string>
{
["Location"] = $"{baseUrl}/signin-callback?code=mock_auth_code"
}
});
});
Pattern 5: Click Events and Touch Interactions
[Fact]
public async Task ClickInteractions()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync(baseUrl);
// Standard click
await page.GetByText("Click Me").ClickAsync();
// Right-click
await page.ClickAsync("[data-test='context-menu']", new()
{
Button = MouseButton.Right
});
// Double-click
await page.DblClickAsync("[data-test='item']");
// Hover then click dropdown
var menu = page.Locator("#profile-menu");
await menu.HoverAsync();
await menu.GetByText("Sign out").ClickAsync();
// Touch events (mobile emulation)
await page.EmulateMediaAsync(new() { Media = Media.Screen });
await page.Touchscreen.TapAsync(150, 300);
}
Pattern 6: Form Handling
[Fact]
public async Task ComplexForm()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync($"{baseUrl}/form");
// Text input
await page.FillAsync("[data-test='name']", "John Doe");
// Select dropdown
await page.SelectOptionAsync("[data-test='country']", "US");
// Checkbox
await page.CheckAsync("[data-test='terms']");
// Radio button
await page.CheckAsync("[data-test='option-a']");
// File upload
await page.SetInputFilesAsync("[data-test='file-input']",
"/path/to/test-file.pdf");
// Submit
await page.ClickAsync("[data-test='submit']");
// Wait for success message
await page.WaitForSelectorAsync("[data-test='success-message']");
}
Pattern 7: Handling Blazor Error UI
Blazor shows an error overlay when unhandled exceptions occur. Always check for this:
public static async Task AssertNoBlazorErrors(this IPage page)
{
var errorUi = page.Locator("#blazor-error-ui");
if (await errorUi.IsVisibleAsync())
{
var errorText = await errorUi.InnerTextAsync();
Assert.Fail($"Blazor error occurred: {errorText}");
}
}
[Fact]
public async Task Page_ShouldNotHaveErrors()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync(baseUrl);
// Perform some actions
await page.ClickAsync("[data-test='action-button']");
// Verify no errors occurred
await page.AssertNoBlazorErrors();
}
Pattern 8: Testing Real-Time Updates (SignalR)
Blazor Server uses SignalR for real-time communication:
[Fact]
public async Task RealTimeUpdates()
{
// Open two browser contexts (simulating two users)
var page1 = await _fixture.Browser.NewPageAsync();
var page2 = await _fixture.Browser.NewPageAsync();
await page1.GotoAsync($"{baseUrl}/drawing");
await page2.GotoAsync($"{baseUrl}/drawing");
// User 1 draws something
await page1.ClickAsync("[data-test='draw-button']");
await page1.Mouse.ClickAsync(100, 100);
// User 2 should see the update
await page2.WaitForSelectorAsync("[data-test='drawing-canvas']");
// Verify both pages show the same content
var canvas1 = await page1.GetByTestId("drawing-canvas")
.GetAttributeAsync("data-strokes");
var canvas2 = await page2.GetByTestId("drawing-canvas")
.GetAttributeAsync("data-strokes");
Assert.Equal(canvas1, canvas2);
}
Pattern 9: Screenshot and Visual Testing
[Fact]
public async Task CaptureScreenshots()
{
var page = await _fixture.Browser.NewPageAsync();
await page.GotoAsync(baseUrl);
// Full page screenshot
await page.ScreenshotAsync(new()
{
Path = "screenshots/homepage.png",
FullPage = true
});
// Element screenshot
var header = page.Locator("header");
await header.ScreenshotAsync(new()
{
Path = "screenshots/header.png"
});
// Screenshot with viewport size
await page.SetViewportSizeAsync(1920, 1080);
await page.ScreenshotAsync(new()
{
Path = "screenshots/desktop.png"
});
// Mobile viewport
await page.SetViewportSizeAsync(375, 667);
await page.ScreenshotAsync(new()
{
Path = "screenshots/mobile.png"
});
}
Pattern 10: Running Against HTTPS with Dev Certs
public async Task InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new()
{
Headless = true,
// Ignore certificate errors for local dev certs
Args = new[] { "--ignore-certificate-errors" }
});
}
For stricter setups, export and trust the dev certificate:
dotnet dev-certs https --export-path cert.pfx -p YourPassword
Common Selectors for Blazor Components
// By role (best for accessibility)
await page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
await page.GetByRole(AriaRole.Link, new() { Name = "Home" });
await page.GetByRole(AriaRole.Heading, new() { Name = "Welcome" });
// By test ID
await page.GetByTestId("user-profile");
// By text content
await page.GetByText("Hello, World!");
// By label (for inputs)
await page.GetByLabel("Email Address");
// By placeholder
await page.GetByPlaceholder("Enter your name");
// CSS selectors (use sparingly)
await page.Locator(".mud-button-primary");
await page.Locator("#login-form");
// XPath (use as last resort)
await page.Locator("xpath=//button[contains(text(), 'Submit')]");
Parallelization Considerations
Blazor Server uses SignalR websockets. Multiple Playwright tests can saturate connections:
// Limit parallel execution for Blazor Server tests
[Collection("Blazor Server")]
public class BlazorServerTests { }
// In AssemblyInfo.cs or test startup
[assembly: CollectionBehavior(MaxParallelThreads = 2)]
Blazor WebAssembly doesn't have this limitation and can run fully parallel.
CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Install Playwright Browsers
run: pwsh -Command "playwright install --with-deps"
- name: Build
run: dotnet build -c Release
- name: Run Playwright Tests
run: |
dotnet test tests/YourApp.UITests \
--no-build \
-c Release \
--logger trx
- name: Upload Screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-screenshots
path: "**/screenshots/"
- name: Upload Test Results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: "**/TestResults/*.trx"
Debugging Tips
- Run Headed - Set
Headless = falseto watch tests execute - Slow Motion - Add
SlowMo = 500to slow down actions - Pause Execution - Call
await page.PauseAsync()to open Playwright Inspector - Console Logs - Capture browser console:
page.Console += (_, msg) => Console.WriteLine(msg.Text); - Network Traffic - Monitor requests:
page.Request += (_, req) => Console.WriteLine(req.Url); - Screenshots on Failure - Always capture screenshots in catch blocks
Best Practices
- Use data-test attributes - More stable than CSS classes or IDs
- Prefer semantic selectors - Use roles, labels, and text content
- Wait for specific elements - Don't use blanket delays
- Check for Blazor errors - Always verify
#blazor-error-uiis not visible - Test with multiple viewports - Verify responsive design
- Reuse browser contexts - Faster than creating new browsers
- Clean up resources - Always dispose pages and browsers
- Use collections for Blazor Server - Avoid SignalR connection saturation
- Capture screenshots on failure - Essential for debugging CI failures
- Pin browser channels - Use specific channels for reproducibility
Advanced: Custom Wait Helpers
public static class PlaywrightExtensions
{
public static async Task WaitForBlazorAsync(this IPage page)
{
// Wait for Blazor to finish rendering
await page.EvaluateAsync(@"
() => new Promise(resolve => {
if (typeof Blazor !== 'undefined') {
resolve();
} else {
const interval = setInterval(() => {
if (typeof Blazor !== 'undefined') {
clearInterval(interval);
resolve();
}
}, 100);
}
})
");
}
public static async Task WaitForNoSpinnersAsync(
this IPage page,
int timeout = 5000)
{
var locator = page.Locator(".spinner, .loading");
await locator.WaitForAsync(new()
{
State = WaitForSelectorState.Hidden,
Timeout = timeout
});
}
public static async Task FillWithValidationAsync(
this IPage page,
string selector,
string value)
{
await page.FillAsync(selector, value);
// Trigger blur to activate validation
await page.Locator(selector).BlurAsync();
// Wait a bit for validation to complete
await Task.Delay(100);
}
}