ScreenshotNeo

BlogGuides

Playwright with C#: Interview Questions

Prepare for Playwright with C# interviews with practical answers, runnable NUnit examples, locator, wait, isolation, download and debugging guidance.

By the ScreenshotNeo team1 October 202610 min read

Playwright for .NET is a browser automation library for end-to-end tests and browser workflows. A strong C# interview answer connects four ideas: locators that resolve against the current page, automatic waiting plus retrying assertions, isolated browser contexts, and diagnostics such as tracing. The examples below use NUnit, but the same Playwright library works with MSTest, xUnit and other .NET test runners.

Official references: Playwright .NET library guide, locators, web-first assertions, browser contexts, tracing and downloads.

1. What is Playwright for .NET, and what does a basic test look like?

Initialize Playwright, launch a browser, create a page (usually through a fresh context), navigate, interact through locators, and assert the expected result. Browsers run headless by default; set Headless = false when you need to watch a run.

Install and install browsers

dotnet new nunit -n PlaywrightInterview
cd PlaywrightInterview
dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install

Use the generated script path that matches your target framework and configuration. On a system without PowerShell, run the equivalent Playwright CLI command documented for your platform.

Complete NUnit example

using Microsoft.Playwright;
using NUnit.Framework;

namespace PlaywrightInterview;

public class StoreTests
{
    private IPlaywright _playwright = null!;
    private IBrowser _browser = null!;

    [SetUp]
    public async Task SetUp()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
    }

    [TearDown]
    public async Task TearDown()
    {
        await _browser.CloseAsync();
        _playwright.Dispose();
    }

    [Test]
    public async Task UserCanSearch()
    {
        await using var context = await _browser.NewContextAsync();
        var page = await context.NewPageAsync();

        await page.GotoAsync("https://example.com");
        await page.GetByRole(AriaRole.Heading, new() { Name = "Example Domain" })
            .IsVisibleAsync();

        await Expect(page).ToHaveTitleAsync(new Regex("Example Domain"));
    }
}

In production code, replace the example URL and selectors with your application’s contract. A test normally performs an action and then makes an assertion about the user-visible result.

2. Why are locators preferred over element handles and long selectors?

Locators are live queries, not permanently captured DOM nodes. Playwright resolves a locator when you use it, so it can continue to work when a framework rerenders the page. Locators are also the central piece of Playwright’s auto-waiting and retry behavior.

Preferred locator order

Locator C# example When to use it
Role page.GetByRole(AriaRole.Button, new() { Name = "Save" }) Controls and landmarks exposed to users and assistive technology.
Label page.GetByLabel("Email") Inputs associated with a visible label.
Placeholder page.GetByPlaceholder("Search") Only when the placeholder is a stable product contract.
Text page.GetByText("Order complete") Stable visible text, especially for status messages.
Test ID page.GetByTestId("checkout-submit") A deliberate selector contract owned by the application and tests.
CSS or XPath page.Locator("[data-state='open']") Use when no user-facing or test-ID contract exists; avoid encoding deep DOM structure.
var email = page.GetByLabel("Email");
await email.FillAsync("dev@example.com");
await page.GetByRole(AriaRole.Button, new() { Name = "Continue" }).ClickAsync();
await Expect(page.GetByRole(AriaRole.Status)).ToContainTextAsync("Signed in");

Strictness and multiple matches

Actions that require one target can fail when a locator matches multiple elements. Narrow the locator with a role name, filter, or an explicit index only when the position is part of the contract.

var row = page.GetByRole(AriaRole.Row).Filter(new() { HasText = "INV-1042" });
await row.GetByRole(AriaRole.Button, new() { Name = "Download" }).ClickAsync();

// Use nth only when order is intentional and documented.
await page.GetByRole(AriaRole.Listitem).Nth(0).ClickAsync();

3. How does Playwright wait, and why are fixed sleeps flaky?

Actions wait for applicable actionability checks before they run. Depending on the action, Playwright waits for conditions such as visibility, stability, enabled state and the element receiving events. Web-first assertions retry until the expected condition passes or the assertion timeout is reached. The documented default assertion timeout is five seconds.

await page.GetByRole(AriaRole.Button, new() { Name = "Load report" }).ClickAsync();
await Expect(page.GetByRole(AriaRole.Progressbar)).ToBeHiddenAsync();
await Expect(page.GetByRole(AriaRole.Heading, new() { Name = "Monthly report" }))
    .ToBeVisibleAsync();
await Expect(page.GetByTestId("report-total")).ToHaveTextAsync("42");

Configure timeouts deliberately

await using var context = await _browser.NewContextAsync(new BrowserNewContextOptions
{
    BaseURL = "https://app.example.test"
});
var page = await context.NewPageAsync();
page.SetDefaultTimeout(10_000); // locator and action timeout in milliseconds
page.SetDefaultNavigationTimeout(30_000);

