Playwright Blazor Testing
Write UI tests for Blazor applications (Server or WebAssembly) using Playwright. Covers navigation, interaction, authentication, selectors, and common Blazor-specific patterns.
Specification
- Skill ID
- aaronontheweb/dotnet-skills/playwright-blazor-testing
- Publisher
- aaronontheweb
- Repository
- dotnet-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
How to use it
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
aaronontheweb/dotnet-skills/playwright-blazor-testingInstalls these files- SKILL.md
What this skill tells the agent
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)More from aaronontheweb
Use this skill in a project
Open any RiverX project, search the Skills panel for aaronontheweb/dotnet-skills/playwright-blazor-testing, and install it in one click.
