Playwright interview questions
Updated 2026-11-12 · 11 min read
Thirty Playwright questions with short TypeScript examples, covering what makes it different from Selenium and the framework features interviewers ask about: locators, auto-waiting, fixtures, authentication, network, reporters and CI.
Examples use @playwright/test with TypeScript. They assume import { test, expect } from '@playwright/test'; at the top of the file.
Fundamentals
1. What is Playwright and how is it different from Selenium?
Playwright is an end-to-end testing library and test runner from Microsoft for Chromium, Firefox and WebKit. Differences worth saying: it connects to browsers through its own protocol rather than a separate driver executable, it waits for elements automatically before acting, it ships with a test runner, assertions with retries, tracing, parallelism and reporters, and each test gets a fresh, isolated browser context. Selenium is a library you assemble a framework around, with wider language and browser coverage. See the full comparison.
2. What is a browser context and why does it matter?
A context is an isolated browser profile (its own cookies, storage and cache) inside one browser instance. It is cheap to create, so each test gets its own, which means tests do not leak state into each other and can run in parallel without logging each other out.
3. What languages does Playwright support?
JavaScript and TypeScript (with the full test runner), Python, Java and .NET. The built-in @playwright/test runner is for JS/TS; the other languages use a library with pytest, JUnit or NUnit style runners.
Locators and waiting
4. Which locators do you prefer?
Those that follow what the user sees: getByRole, getByLabel, getByText, getByPlaceholder, and getByTestId where there is a dedicated attribute. CSS and XPath are available but are the fallback.
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
Role-based locators double as an accessibility check: if you cannot find a button by role and name, a screen reader user cannot either.
5. What is auto-waiting?
Before an action such as click, Playwright waits until the element is attached, visible, stable (not animating), enabled and receiving events, then acts. You rarely write explicit waits. Assertions with expect(locator) also retry until the condition is true or the timeout expires.
Likely follow-up: Does that mean you never need waitFor? Mostly. You still wait for things that are not element states, such as page.waitForResponse for a particular network call, or waitForURL after navigation.
6. What does "strict mode violation" mean?
A locator that matches more than one element was used for an action that needs exactly one, so Playwright refuses to guess. Make the locator more specific (getByRole('button', { name: 'Save', exact: true })), narrow it to a container, or use .first(), .nth(i) or .filter(...) on purpose.
7. How do you handle frames, new tabs and dialogs?
// iframe
await page.frameLocator('#payment').getByLabel('Card number').fill('4111111111111111');
// new tab opened by a click
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('link', { name: 'Terms' }).click(),
]);
await expect(popup).toHaveTitle(/Terms/);
// alert / confirm: register the handler before the action that triggers it
page.once('dialog', (d) => d.accept());
await page.getByRole('button', { name: 'Delete' }).click();
Test structure
8. What are fixtures?
A fixture is set-up that a test asks for by name and that Playwright creates and tears down for it. page is the built-in one. You can define your own, such as a logged-in page or a page object, and tests simply list them as parameters.
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login-page';
export const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await use(loginPage);
},
});
9. How do you avoid logging in before every test?
Log in once in a set-up step, save the cookies and local storage with storageState, and reuse that state in the tests.
// global setup or a "setup" project
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: '.auth/user.json' });
// playwright.config.ts
use: { storageState: '.auth/user.json' }
Keep the .auth folder out of git because it holds a live session.
10. How does Playwright run tests in parallel?
Test files run in parallel across worker processes by default, and tests inside one file run in order unless you mark the file fullyParallel. Because each test has its own context, tests must not depend on each other. Control the worker count with workers in the config or --workers.
Beyond the UI
11. How do you test APIs with Playwright?
Use the request fixture. It shares the same runner and reporter, which lets you create data through the API and verify through the UI.
test('created user shows in the list', async ({ request, page }) => {
const res = await request.post('/api/users', { data: { name: 'Asha' } });
expect(res.status()).toBe(201);
await page.goto('/users');
await expect(page.getByText('Asha')).toBeVisible();
});
12. How do you mock or intercept network calls?
await page.route('**/api/products', (route) =>
route.fulfill({ status: 500, json: { error: 'boom' } }),
);
await page.goto('/shop');
await expect(page.getByText('Something went wrong')).toBeVisible();
This is the easiest way to test error states that are hard to create on a real server.
13. What is the trace viewer?
A recording of a test run: a timeline of every action with before and after DOM snapshots, network calls, console output and a screenshot at each step. Set trace: 'on-first-retry' in the config so a failed CI run leaves a file you can open with npx playwright show-trace trace.zip. It makes "works on my machine" debugging far shorter.
14. How do you take screenshots, record video and do visual comparison?
screenshot: 'only-on-failure' and video: 'retain-on-failure' in the config. For visual checks use await expect(page).toHaveScreenshot(), which compares against a stored baseline. Generate baselines on the same OS and browser as CI, because font rendering differs.
Running it
15. How do you run Playwright in CI?
Install browsers with npx playwright install --with-deps, run npx playwright test, and upload the HTML report and traces as artifacts. Use retries (retries: 2 in CI only) and look at failures on the retried run, but do not use retries to hide flaky tests.
16. How do you debug a failing test?
npx playwright test --debug opens the inspector, --ui opens the UI mode with a time-travel view, page.pause() stops at a line, and the trace viewer covers CI failures. The codegen command records actions into code, which is useful for finding a locator, not for writing the finished test.
More Playwright in practice
17. How do you narrow a locator when many elements match?
Chain, filter and index on purpose.
const row = page.getByRole('row').filter({ hasText: 'Meena' });
await row.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('listitem').nth(2).click(); // by position, last resort
await page.locator('.card', { has: page.getByText('Pro plan') }).getByRole('button').click();
Chaining from a stable container is more robust than counting positions.
18. What is the difference between expect and expect.soft?
A normal expect failure stops the test at that line. expect.soft records the failure, continues and still fails the test at the end, so one run reports every broken field on a page instead of the first.
await expect.soft(page.getByTestId('name')).toHaveText('Asha');
await expect.soft(page.getByTestId('city')).toHaveText('Pune');
19. How do you group steps, skip tests and tag them?
test.step names a block so reports and traces read well. test.skip, test.fixme (known broken) and test.slow() change how a test runs. Tags in the title or details let you run a subset.
test('checkout @smoke', async ({ page }) => {
await test.step('add to cart', async () => { /* ... */ });
await test.step('pay', async () => { /* ... */ });
});
// run only smoke tests: npx playwright test --grep @smoke
20. How do you run the same tests on several browsers and devices?
Define projects in the config.
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'pixel', use: { ...devices['Pixel 7'] } },
],
npx playwright test --project=webkit runs one. Device projects emulate the viewport, user agent and touch; they are not real phones.
21. How do retries work, and when should you use them?
retries: 2 reruns a failed test up to twice; a test that fails and then passes is reported as flaky. Use retries in CI so one hiccup does not fail a build, but track flaky results and fix them. Do not set retries in local runs, where you want failures to be loud.
22. How do you do set-up that must run before all tests, such as logging in once?
Use a set-up project that other projects depend on (or a global set-up). The set-up project logs in and saves the storage state; the test projects declare dependencies: ['setup'] and use that state.
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{ name: 'chromium', use: { storageState: '.auth/user.json' }, dependencies: ['setup'] },
],
23. How do you handle tests for different user roles?
Create one saved state per role (admin, customer) in the set-up step and choose the state per test file or project. For a test that needs two users at the same time, open two browser contexts with different storage states.
const admin = await browser.newContext({ storageState: '.auth/admin.json' });
const user = await browser.newContext({ storageState: '.auth/user.json' });
24. How do you test file downloads and uploads?
Wait for the download event while performing the click, then inspect the file. For uploads set the files on the input.
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('link', { name: 'Export CSV' }).click(),
]);
expect(download.suggestedFilename()).toBe('orders.csv');
await download.saveAs('tmp/orders.csv');
25. How do you test with a different locale, time zone, location or permission?
Set them in the context options and the test sees a browser configured that way.
use: { locale: 'en-IN', timezoneId: 'Asia/Kolkata', geolocation: { latitude: 18.52, longitude: 73.85 }, permissions: ['geolocation'] }
It is the easiest way to check date formats, currency and time zone bugs.
26. How is Playwright different from Cypress?
Playwright drives the browser from outside, supports several tabs and origins in one test, runs Chromium, Firefox and WebKit, and has multi-language bindings. Cypress runs inside the browser, has a very friendly interactive runner, and historically had limits with multiple tabs and origins. Both wait automatically. Choose by team skills and needs; say which you have used.
27. What reporters does Playwright have, and how do you use them in CI?
list and line for the console, html for a browsable report, json and junit for tools, blob for merging reports. In CI use ['html'], ['junit', { outputFile: 'results/junit.xml' }] and upload the report as an artifact so a failed run can be inspected. See GitHub Actions for a test suite.
28. What is sharding and when would you use it?
Splitting the test run across several machines: npx playwright test --shard=1/4 runs the first quarter of the tests, --shard=2/4 the second, and so on. Use it when the suite is too slow for one machine; run the shards as a matrix in CI and merge the blob reports afterwards.
29. How do you debug a flaky Playwright test?
Reproduce it: run it repeatedly (--repeat-each=20) and in the CI-like mode. Open the trace of a failed run and look at what the page was doing when the action failed. Typical causes: asserting before the app is ready (use a web-first assertion), a shared test account or data, an animation, an assertion on a value that changes, or a network call you should wait for. Fix the cause, not the test timeout.
30. How do you wait for a specific network response, and how do you avoid waiting for "network idle"?
Wait for the response that matters while doing the action that triggers it.
const [response] = await Promise.all([
page.waitForResponse((r) => r.url().includes('/api/orders') && r.status() === 201),
page.getByRole('button', { name: 'Place order' }).click(),
]);
expect((await response.json()).status).toBe('placed');
networkidle waits for quiet and is discouraged: pages with polling or analytics may never be idle, and it hides what you are really waiting for.
Practise it
Open the playground and write one test for each: the form, the alert, the iframe, a dynamic wait and the table. Run them with --ui and watch the timeline.
Want a project you can show in interviews?
The portfolio project kit walks you through testing a deliberately buggy app with Selenium or Playwright and API tests, then explaining it. The app itself is free.