await Expect(page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }))
    .ToBeVisibleAsync(new() { Timeout = 15_000 });

Prefer a state-based wait or assertion over a guessed delay. The API documentation describes tests that wait for time as inherently flaky. WaitForTimeoutAsync can be useful while investigating locally, but it should not be normal synchronization in a test suite.

4. What does test isolation mean in Playwright?

A browser context is an isolated profile with its own cookies, local storage and session storage. Create a new context for each test or fixture that needs independent state. This prevents one test’s login, feature flags or local data from leaking into another test and reduces order-dependent failures.

[Test]
public async Task TwoUsersHaveIndependentSessions()
{
    await using var alice = await _browser.NewContextAsync();
    await using var bob = await _browser.NewContextAsync();
    var alicePage = await alice.NewPageAsync();
    var bobPage = await bob.NewPageAsync();

    await alicePage.GotoAsync("https://example.com/login");
    await bobPage.GotoAsync("https://example.com/login");

    // Login actions for Alice and Bob remain isolated because their
    // cookies and storage belong to different contexts.
}

Reuse authenticated state carefully

Persisted storage state can speed up a suite, but treat the state file as a credential. Keep it outside source control, create it with a dedicated setup flow, and avoid sharing mutable accounts between tests that modify server-side data. If a test must be fully independent, create a new context without a shared state file.

5. How do you write a robust C# test?

  1. Define the user outcome you need to prove.
  2. Choose a stable locator based on role, label, text or a test ID.
  3. Perform one user action at a time.
  4. Assert the resulting UI state with a web-first assertion.
  5. Keep test data and browser state isolated.
  6. Capture diagnostics when a failure occurs.
[Test]
public async Task CheckoutShowsConfirmation()
{
    await using var context = await _browser.NewContextAsync(new()
    {
        BaseURL = "https://shop.example.test"
    });
    var page = await context.NewPageAsync();

    await page.GotoAsync("/cart");
    await page.GetByRole(AriaRole.Button, new() { Name = "Checkout" }).ClickAsync();
    await page.GetByLabel("Email").FillAsync("buyer@example.com");
    await page.GetByRole(AriaRole.Button, new() { Name = "Place order" }).ClickAsync();

    await Expect(page.GetByRole(AriaRole.Heading, new() { Name = "Thank you" }))
        .ToBeVisibleAsync();
    await Expect(page.GetByTestId("order-status")).ToHaveTextAsync("Confirmed");
}

6. How do you debug a failed Playwright test?

Start with the failure message and the locator that timed out. Then inspect a trace, screenshot, page URL, title and relevant console or network errors. A direct context.Tracing trace records browser operations and network activity, but it does not record test assertions. Playwright’s test-runner configuration can capture a more complete trace that includes assertions.

Capture a trace around one scenario

await using var context = await _browser.NewContextAsync();
await context.Tracing.StartAsync(new TracingStartOptions
{
    Screenshots = true,
    Snapshots = true,
    Sources = true
});

try
{
    var page = await context.NewPageAsync();
    await page.GotoAsync("https://example.com");
    await Expect(page.GetByRole(AriaRole.Heading)).ToBeVisibleAsync();
}
finally
{
    await context.Tracing.StopAsync(new TracingStopOptions
    {
        Path = "artifacts/example-trace.zip"
    });
}

Open the resulting archive with the Playwright trace viewer documented at playwright.dev. In a framework-integrated suite, configure tracing on the failure path so the report also includes assertion information.

Useful failure artifacts

await page.ScreenshotAsync(new PageScreenshotOptions
{
    Path = "artifacts/failure.png",
    FullPage = true
});

Console.WriteLine($"URL: {page.Url}");
Console.WriteLine($"Title: {await page.TitleAsync()}");

7. How do you handle downloads?

Register the download wait before clicking the control that starts it. Await the download and save it to a path you control. Temporary download files are removed when the producing browser context closes.

var downloadTask = page.WaitForDownloadAsync();
await page.GetByRole(AriaRole.Link, new() { Name = "Export CSV" }).ClickAsync();
var download = await downloadTask;
await download.SaveAsAsync(Path.Combine(TestContext.CurrentContext.WorkDirectory, "orders.csv"));

For a download that opens a new page, wait for both events according to the application behavior. Assert the suggested filename or file contents when that is part of the requirement.

8. How do you test popups, dialogs and frames?

New pages or popups

var popupTask = page.WaitForPopupAsync();
await page.GetByRole(AriaRole.Link, new() { Name = "Open receipt" }).ClickAsync();
var popup = await popupTask;
await Expect(popup).ToHaveTitleAsync(new Regex("Receipt"));

Browser dialogs

