Playwright interviews for SDET, QA automation and frontend roles test whether you understand why the tool is built the way it is: auto-waiting, locators, isolated browser contexts, fixtures and traces. The questions below are the ones that come up most, each with what the interviewer is checking, an answer frame and a short spoken answer.
Playwright: drives Chromium, Firefox and WebKit through browser-level protocols from an out-of-process runner; bindings for TypeScript, Python, Java and .NET; auto-waiting, contexts and tracing built in.
Selenium: the WebDriver standard, widest browser and language support, no built-in auto-waiting or test runner.
Cypress: runs inside the browser alongside the app; great developer experience, JavaScript only, and some limits on multi-tab and cross-origin flows.
"Playwright controls the browser from outside through low-level protocols, which gives it fast, reliable control of Chromium, Firefox and WebKit, and it ships auto-waiting, isolated contexts, network interception and a trace viewer as first-class features. Selenium speaks WebDriver, which is a standard with the broadest support but leaves waiting and tooling to you. Cypress runs in the browser with the app, which makes debugging pleasant but limits things like multiple tabs, and it is JavaScript only."
Saying one is simply 'better' with no architectural reason, or not knowing Playwright supports WebKit.
Before an action such as click, Playwright waits until the element is attached, visible, stable, enabled and receives events at the click point.
Timeout: if the checks never pass, the action fails with a clear error, not a silent misclick.
Result: most explicit waits and sleeps become unnecessary.
"Every action in Playwright first waits for the element to be ready: attached to the DOM, visible, not animating, enabled, and not covered by something else. Only then does it perform the click or fill. If those conditions are never met within the timeout, the test fails with a message saying which check failed. That is why I almost never write an explicit wait or a sleep."
Adding fixed sleeps by habit, or not knowing what 'stable' means.
Locator: a lazy description of how to find elements; it re-resolves on every action and assertion, so it never goes stale.
User-facing locators: role, label, placeholder, text and test id describe what a user sees; they survive markup changes.
Strictness: an action on a locator matching several elements throws, which catches ambiguous selectors early.
"A locator is not an element; it is a recipe for finding one that is re-run every time I act on it or assert against it, so it cannot go stale. I prefer getByRole, getByLabel and getByText because they describe the page the way a user sees it, so a CSS class rename does not break the test, and they double as an accessibility check. If a locator matches more than one element, an action fails loudly, which I like, because ambiguous selectors are bugs waiting to happen."
Defaulting to long XPath chains, or not knowing locators are lazy.
Browser: one running browser process, expensive to start.
Context: an incognito-like session inside that browser with its own cookies, storage and permissions; cheap to create.
Page: a tab inside a context. Each test gets a fresh context by default, so tests cannot leak state into each other.
"A browser is the heavy process; a context is a lightweight isolated profile inside it with its own cookies, local storage and permissions; a page is a tab in that context. Playwright Test gives every test a brand-new context, so one test cannot pollute another, and because contexts are cheap that isolation costs almost nothing. It also lets me simulate two users in one test by opening two contexts."
Sharing one page across tests to save time.
Setup project: a setup test logs in through the UI or API and saves storageState to a file.
Reuse: other projects depend on the setup project and set storageState in use, so every context starts already authenticated.
Roles: save one state file per role and pick it per test or per project.
"I create a setup project with one test that signs in and calls storageState to write the cookies and local storage to a file. The real test projects declare a dependency on that setup and point their storageState option at the file, so every new context starts logged in without touching the login page. For different roles I keep one state file each and choose it per project or per test."
Logging in through the UI in every test's beforeEach.
Workers: each worker is a separate process; test files are distributed across workers; fullyParallel also spreads tests within a file.
What breaks: shared test data, shared accounts, order-dependent tests, and fixed ports or files.
Fixes: per-test data with unique ids, per-worker accounts, worker-scoped fixtures, and serial mode only where truly needed.
"Playwright Test starts several worker processes and hands them test files; with fullyParallel it splits individual tests too. What breaks is anything shared: two tests editing the same user, tests that assume the previous one ran, or a fixed file path. I fix that with unique test data per test, one account per worker using a worker-scoped fixture, and I reserve serial mode for the rare flow that genuinely depends on order."
Turning on workers and then marking everything serial to make it pass.
Trace: a recording of every action, DOM snapshot before and after, network calls, console logs and a screencast.
Config: trace: 'on-first-retry' in CI keeps runs fast and still captures failures.
Use: open the trace file locally or from the HTML report, step through actions, inspect the DOM at the moment of failure.
"A trace records the whole test: each action with a DOM snapshot before and after it, network requests, console output and a filmstrip. In CI I record traces on the first retry, so a green run pays nothing and a flaky failure still gets captured. When something fails I open the trace, jump to the failing action and inspect the live DOM at that moment, which usually shows the cause immediately."
Debugging CI failures by re-running until green.
Idea: a fixture sets something up, hands it to the test with use, and tears it down after; page, context and request are built-in fixtures.
Custom: test.extend defines a fixture as an async function that builds the object, calls use(obj), then cleans up.
Scope: test-scoped by default; { scope: 'worker' } for expensive resources shared across tests in one worker.
"A fixture is setup and teardown wrapped around a value the test asks for by name. I create one with test.extend: the function builds the thing, for example a logged-in page or a seeded database row, calls use with it so the test runs, and whatever comes after use is the teardown. Test-scoped fixtures rebuild per test; for expensive resources like a server or an admin account I use worker scope so it is built once per worker. Tests then declare only what they need, which keeps them short and independent."
Using global beforeAll hooks with shared mutable state instead of fixtures.
page.route: match a URL pattern, then route.fulfill with a fake response, route.continue with changes, or route.abort.
HAR replay: record real traffic once and replay it for deterministic runs.
Judgement: mock at the edge for UI tests; keep a smaller set of unmocked end-to-end tests.
"I register a route on the page or context for a URL pattern and decide per request: fulfil it with a fixed JSON body, let it continue with modified headers, or abort it to simulate a failure. For bigger surfaces I record a HAR once and replay it, which makes UI tests deterministic and fast. I still keep a thin layer of unmocked tests so a real API change is caught somewhere."
Mocking everything so the suite can never catch an integration break.
Evidence: turn on trace and video on retry, read the failing action and the DOM at that moment.
Usual causes: asserting once instead of with a retrying assertion, a fixed sleep, a race with an animation or a network call, shared data between parallel tests, a viewport or timezone difference.
Fix the cause: web-first assertions, wait for the specific response, isolate data; use retries as a safety net, not a fix.
"First I get evidence: traces on retry in CI, then I look at the exact action that failed and what the DOM looked like. Most of the time it is a check that ran once, like reading text and comparing immediately, which I replace with a retrying expect. The next most common causes are a race with a request, which I fix by waiting for that response, and shared test data across parallel workers, which I fix with unique data. I keep retries on in CI as a safety net, but a test that needs them is still a bug to me."
Adding a sleep or bumping the timeout and calling it fixed.
Web-first assertion: expect(locator).toBeVisible() re-checks until it passes or times out.
One-shot check: await locator.isVisible() returns the state right now; wrapping it in a plain expect gives a race.
Rule: assert with the locator matchers; use one-shot methods only for branching logic.
"expect with a locator matcher keeps polling until the condition is true or the timeout runs out, so it naturally waits for the app to catch up. isVisible answers instantly about this exact moment, and if I put that into a normal expect the assertion races the page and fails randomly. So I assert with locator matchers like toBeVisible, toHaveText and toHaveCount, and I only call the instant methods when I need to branch."
Not knowing that assertions retry, or wrapping instant checks in expect everywhere.
Projects: each project in the config sets a browser and options; the built-in device descriptors set viewport, user agent and touch.
Selection: run the full matrix nightly, one browser on every pull request.
Caveat: device emulation is not a real device; keep a small real-device check for critical mobile flows if it matters.
"In the config I define projects: one for Chromium, one for Firefox, one for WebKit, and mobile projects that use the built-in device descriptors for viewport, user agent and touch. Every pull request runs one desktop project to stay fast, and the full matrix runs nightly. I am clear with the team that emulation covers layout and behaviour, not real device quirks."
Running all browsers on every commit and complaining CI is slow, or assuming emulation equals a real phone.
Iframes: page.frameLocator('#id') then normal locators inside it.
New tabs and popups: start waiting for the popup event on the page, or page on the context, before the click, then await the new page.
Rule: set up the wait first, trigger second, or the event is missed.
"For an iframe I use frameLocator to scope into it and then use ordinary locators inside. For a link that opens a new tab I first create a promise waiting for the popup event, then click, then await the promise to get the new page object and work with it like any page. The order matters: the listener has to exist before the click or the event is gone."
Clicking first and then trying to find the new tab.
Mechanism: expect(page).toHaveScreenshot() compares against a stored baseline; a threshold on pixel or ratio difference allows small noise.
Baselines: created on first run and updated with a flag; they are per browser and per platform.
Pitfalls: fonts and rendering differ between machines, so generate baselines in the same Docker image as CI; mask dynamic regions.
"toHaveScreenshot takes a screenshot and compares it with a baseline image committed to the repo, with a tolerance for tiny differences. Baselines are stored per browser and platform, and I regenerate them deliberately with the update flag when the UI changes. The main trap is that fonts and anti-aliasing differ between a laptop and CI, so I create baselines inside the same container image the pipeline uses, and I mask timestamps and other moving parts."
Screenshot tests generated on a laptop and run on Linux CI, then disabled as 'flaky'.
Still useful: grouping locators and flows for a page keeps tests readable and changes local.
Less needed: user-facing locators and fixtures already remove a lot of duplication; do not build deep class hierarchies.
Modern shape: small page classes injected through fixtures, plus API helpers for setup.
"Yes, but a lighter version. Grouping a page's locators and common flows in a class still makes tests read like a story and keeps changes in one place. What I avoid is the old heavyweight pattern with inheritance and wrappers around every method, because Playwright's locators and fixtures already handle waiting and setup. I usually expose page objects as fixtures so a test just asks for the page it needs."
Either 'always POM, wrap everything' or 'never, put selectors inline everywhere'.
Environment: the official Docker image, or playwright install --with-deps on the runner; headless by default.
Reporting: HTML report with traces on retry, plus a JUnit or blob report for the CI system.
Speed: shard across machines with --shard, merge the reports, cache browsers, and run only affected projects on pull requests.
"I run the suite in the official Playwright container so browsers and fonts match everywhere, headless, with traces on first retry. The runner uploads the HTML report and a machine-readable report for the CI dashboard. To keep it fast I shard the suite across several machines and merge the blob reports into one, cache the browser downloads, and run the full cross-browser matrix nightly rather than on every push."
Installing browsers from scratch on every run, or one giant serial job.
Upload: locator.setInputFiles() on the file input, with a path, several paths, or an in-memory buffer; for custom pickers use the filechooser event.
Download: wait for the download event around the click, then read the path or save it with saveAs and assert on the content.
Cleanup: keep test files small and inside the repo.
"For uploads I call setInputFiles on the input element with a fixture file path, or with a buffer if I want to generate the file in the test; if the app uses a custom picker I wait for the filechooser event instead. For downloads I start waiting for the download event, click the button, and then save the file and check its name and contents. Both are a few lines and need no plugins."
Trying to automate the operating system's file dialog.
Element handle: a reference to one specific DOM node at one moment; after a re-render it points at nothing.
Locator: a query re-evaluated on every use; participates in auto-waiting and strictness.
Guidance: use locators everywhere; handles only for rare low-level needs.
"An element handle is a pointer to a specific node that existed when I fetched it; if the framework re-renders that part of the page, my handle is stale and actions on it fail or hit the wrong thing. A locator is just the query, re-run at the moment of each action or assertion, so it always finds the current element and gets auto-waiting for free. That is why the docs steer everyone to locators."
Using the dollar-sign query methods by default.
request fixture: send HTTP calls with an API request context; assert on status and JSON.
Mixing: create data through the API, then verify it in the UI; or act in the UI and verify through the API.
Sharing auth: a request context created from a browser context shares its cookies.
"Playwright has a request fixture for plain HTTP calls, so I can write pure API tests with the same runner and reports. More often I use it to prepare state: create the order through the API in one call, then open the page and check it renders, which is much faster than clicking through a checkout. If the API needs the same session as the browser I create the request context from the browser context so the cookies are shared."
Building all test data through the UI.
Problem: the symptom, its cost to the team, and why it was hard.
Investigation: traces, isolation, what you ruled out.
Outcome: the fix, and the measurable change in pass rate or run time.
"Our checkout suite failed randomly and people had stopped trusting it. Traces showed most failures came from tests reading a price the moment the page loaded, before a currency call returned. I replaced the instant checks with retrying assertions, added a wait for that specific response where the flow depended on it, and moved test data to unique accounts per worker. The suite went from failing several times a day to essentially stable, and the team turned required checks back on."
A story that ends with 'we added retries'.
Automation interviews move from 'what is a locator' to 'why is this test flaky' to 'show me a fixture' in minutes. When the interviewer shares a failing test and asks what you would change, you need the answer shape before the silence gets long.
ClapAssist is your silent co-pilot. Runs natively on macOS and Windows, listens to the interviewer's exact question, and surfaces concise talking points right next to your camera eye-line. Excluded at the OS level from Zoom, Google Meet, and Teams screen sharing.