Playwright for .NET - E2E Testing Guide
Playwright is a modern browser automation framework for reliable end-to-end testing.
Getting Started
Installation
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit
Initialize Playwright
pwsh bin/Debug/net8.0/playwright.ps1 install
Basic Test Structure
using Microsoft.Playwright.NUnit;
[TestFixture]
public class ExampleTests : PageTest
{
[Test]
public async Task BasicNavigationTest()
{
await Page.GotoAsync("https://example.com");
await Expect(Page).ToHaveTitleAsync("Example Domain");
}
[Test]
public async Task LoginTest()
{
await Page.GotoAsync("https://myapp.com/login");
await Page.FillAsync("#email", "user@example.com");
await Page.FillAsync("#password", "password123");
await Page.ClickAsync("button[type='submit']");
await Expect(Page).ToHaveURLAsync("https://myapp.com/dashboard");
}
}
Selectors
Common Strategies
// By Role (Recommended)
await Page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();
// By Text
await Page.GetByText("Welcome").ClickAsync();
// By Label
await Page.GetByLabel("Email").FillAsync("user@test.com");
// By CSS
await Page.Locator(".submit-button").ClickAsync();
// By XPath
await Page.Locator("//button[@type='submit']").ClickAsync();
Assertions
// Element visibility
await Expect(Page.GetByText("Success")).ToBeVisibleAsync();
// Element count
await Expect(Page.Locator(".item")).ToHaveCountAsync(5);
// Text content
await Expect(Page.Locator("h1")).ToHaveTextAsync("Dashboard");
// URL
await Expect(Page).ToHaveURLAsync("/dashboard");
// Attribute
await Expect(Page.Locator("#username")).ToHaveAttributeAsync("disabled", "");
Page Object Pattern
public class LoginPage
{
private readonly IPage _page;
public LoginPage(IPage page) => _page = page;
private ILocator EmailInput => _page.GetByLabel("Email");
private ILocator PasswordInput => _page.GetByLabel("Password");
private ILocator SubmitButton => _page.GetByRole(AriaRole.Button, new() { Name = "Login" });
public async Task LoginAsync(string email, string password)
{
await EmailInput.FillAsync(email);
await PasswordInput.FillAsync(password);
await SubmitButton.ClickAsync();
}
}
// Usage
[Test]
public async Task LoginWithPageObject()
{
var loginPage = new LoginPage(Page);
await Page.GotoAsync("/login");
await loginPage.LoginAsync("user@test.com", "password");
await Expect(Page).ToHaveURLAsync("/dashboard");
}
API Testing
[Test]
public async Task ApiTest()
{
var context = await Browser.NewContextAsync();
var request = await context.Request.GetAsync("https://api.example.com/users");
Assert.That(request.Status, Is.EqualTo(200));
var json = await request.JsonAsync();
Assert.That(json?.users?.length, Is.GreaterThan(0));
}
CI/CD Integration
GitHub Actions
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Install dependencies
run: dotnet restore
- name: Install Playwright
run: pwsh src/bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Run tests
run: dotnet test
Best Practices
- Use role-based selectors - More resilient to changes
- Implement Page Objects - Better maintainability
- Use auto-waiting - Playwright waits automatically
- Run in parallel - Faster test execution
- Take screenshots on failure - Easier debugging
[TearDown]
public async Task TearDown()
{
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
await Page.ScreenshotAsync(new() { Path = $"screenshot-{TestContext.CurrentContext.Test.Name}.png" });
}
}