page.Dialog += async (_, dialog) =>
{
    if (dialog.Type == DialogType.Confirm)
        await dialog.AcceptAsync();
    else
        await dialog.DismissAsync();
};
await page.GetByRole(AriaRole.Button, new() { Name = "Delete" }).ClickAsync();

Frames

var paymentFrame = page.FrameLocator("iframe[title='Payment form']");
await paymentFrame.GetByLabel("Card number").FillAsync("4242424242424242");

9. What common interview traps and production failures should you know?

Symptom Likely cause Fix
Locator timeout Wrong role/name, element never rendered, navigation failed, or a frame was omitted. Inspect the trace and URL, verify accessible name, wait for a meaningful state, and use FrameLocator for iframe content.
Strict mode violation Locator matches more than one element. Refine with role, name, label, filter or an intentional index.
Intermittent click failure Overlay, animation, disabled control or unstable layout. Use the user-facing locator and let actionability checks complete; remove overlays in test data or wait for a visible state.
Assertions pass locally but fail in CI Different viewport, fonts, timezone, browser dependencies, speed or test state. Set context options explicitly, isolate data, collect traces and avoid fixed sleeps.
Tests pass alone but fail in a suite Shared cookies, storage, accounts or server data. Create a fresh context and independent test data; do not rely on execution order.
Trace lacks assertion details Tracing was started through the direct browser API. Use the test framework’s trace configuration when assertion-level diagnostics are required.
Downloaded file disappears The context closed before the temporary file was persisted. Call SaveAsAsync before closing the context.
Browser executable missing Playwright package is installed but browser binaries are not. Run the generated Playwright install script for the target framework and environment.

10. Which browser and context options matter in interviews?

  • Headless: headless is the default; set Headless = false for visible debugging.
  • Viewport: set a known size when responsive layout changes the DOM or accessible names.
  • BaseURL: use it to keep navigation paths short and environment-specific.
  • Locale, timezone and geolocation: set them when formatting, date logic or location permissions affect behavior.
  • Storage state: preload authentication only when the state is controlled and protected.
  • Permissions: grant only the browser permissions a scenario needs.
  • Ignore HTTPS errors: reserve this for controlled test certificates, never as a blanket production assumption.
  • Proxy and HTTP credentials: configure them at launch or context level when the environment requires them.

Choose options to make the test’s assumptions explicit. Do not hide environment differences behind arbitrary delays or broad retries.

11. How should a Playwright suite be made reliable and fast?

  • Create contexts per test or fixture boundary so tests can run in parallel without browser state leakage.
  • Reuse a browser process where the runner supports it, while still creating isolated contexts.
  • Use locators and web-first assertions instead of polling application internals or sleeping.
  • Keep each test focused on one behavior and use deterministic server-side data.
  • Capture traces, screenshots and logs on failure rather than for every passing test unless you need continuous diagnostics.
  • Set explicit viewport, locale, timezone and permissions when they affect rendering.
  • Use parallel workers only when accounts and data are designed for concurrent access.

There is no fair benchmark in the supplied sources that proves Playwright is universally faster than another framework. In an interview, explain the tradeoff you can control: resilient locators, isolated state, condition-based synchronization and useful diagnostics.

12. Or skip the browser setup

If the requirement is simply to obtain a clean image or PDF of a URL, ScreenshotNeo provides a GET-based screenshot API and an MCP server for AI agents. It accepts the consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await require('node:fs').promises.writeFile('shot.webp', data);

See the ScreenshotNeo API documentation for the full option set: full-page or CSS-selector capture, dark mode, device presets and custom viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks and waits, ad/tracker/request blocking, headers, cookies, user agents and authorization, timezone and geolocation, transparent backgrounds, resizing, cache TTL, signed image links, asynchronous jobs and webhooks, bulk capture, usage reporting and the OpenAPI specification. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

ScreenshotNeo has 1,000 free shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

13. Short interview FAQ

What is the best first locator to try?

Use a user-facing role or label, then an explicit test ID when your team maintains that contract. Resort to CSS or XPath when the structure itself is the requirement.

What is the default assertion timeout?

The Playwright documentation gives five seconds as the default web-first assertion timeout. Set a higher timeout for a known slow operation instead of adding a sleep everywhere.

Does a browser context equal a browser?

No. One browser can contain multiple isolated contexts. A context is the profile boundary for cookies and storage.

Does direct tracing include assertions?

No. Direct tracing records browser operations and network activity. Use the test framework’s trace configuration for assertion-level trace information.

Why must download waiting start before the click?

The download event can be emitted immediately after the click. Registering the wait first prevents the test from missing that event.

Can Playwright tests use MSTest or xUnit?

Yes. Playwright’s .NET library is independent of the test runner; the official writing-tests material shows MSTest, NUnit and xUnit patterns.