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.
๐ Related Resources¶
๐ฆ Required Packages¶
Add the shared Aspire integration test packages and follow native MTP setup for the runner configuration, central package pins, and test command. Playwright tests also need:
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Playwright" />
Also add a project reference to your AppHost:
Coverage¶
Use the dotnet-coverage command in Testing. It instruments the test process without a coverage package in the test project.
๐งช 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 ValueTask 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 ValueTask 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¶
- Arrange: Use the shared browser from the test class
- Act: Navigate to the page and wait for data to load
- 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¶
- Install Playwright CLI:
- Start your AppHost:
- Start a recording session:
A Playwright Inspector window and browser will open.
- Record your actions:
- Perform the actions you want to test
-
Use the movable menu bar for assertions
-
Copy the generated code:
- In the Playwright Inspector, change
TargettoNUnit -
Copy the generated code to your test file
-
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");
- 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¶
- Use
IAsyncLifetime: Share browser and app lifecycle across tests for efficiency - Wait for resources: Use
ResourceNotificationServiceto ensure services are ready - Use
data-testidattributes: Add test IDs to elements for stable selectors - Use
try/finally: Always close pages in afinallyblock to ensure cleanup - Run headless in CI: Set
Headless = truefor CI/CD pipelines - Use
WaitForSelectorAsync: Wait for elements before interacting with them - Module initializer for browsers: Use
[ModuleInitializer]to install browsers automatically