playwright-blazor-testing

Escreva testes de UI para aplicações Blazor (Server ou WebAssembly) usando Playwright. Aborda navegação, interação, autenticação, seletores e padrões comuns específicos do Blazor.

O que é playwright-blazor-testing?

Esta skill fornece um guia completo para escrever testes de interface do usuário (UI) para aplicações Blazor, tanto no modo Server quanto WebAssembly, utilizando a ferramenta Playwright. Ela é projetada para desenvolvedores e engenheiros de qualidade que precisam criar testes end-to-end robustos, cobrindo desde a navegação inicial e interação com componentes até cenários complexos como autenticação, testes de formulários e verificação de atualizações em tempo real via SignalR. A skill cobre as plataformas de teste como MSTest e xUnit, e oferece padrões específicos para lidar com as particularidades do Blazor, como a renderização assíncrona e a navegação sem recarregamento de página. Com exemplos de código detalhados, ela ensina a usar seletores estáveis, evitar estados de espera incorretos e integrar os testes em pipelines de CI/CD como GitHub Actions, tornando o processo de teste mais confiável e eficiente para aplicações Blazor.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/playwright-blazor

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Testando Aplicações Blazor com Playwright

Quando Usar Esta Skill

Use esta skill quando:

  • Escrevendo testes de UI end-to-end para aplicações Blazor Server ou WebAssembly
  • Testando componentes interativos, formulários e fluxos de trabalho do usuário
  • Verificando fluxos de autenticação e autorização
  • Testando atualizações em tempo real baseadas em SignalR no Blazor Server
  • Capturando capturas de tela para testes de regressão visual
  • Testando designs responsivos e emulação móvel
  • Depurando problemas de UI com ferramentas de desenvolvedor do navegador

Princípios Fundamentais

  1. Aguarde a Renderização - O Blazor renderiza de forma assíncrona; use estratégias de espera adequadas
  2. Atributos de Teste - Use atributos data-test ou data-testid para seletores estáveis
  3. Headless por Padrão - Execute testes headless em CI, com cabeçalho para depuração local
  4. Lide com a UI de Erro - Sempre verifique se há #blazor-error-ui para capturar exceções não tratadas
  5. Evite Estados de Espera de Rede - A navegação no Blazor não dispara carregamentos de rede; aguarde mudanças no DOM
  6. Fixar Canais do Navegador - Use canais de navegador específicos (msedge, chrome) para reprodutibilidade

Pacotes NuGet Necessários

<ItemGroup>
  <PackageReference Include="Microsoft.Playwright" Version="*" />
  <PackageReference Include="Microsoft.Playwright.MSTest" Version="*" />
  <!-- OU para xUnit -->
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />
</ItemGroup>

Instalação

Antes de executar os testes, instale os navegadores Playwright:

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

Padrão 1: Configuração Básica do 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,
            // Para CI/depuração, você pode querer:
            // Headless = Environment.GetEnvironmentVariable("CI") != null,
            // SlowMo = 100 // Desacelera as ações para depuração
        });
    }

    public async Task DisposeAsync()
    {
        if (_browser is not null)
            await _browser.DisposeAsync();

        _playwright?.Dispose();
    }
}

Padrão 2: Navegação em Aplicações Blazor

Carregamento de Página Inicial (Navegação Clássica)

[Fact]
public async Task InitialPageLoad()
{
    var page = await _fixture.Browser.NewPageAsync();

    // O primeiro carregamento é uma navegação HTTP clássica
    await page.GotoAsync("https://localhost:5001");

    // Aguarde a inicialização do Blazor
    await page.WaitForSelectorAsync("h1:has-text('Welcome')");

    Assert.True(await page.IsVisibleAsync("h1:has-text('Welcome')"));
}

Navegação Interna (Sem Recarregamento de Página)

O Blazor usa roteamento do lado do cliente, portanto, navegações subsequentes não disparam recarregamentos de página:

[Fact]
public async Task InternalNavigation()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync("https://localhost:5001");

    // Método 1: Clicar em um link de navegação
    await page.GetByRole(AriaRole.Link, new() { Name = "Counter" })
        .ClickAsync();

    // Aguarde o novo conteúdo da página (NÃO network idle!)
    await page.WaitForSelectorAsync("h1:has-text('Counter')");

    // Método 2: Navegação programática (Blazor 8+)
    await page.EvaluateAsync("window.Blazor.navigateTo('/fetchdata')");
    await page.WaitForSelectorAsync("h1:has-text('Weather')");

    // Método 3: Navegação por URL direta (causa recarregamento completo)
    await page.GotoAsync("https://localhost:5001/counter");
    await page.WaitForSelectorAsync("h1:has-text('Counter')");
}

