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