Skip to content

Web Testing with Playwright

Learn how to write browser-based end-to-end tests using Playwright with Aspire's DistributedApplicationTestingBuilder.

๐Ÿ“‹ Overview

Property Value
Goal Test web applications using Playwright browser automation
Prerequisites .NET 10 SDK, Aspire project with AppHost, Node.js
Time 20 minutes

This guide demonstrates how to create end-to-end tests for your web application using Playwright and Aspire's distributed testing framework.


๐Ÿ“ฆ Required Packages

Add the following NuGet packages to your test project:

<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Playwright" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
</PackageReference>

Also add a project reference to your AppHost:

<ProjectReference Include="..\YourAppHost\YourAppHost.csproj" />

๐Ÿงช Test Structure

Namespaces

using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright;

Test Class with Shared Lifecycle

Use IAsyncLifetime to manage the distributed application and browser lifecycle across tests:

public class WebTests : IAsyncLifetime
{
    private DistributedApplication? _app;
    private IPlaywright? _playwright;
    private IBrowser? _browser;
    private string? _frontendUrl;

    public async Task InitializeAsync()
    {
        // Build and start the distributed application
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.YourAppHost>();

        // Configure services for testing
        appHost.Services.ConfigureHttpClientDefaults(clientBuilder =>
        {
            clientBuilder.AddStandardResilienceHandler();
        });

        _app = await appHost.BuildAsync();
        await _app.StartAsync();

        // Wait for resources to be ready
        var resourceNotificationService = _app.Services
            .GetRequiredService<ResourceNotificationService>();
        await resourceNotificationService
            .WaitForResourceAsync("frontend", KnownResourceStates.Running)
            .WaitAsync(TimeSpan.FromSeconds(60));

        // Get the frontend URL
        var httpClient = _app.CreateHttpClient("frontend");
        _frontendUrl = httpClient.BaseAddress?.ToString();

        // Initialize Playwright
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(
            new BrowserTypeLaunchOptions { Headless = true });
    }

    public async Task DisposeAsync()
    {
        if (_browser is not null) await _browser.CloseAsync();
        _playwright?.Dispose();

        if (_app is not null)
        {
            await _app.StopAsync();
            await _app.DisposeAsync();
        }
    }
}

Playwright Browser Installation

Add a module initializer to automatically install browsers:

using System.Runtime.CompilerServices;

public static class PlaywrightSetup
{
    [ModuleInitializer]
    public static void Initialize()
    {
        // Install Playwright browsers on first run
        Microsoft.Playwright.Program.Main(["install", "--with-deps", "chromium"]);
    }
}

๐Ÿ“ Example: Testing Page Content

This test verifies that the weather table displays the expected number of records.

Steps

  1. Arrange: Use the shared browser from the test class
  2. Act: Navigate to the page and wait for data to load
  3. Assert: Verify the page content

Code

[Fact]
public async Task WeatherTable_Should_Display_5_Records()
{
    // Arrange
    var page = await _browser!.NewPageAsync();

    try
    {
        // Act
        await page.GotoAsync(_frontendUrl!, new PageGotoOptions
        {
            WaitUntil = WaitUntilState.NetworkIdle,
            Timeout = 30000
        });

        // Wait for the weather table to load
        await page.WaitForSelectorAsync("[data-testid='weather-table']",
            new PageWaitForSelectorOptions { Timeout = 10000 });

        // Assert - Check that there are exactly 5 weather records
        var rows = await page.Locator("[data-testid='weather-tbody'] tr").AllAsync();
        Assert.Equal(5, rows.Count);

        // Verify the record count display
        var recordCountText = await page.Locator("[data-testid='record-count']").TextContentAsync();
        Assert.Contains("5", recordCountText);
    }
    finally
    {
        await page.CloseAsync();
    }
}

Example: Testing User Interactions

[Fact]
public async Task RefreshButton_Should_Update_Weather_Data()
{
    var page = await _browser!.NewPageAsync();

    try
    {
        await page.GotoAsync(_frontendUrl!, new PageGotoOptions
        {
            WaitUntil = WaitUntilState.NetworkIdle,
            Timeout = 30000
        });

        await page.WaitForSelectorAsync("[data-testid='weather-table']",
            new PageWaitForSelectorOptions { Timeout = 10000 });

        // Capture initial data
        var initialRows = await page.Locator("[data-testid='weather-tbody'] tr").AllAsync();
        var initialData = new List<string>();
        foreach (var row in initialRows)
        {
            initialData.Add(await row.TextContentAsync() ?? string.Empty);
        }

        // Click the refresh button
        await page.Locator("[data-testid='refresh-button']").ClickAsync();
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
        await Task.Delay(500); // Allow React to update DOM

        // Verify refresh count increased
        var refreshCountText = await page.Locator("[data-testid='refresh-count']").TextContentAsync();
        Assert.Contains("1", refreshCountText);
    }
    finally
    {
        await page.CloseAsync();
    }
}

๐ŸŽฌ Code Generation with Playwright

Playwright CLI can generate test code by recording your browser actions, making it faster to develop end-to-end tests.

Steps

  1. Install Playwright CLI:
npm install -g playwright@latest
  1. Start your AppHost:
dotnet run --project YourAppHost
  1. Start a recording session:
npx playwright codegen http://localhost:5000/

A Playwright Inspector window and browser will open.

  1. Record your actions:
  2. Perform the actions you want to test
  3. Use the movable menu bar for assertions

  4. Copy the generated code:

  5. In the Playwright Inspector, change Target to NUnit
  6. Copy the generated code to your test file

  7. Adapt for xUnit:

โš ๏ธ Note: The Playwright Inspector does not currently support xUnit as a target. You'll need to adapt the NUnit code:

// NUnit generated code
await Expect(page.GetByRole(AriaRole.Main)).ToContainTextAsync("Hello");

// Adapted for xUnit
await Assertions.Expect(page.GetByRole(AriaRole.Main)).ToContainTextAsync("Hello");
  1. Close the browser to end the recording session.

๐Ÿ”‘ Key Concepts

Browser Launch Options

Configure how Playwright launches the browser:

var browser = await Playwright.Chromium.LaunchAsync(
    new BrowserTypeLaunchOptions
    {
        Headless = true,      // Run without visible browser window
        SlowMo = 100          // Slow down actions for debugging
    });

Page Locators

Use Playwright's built-in locators for reliable element selection:

// By role (recommended)
page.GetByRole(AriaRole.Button, new() { Name = "Submit" })

// By text
page.GetByText("Hello World")

// By test ID
page.GetByTestId("submit-button")

// By CSS selector
page.Locator("[data-testid='weather-table']")

Assertions

Playwright provides built-in assertions that auto-wait:

// Text content
await Assertions.Expect(page.GetByRole(AriaRole.Main))
    .ToContainTextAsync("Expected text");

// Visibility
await Assertions.Expect(page.Locator("#element"))
    .ToBeVisibleAsync();

// Count
await Assertions.Expect(page.Locator("tr"))
    .ToHaveCountAsync(5);

๐Ÿ’ก Best Practices

  1. Use IAsyncLifetime: Share browser and app lifecycle across tests for efficiency
  2. Wait for resources: Use ResourceNotificationService to ensure services are ready
  3. Use data-testid attributes: Add test IDs to elements for stable selectors
  4. Use try/finally: Always close pages in a finally block to ensure cleanup
  5. Run headless in CI: Set Headless = true for CI/CD pipelines
  6. Use WaitForSelectorAsync: Wait for elements before interacting with them
  7. Module initializer for browsers: Use [ModuleInitializer] to install browsers automatically