ScreenshotNeo

BlogHow-to

Playwright with C#: Tutorial

Learn Playwright with C# from first install to reliable tests, browser setup, locators, CI, troubleshooting, and screenshot automation.

By the ScreenshotNeo team1 October 20269 min read

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-deps where 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

  • GetByRole with an accessible name for buttons, links, headings, checkboxes, and form controls.
  • GetByLabel for form fields associated with labels.
  • GetByText for stable, user-visible text.
  • GetByTestId when 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.