Claude
Skills
Sign in
Back

playwright-blazor-testing

Included with Lifetime
$97 forever

Write UI tests for Blazor applications (Server or WebAssembly) using Playwright. Covers navigation, interaction, authentication, selectors, and common Blazor-specific patterns.

Design

What this skill does


# 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

1. **Wait for Rendering** - Blazor renders asynchronously; use proper wait strategies
2. **Test Attributes** - Use `data-test` or `data-testid` attributes for stable selectors
3. **Headless by Default** - Run tests headless in CI, headed for local debugging
4. **Handle Error UI** - Always check for `#blazor-error-ui` to catch unhandled exceptions
5. **Avoid Network Wait States** - Blazor navigation doesn't trigger network loads; wait for DOM changes
6. **Pin Browser Channels** - Use specific browser channels (msedge, chrome) for reproducibility

## Required NuGet Packages

```xml
<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:

```bash
pwsh -Command "playwright install --with-deps"
```

## Pattern 1: Basic Playwright Setup

```csharp
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)

```csharp
[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:

```csharp
[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

```csharp
// ❌ 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

```razor
<!-- 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

```csharp
[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

```csharp
[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)

```csharp
[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

```csharp
// 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

```csharp
[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 }

Related in Design