Playwright with C#: Interview Questions
Prepare for Playwright with C# interviews with practical answers, runnable NUnit examples, locator, wait, isolation, download and debugging guidance.
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?
- Define the user outcome you need to prove.
- Choose a stable locator based on role, label, text or a test ID.
- Perform one user action at a time.
- Assert the resulting UI state with a web-first assertion.
- Keep test data and browser state isolated.
- 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 = falsefor 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.


