| name | playwright-blazor-e2e |
| description | End-to-end testing for Blazor applications using Playwright for .NET. Use when writing E2E tests, creating test infrastructure, testing MudBlazor components, handling Blazor WebAssembly loading, debugging test failures, or setting up CI/CD test pipelines. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash(dotnet:*), Bash(pwsh:*), WebFetch |
Playwright E2E Testing for Blazor
Overview
Playwright for .NET provides cross-browser automation for testing Blazor applications. It handles Blazor's asynchronous rendering, WebAssembly loading, and SignalR connections with robust auto-waiting and retry mechanisms.
Key Capabilities
- Cross-browser testing (Chromium, Firefox, WebKit)
- Auto-waiting for elements and network requests
- Trace viewer for debugging failed tests
- Parallel test execution
- Full support for Blazor WebAssembly and Server
CRITICAL: Blazor-Specific Considerations
| Challenge | Solution |
|---|---|
| WASM loading delay | Wait for Blazor to initialize before interacting |
| Component re-renders | Use auto-retrying assertions, not Thread.Sleep |
| MudBlazor components | Use role/label locators, not CSS selectors |
| Async operations | Wait for loading indicators to disappear |
| SignalR reconnection | Handle connection state changes gracefully |
Quick Start
1. Create Test Project
# MSTest (recommended for .NET projects)
dotnet new mstest -n MyApp.E2E
cd MyApp.E2E
# Add Playwright
dotnet add package Microsoft.Playwright.MSTest
# Build to generate playwright.ps1
dotnet build
# Install browsers
pwsh bin/Debug/net8.0/playwright.ps1 install
2. Basic Blazor Test
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace MyApp.E2E;
[TestClass]
public class HomePageTests : PageTest
{
[TestMethod]
public async Task HomePage_DisplaysWelcomeMessage()
{
// Navigate and wait for Blazor to load
await Page.GotoAsync("https://localhost:5001/");
await WaitForBlazorAsync();
// Use role-based locator (accessibility-first)
var heading = Page.GetByRole(AriaRole.Heading, new() { Name = "Welcome" });
await Expect(heading).ToBeVisibleAsync();
}
private async Task WaitForBlazorAsync()
{
// Wait for Blazor WebAssembly to finish loading
await Page.WaitForFunctionAsync("window.Blazor !== undefined");
// Wait for any initial loading indicators to disappear
var loader = Page.GetByTestId("app-loading");
await Expect(loader).ToBeHiddenAsync(new() { Timeout = 30000 });
}
}
3. Configure Base URL
Override ContextOptions to set a base URL:
[TestClass]
public class BlazorTestBase : PageTest
{
public override BrowserNewContextOptions ContextOptions => new()
{
BaseURL = "https://localhost:5001",
IgnoreHTTPSErrors = true // For dev certificates
};
}
Locator Strategy (Priority Order)
Always prefer user-facing locators for resilient tests:
| Priority | Method | Example | Use When |
|---|---|---|---|
| 1 | GetByRole() |
GetByRole(AriaRole.Button, new() { Name = "Submit" }) |
Interactive elements |
| 2 | GetByLabel() |
GetByLabel("Email") |
Form inputs |
| 3 | GetByPlaceholder() |
GetByPlaceholder("Search...") |
Inputs with placeholder |
| 4 | GetByText() |
GetByText("Welcome") |
Static text content |
| 5 | GetByTestId() |
GetByTestId("submit-button") |
When other locators fail |
| 6 | Locator() |
Locator(".mud-button") |
Last resort only |
MudBlazor Component Locators
// MudButton - use role and accessible name
var saveButton = Page.GetByRole(AriaRole.Button, new() { Name = "Save" });
// MudTextField - use label
var emailField = Page.GetByLabel("Email");
// MudSelect - use label then interact
var categorySelect = Page.GetByLabel("Category");
await categorySelect.ClickAsync();
await Page.GetByRole(AriaRole.Option, new() { Name = "Electronics" }).ClickAsync();
// MudDataGrid row - use text content
var row = Page.GetByRole(AriaRole.Row).Filter(new() { HasText = "Product ABC" });
// MudDialog - use role
var dialog = Page.GetByRole(AriaRole.Dialog);
await Expect(dialog).ToBeVisibleAsync();
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
Thread.Sleep(2000) |
Slow, flaky | Use auto-waiting assertions |
Locator(".css-class") |
Brittle selectors | Use role/label locators |
| Hard-coded waits | Race conditions | Use Expect() assertions |
| Testing implementation | Breaks on refactor | Test user-visible behavior |
| No base URL | Duplicate URLs | Configure in ContextOptions |
| Ignoring loading states | Flaky tests | Wait for loaders to disappear |
Additional Resources
For detailed guidance, see:
- Test Patterns - Form submission, dialogs, grids, auth flows
- Configuration - Project structure, .runsettings, CI/CD
- Project Setup - Full project configuration
- Blazor Patterns - Blazor-specific testing patterns, assertions, and actionability
- MudBlazor Selectors - Finding MudBlazor components
- Debugging - Trace viewer, headed mode, Inspector, and debugging techniques