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 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:
๐งช 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¶
- 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