Playwright with C#: Tutorial
Learn Playwright with C# from first install to reliable tests, browser setup, locators, CI, troubleshooting, and screenshot automation.
How do I use Playwright with C#? Install the Playwright .NET package, build your project, install the matching browser binaries, then write asynchronous tests with locators and web-first assertions. You can use Playwright through MSTest, NUnit, xUnit, or xUnit v3, or use the standalone .NET library from a console application.
This tutorial starts with a reproducible end-to-end test, then explains browser choices, code generation, configuration, CI, troubleshooting, performance, reliability, and screenshot capture.
1. Choose your Playwright with C# setup
| Route | Use it when | What you add |
|---|---|---|
| Test-framework integration | You already run MSTest, NUnit, xUnit, or xUnit v3 tests. | The matching Microsoft.Playwright integration package and its fixture/base class. |
| Standalone library | You need browser automation in a console app, service, job, or another test runner. | Microsoft.Playwright and your own browser lifecycle code. |
| Codegen-assisted start | You are exploring an unfamiliar page or need a first locator draft. | The generated script command, followed by manual review. |
Playwright .NET is distributed as a .NET Standard 2.0 library, and the official documentation recommends .NET 8. Check the current installation guide for supported operating systems before publishing a long-lived build image.
2. First Playwright test with C# and NUnit
NUnit is one supported integration choice. The same overall process applies to MSTest, xUnit, and xUnit v3, but the package and base class differ.
Create the project
dotnet new nunit -n PlaywrightDemo
cd PlaywrightDemo
dotnet add package Microsoft.Playwright.NUnit
dotnet build
After the first build, install the browsers generated for the project. The script is placed in the build output directory for your target framework.
pwsh bin/Debug/net8.0/playwright.ps1 install
If your project targets another framework, replace net8.0 with that framework’s output folder. On CI, install operating-system dependencies as well:
pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
Write the test
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
using System.Threading.Tasks;
namespace PlaywrightDemo;
public class GettingStartedTests : PageTest
{
[Test]
public async Task InstallationPageIsVisible()
{
await Page.GotoAsync("https://playwright.dev");
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" }))
.ToBeVisibleAsync();
}
}
The PageTest base class supplies an isolated page for the test. GotoAsync navigates, GetByRole describes the link by its user-facing role and accessible name, ClickAsync performs the action, and Expect(...).ToBeVisibleAsync() waits until the heading is visible. Every Playwright operation is asynchronous, so await navigation, actions, and assertions.
Run it
dotnet test
The official installation guide also documents templates and integrations for MSTest, NUnit, xUnit, and xUnit v3. Keep the package and base class matched to the framework you selected.
3. Use Playwright as a standalone .NET library
A standalone program is useful when the browser task is not naturally a test case.
dotnet new console -n PlaywrightConsole
cd PlaywrightConsole
dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install
Create Program.cs:
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
Headless = true
});
var page = await browser.NewPageAsync(new()
{
ViewportSize = new() { Width = 1440, Height = 900 }
});
await page.GotoAsync("https://playwright.dev", new()
{
WaitUntil = WaitUntilState.NetworkIdle
});
await page.ScreenshotAsync(new()
{
Path = "playwright-home.png",
FullPage = true
});
This follows the official library setup: create Playwright, launch Chromium, navigate, and perform an action. Use await using so the browser is closed even when the program exits normally.
4. Install and manage Playwright browsers
Playwright browser binaries are version-coupled to the Playwright package. Install them after the initial build, and rerun the installation when updating the package if newer binaries are required.
- Install all supported default browsers with
playwright.ps1 install. - Install operating-system dependencies in CI with
install --with-depswhere supported. - Install only the engine you need when disk space is constrained; consult the browser guide for the current command.
- Expect browser binaries to consume a few hundred megabytes.
Playwright supports Chromium, WebKit, and Firefox. It also documents branded Chrome and Edge channels and device emulation. Choose the engine that matches the compatibility risk you need to check; a Chromium run does not prove behavior on Firefox or WebKit.
5. Locators and web-first assertions
Locators express how a user identifies an element and automatically wait for it to become actionable. Prefer accessible role and name locators when they match the UI contract.
var submit = Page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
await submit.ClickAsync();
await Expect(Page.GetByText("Saved successfully")).ToBeVisibleAsync();
await Expect(Page).ToHaveTitleAsync(new Regex("Account"));
await Expect(Page).ToHaveURLAsync(new Regex("/account"));
await Expect(Page.GetByLabel("Email")).ToHaveValueAsync("user@example.com");
Assertions retry until the condition passes or the timeout is reached. This makes them suitable for asynchronous UI updates. Avoid fixed sleeps: they slow successful tests and do not state the condition you actually need. Use a locator that reflects the intended behavior, then assert visibility, text, value, title, or URL.
Useful locator choices
GetByRolewith an accessible name for buttons, links, headings, checkboxes, and form controls.GetByLabelfor form fields associated with labels.GetByTextfor stable, user-visible text.GetByTestIdwhen the application deliberately exposes a test id.- CSS or XPath only when semantic locators cannot express the target; keep selectors narrow and intentional.
6. Generate a first draft with codegen
Build the project, then use the generated script’s codegen command to open a page, interact with it, and inspect generated actions and locators. Codegen favors role, text, and test-id locators.
pwsh bin/Debug/net8.0/playwright.ps1 codegen https://playwright.dev
Use the output as a starting point. Review every locator and assertion against the real behavior you want to protect. Generated storage-state files can contain authentication cookies and tokens; keep them local and out of source control.
7. Browser, context, and page configuration
Playwright separates the browser process, isolated browser contexts, and pages. A context is useful for a clean session with its own cookies and storage.
await using var browser = await playwright.Chromium.LaunchAsync();
var context = await browser.NewContextAsync(new()
{
ViewportSize = new() { Width = 1280, Height = 800 },
ColorScheme = ColorScheme.Dark,
Locale = "en-US",
TimezoneId = "America/New_York"
});
var page = await context.NewPageAsync();
Use device presets or explicit viewport settings when responsive behavior matters. Select a branded Chrome or Edge channel only when that compatibility target is part of the requirement. Keep test data and authentication isolated per context to reduce cross-test leakage.
Waiting deliberately
await page.GotoAsync("https://example.com", new()
{
WaitUntil = WaitUntilState.DOMContentLoaded,
Timeout = 60_000
});
await page.GetByRole(AriaRole.Main).WaitForAsync();
Wait for a meaningful selector or assertion instead of adding arbitrary delays. Use network-idle waiting only when the page’s request pattern makes it appropriate; applications with long polling may never become truly idle.
8. Screenshots, PDFs, and page artifacts
await page.ScreenshotAsync(new()
{
Path = "artifacts/home.webp",
FullPage = true,
Type = ScreenshotType.Webp
});
await page.PdfAsync(new()
{
Path = "artifacts/report.pdf",
Format = "A4",
PrintBackground = true,
Landscape = false
});
Create the artifact directory before writing files, and give each parallel test a unique path. For visual checks, keep viewport, browser engine, fonts, locale, and color scheme consistent so that differences represent application changes rather than environment drift.
9. Or skip the browser setup
If your goal is a clean website screenshot rather than interactive browser testing, ScreenshotNeo provides a single HTTP request. Its API accepts a URL and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for the complete option list.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. It supports full-page capture with lazy images loaded, CSS element capture, dark mode, device presets, retina scale, PDF controls, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification.
There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.
10. Continuous integration
A basic CI job should check out the repository, install .NET, build the project, install Playwright browsers and required operating-system dependencies, then run the tests. The official CI guide demonstrates this sequence with GitHub Actions.
name: Playwright tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
- run: dotnet test
Action versions change over time; compare this outline with the current official sample before pinning a workflow. Upload traces, screenshots, and videos only when they help diagnose a failure, and avoid storing authentication state as a public artifact.
11. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
| Executable not found | Browser binaries were not installed or are for another package version. | Build first, run the generated playwright.ps1 install, and repeat after package upgrades. |
| Browser fails to launch in CI | Linux browser dependencies are missing. | Use install --with-deps in the CI image or install the documented dependencies. |
| Test times out on a click | The locator is ambiguous, hidden, or the page has not reached the expected state. | Use a role/name or test-id locator, inspect the page, and assert the prerequisite state before clicking. |
| Assertion is flaky | A fixed delay or unstable selector is being used. | Replace sleeps with a web-first assertion and choose a semantic, stable locator. |
| Navigation never reaches network idle | Long polling, analytics, or streaming requests keep the network active. | Use the default navigation wait or wait for a specific ready selector. |
| Works locally, fails in CI | Different browser, fonts, viewport, OS dependencies, timezone, or environment data. | Pin the intended browser setup, configure context options explicitly, and capture diagnostics. |
| Authentication leaks between tests | Pages share a context or a storage-state file is reused carelessly. | Create isolated contexts and treat storage-state files as sensitive credentials. |
| Screenshot differs between runs | Animations, dynamic data, fonts, or responsive dimensions vary. | Control viewport and color scheme, wait for stable UI state, and disable or mask known dynamic content in the test design. |
12. Performance, reliability, and cost notes
- Reuse the browser process: launch one browser per worker or job and create isolated contexts instead of launching a new process for every assertion.
- Keep tests focused: a small number of user-visible assertions gives clearer failures than checking every implementation detail.
- Use parallelism carefully: parallel tests need isolated data, contexts, and artifact paths. Shared accounts and mutable records create false failures.
- Control external dependencies: third-party outages, rate limits, and changing content can make end-to-end tests unreliable. Use stable test environments where possible.
- Manage browser storage: cache browser binaries in CI when your platform permits it, but invalidate the cache when the Playwright package version changes.
- Plan disk usage: the supported browser binaries consume a few hundred megabytes, and traces or screenshots can grow quickly in large suites.
- Screenshot API costs: ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Its response headers report the verdict and billing status, which helps reconcile usage.
13. A practical checklist
- Choose a framework integration or standalone library.
- Target a supported .NET version and operating system.
- Add the matching NuGet package.
- Build before installing browser binaries.
- Install browsers again when package updates require newer binaries.
- Use asynchronous APIs throughout.
- Prefer role, label, text, and test-id locators.
- Use retrying web-first assertions instead of fixed sleeps.
- Run the target browser engines required by your compatibility risk.
- Keep authentication state private.
- Install OS dependencies in CI.
- Save diagnostics with unique paths for parallel jobs.
14. Frequently asked questions
Can I use Playwright with C# without NUnit, MSTest, or xUnit?
Yes. Add Microsoft.Playwright to a console or service project and manage Playwright, browser, context, and page lifecycles directly.
Which browser should I use first?
Start with Playwright’s Chromium build for routine coverage, then add Firefox, WebKit, or a branded Chrome/Edge channel when your compatibility requirements call for it.
Do I need to install browsers globally?
No. Install the browser binaries produced for the project with its generated playwright.ps1 script.
Why does Playwright use async methods everywhere?
Navigation, browser actions, and assertions involve external browser work. Awaiting them ensures the next operation starts after the intended work is ready.
Is codegen a replacement for writing tests?
No. It accelerates exploration and creates a useful first draft, but you should review locators, assertions, data setup, and security before committing the test.