Estratégias de Espera para Blazor

// ❌ NÃO FAÇA: Aguardar network idle (Blazor não recarrega páginas)
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

// ✅ FAÇA: Aguardar elementos específicos do DOM
await page.WaitForSelectorAsync("h1:has-text('My Page')");

// ✅ FAÇA: Aguardar visibilidade do elemento
await page.Locator("[data-test='content']").WaitForAsync();

// ✅ FAÇA: Aguardar mudança de URL
await page.WaitForURLAsync("**/counter");

Padrão 3: Seletores Estáveis com Atributos de Teste

Em Seus Componentes Blazor

<!-- Adicione atributos data-test para seletores estáveis -->
<button data-test="submit-button" @onclick="HandleSubmit">
    Submit
</button>

<input data-test="username-input" @bind="Username" />

<div data-test="result-container">
    @Result
</div>

Em Seus Testes

[Fact]
public async Task FormSubmission()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync(baseUrl);

    // Use GetByTestId para elementos com atributos data-test
    await page.GetByTestId("username-input").FillAsync("testuser");
    await page.GetByTestId("password-input").FillAsync("password123");
    await page.GetByTestId("submit-button").ClickAsync();

    // Verifique o resultado
    var result = await page.GetByTestId("result-container").TextContentAsync();
    Assert.Contains("Success", result);
}

Padrão 4: Lidando com Autenticação

Login Interativo

[Fact]
public async Task LoginFlow()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync($"{baseUrl}/login");

    // Preencha o formulário de login
    await page.FillAsync("input[name='username']", "alice");
    await page.FillAsync("input[name='password']", "P@ssw0rd");
    await page.ClickAsync("button[type='submit']");

    // Aguarde o redirecionamento para o dashboard
    await page.WaitForURLAsync("**/dashboard");

    // Verifique se está logado
    var username = await page.TextContentAsync("[data-test='user-name']");
    Assert.Equal("alice", username);
}

Injeção de Cookie (Mais Rápido)

[Fact]
public async Task AuthenticatedAccess_ViaCookie()
{
    var page = await _fixture.Browser.NewPageAsync();

    // Injete o cookie de autenticação
    await page.Context.AddCookiesAsync(new[]
    {
        new Cookie
        {
            Name = ".AspNetCore.Cookies",
            Value = GenerateAuthCookie("alice"),
            Url = baseUrl,
            Secure = true,
            HttpOnly = true
        }
    });

    // Navegue diretamente para a página protegida
    await page.GotoAsync($"{baseUrl}/dashboard");

    // Já autenticado!
    var username = await page.TextContentAsync("[data-test='user-name']");
    Assert.Equal("alice", username);
}

private string GenerateAuthCookie(string username)
{
    // Gere um cookie de autenticação válido
    // Isso requer acesso às chaves de criptografia de cookies do seu aplicativo
    // OU use um endpoint de teste que gere cookies válidos
    // OU faça o login real uma vez e reutilize o cookie
}

Simulação de Provedor Externo/OAuth

// Use intercepção de rota para simular redirecionamentos OAuth
await page.RouteAsync("**/signin-microsoft", async route =>
{
    // Intercepte o redirecionamento OAuth e retorne uma resposta simulada
    await route.FulfillAsync(new()
    {
        Status = 302,
        Headers = new Dictionary<string, string>
        {
            ["Location"] = $"{baseUrl}/signin-callback?code=mock_auth_code"
        }
    });
});

Padrão 5: Eventos de Clique e Interações de Toque

[Fact]
public async Task ClickInteractions()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync(baseUrl);

    // Clique padrão
    await page.GetByText("Click Me").ClickAsync();

    // Clique com botão direito
    await page.ClickAsync("[data-test='context-menu']", new()
    {
        Button = MouseButton.Right
    });

    // Clique duplo
    await page.DblClickAsync("[data-test='item']");

    // Passe o mouse e clique no menu suspenso
    var menu = page.Locator("#profile-menu");
    await menu.HoverAsync();
    await menu.GetByText("Sign out").ClickAsync();

    // Eventos de toque (emulação móvel)
    await page.EmulateMediaAsync(new() { Media = Media.Screen });
    await page.Touchscreen.TapAsync(150, 300);
}

Padrão 6: Manipulação de Formulários

[Fact]
public async Task ComplexForm()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync($"{baseUrl}/form");

    // Entrada de texto
    await page.FillAsync("[data-test='name']", "John Doe");

    // Menu suspenso de seleção
    await page.SelectOptionAsync("[data-test='country']", "US");

    // Caixa de seleção
    await page.CheckAsync("[data-test='terms']");

    // Botão de rádio
    await page.CheckAsync("[data-test='option-a']");

    // Upload de arquivo
    await page.SetInputFilesAsync("[data-test='file-input']",
        "/path/to/test-file.pdf");

    // Enviar
    await page.ClickAsync("[data-test='submit']");

    // Aguarde a mensagem de sucesso
    await page.WaitForSelectorAsync("[data-test='success-message']");
}

Padrão 7: Lidando com a UI de Erro do Blazor

O Blazor mostra uma sobreposição de erro quando ocorrem exceções não tratadas. Sempre verifique isso:

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);

    // Execute algumas ações
    await page.ClickAsync("[data-test='action-button']");

    // Verifique se nenhum erro ocorreu
    await page.AssertNoBlazorErrors();
}

Padrão 8: Testando Atualizações em Tempo Real (SignalR)

O Blazor Server usa SignalR para comunicação em tempo real:

[Fact]
public async Task RealTimeUpdates()
{
    // Abra dois contextos de navegador (simulando dois usuários)
    var page1 = await _fixture.Browser.NewPageAsync();
    var page2 = await _fixture.Browser.NewPageAsync();

    await page1.GotoAsync($"{baseUrl}/drawing");
    await page2.GotoAsync($"{baseUrl}/drawing");

    // Usuário 1 desenha algo
    await page1.ClickAsync("[data-test='draw-button']");
    await page1.Mouse.ClickAsync(100, 100);

    // Usuário 2 deve ver a atualização
    await page2.WaitForSelectorAsync("[data-test='drawing-canvas']");

    // Verifique se ambas as páginas mostram o mesmo conteúdo
    var canvas1 = await page1.GetByTestId("drawing-canvas")
        .GetAttributeAsync("data-strokes");
    var canvas2 = await page2.GetByTestId("drawing-canvas")
        .GetAttributeAsync("data-strokes");

    Assert.Equal(canvas1, canvas2);
}

Padrão 9: Captura de Tela e Teste Visual

[Fact]
public async Task CaptureScreenshots()
{
    var page = await _fixture.Browser.NewPageAsync();
    await page.GotoAsync(baseUrl);

    // Captura de tela da página inteira
    await page.ScreenshotAsync(new()
    {
        Path = "screenshots/homepage.png",
        FullPage = true
    });

    // Captura de tela de um elemento
    var header = page.Locator("header");
    await header.ScreenshotAsync(new()
    {
        Path = "screenshots/header.png"
    });

    // Captura de tela com tamanho de viewport
    await page.SetViewportSizeAsync(1920, 1080);
    await page.ScreenshotAsync(new()
    {
        Path = "screenshots/desktop.png"
    });

    // Viewport móvel
    await page.SetViewportSizeAsync(375, 667);
    await page.ScreenshotAsync(new()
    {
        Path = "screenshots/mobile.png"
    });
}

Padrão 10: Executando Contra HTTPS com Certificados de Desenvolvimento

public async Task InitializeAsync()
{
    _playwright = await Playwright.CreateAsync();

    _browser = await _playwright.Chromium.LaunchAsync(new()
    {
        Headless = true,
        // Ignore erros de certificado para certificados de desenvolvimento local
        Args = new[] { "--ignore-certificate-errors" }
    });
}

Para configurações mais rigorosas, exporte e confie no certificado de desenvolvimento:

dotnet dev-certs https --export-path cert.pfx -p YourPassword

Seletores Comuns para Componentes Blazor

// Por função (melhor para acessibilidade)
await page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
await page.GetByRole(AriaRole.Link, new() { Name = "Home" });
await page.GetByRole(AriaRole.Heading, new() { Name = "Welcome" });

// Por ID de teste
await page.GetByTestId("user-profile");

// Por conteúdo de texto
await page.GetByText("Hello, World!");

// Por rótulo (para entradas)
await page.GetByLabel("Email Address");

// Por placeholder
await page.GetByPlaceholder("Enter your name");

// Seletores CSS (use com moderação)
await page.Locator(".mud-button-primary");
await page.Locator("#login-form");

// XPath (use como último recurso)
await page.Locator("xpath=//button[contains(text(), 'Submit')]");

Considerações sobre Paralelização

O Blazor Server usa websockets SignalR. Vários testes do Playwright podem saturar as conexões:

// Limite a execução paralela para testes do Blazor Server
[Collection("Blazor Server")]
public class BlazorServerTests { }

// Em AssemblyInfo.cs ou início do teste
[assembly: CollectionBehavior(MaxParallelThreads = 2)]

O Blazor WebAssembly não tem essa limitação e pode ser executado totalmente em paralelo.

Integração CI/CD

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

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

Habilidades Relacionadas