Skip to content
RED SAGSkills

Playwright interview questions and answers

Playwright questions test modern end-to-end testing: reliable locators, auto-waiting and running tests across browsers.

Below are 35 Playwright questions with the correct answer and a short explanation, grouped from fresher to experienced level. Try each one before you open the answer.

What Playwright interviews usually check

  • ✓Locators and auto-waiting
  • ✓Fixtures and test isolation
  • ✓Network mocking
  • ✓Parallel runs and retries
  • ✓Traces and debugging failures

Playwright interview questions for freshers easy

1. A missing await

This Playwright test sometimes passes and sometimes fails. What is the most likely cause?

test('saves profile', async ({ page }) => {
  await page.goto('/profile')
  page.getByLabel('Name').fill('Asha')
  await page.getByRole('button', { name: 'Save' }).click()
  await expect(page.getByText('Saved')).toBeVisible()
})
  1. A.The fill call is not awaited, so the click can run before the name is typed
  2. B.getByLabel does not exist in Playwright
  3. C.toBeVisible cannot be used with getByText
  4. D.page.goto must always be the last step
Show answer

Answer: A. The fill call is not awaited, so the click can run before the name is typed

Playwright actions return promises. Without await, the test moves on before fill finishes, which creates a race. Linting rules like no-floating-promises catch this mistake automatically.

2. Browsers Playwright covers

Which three browser engines can Playwright run tests against out of the box?

  1. A.Chrome, Edge and Opera only
  2. B.Chromium, Firefox and WebKit
  3. C.Internet Explorer, Safari and Chrome
  4. D.Only Chromium
Show answer

Answer: B. Chromium, Firefox and WebKit

Playwright ships builds of Chromium, Firefox and WebKit, so one test can cover the engines behind Chrome and Edge, Firefox and Safari. You can also run branded Chrome or Edge through the channel option.

3. Using the trace viewer

A Playwright test failed in CI with trace: "on-first-retry". What can you see when you open the trace?

  1. A.A timeline of each action with DOM snapshots, network calls, console logs and the source line
  2. B.Only the final error message
  3. C.The CPU temperature of the CI machine
  4. D.Nothing, traces are only for performance testing
Show answer

Answer: A. A timeline of each action with DOM snapshots, network calls, console logs and the source line

The trace viewer lets you step through the test and inspect the page before and after every action. It is often the fastest way to understand a CI-only failure without reproducing it locally.

4. Recording a first draft of a test

You want Playwright to open a browser, record your clicks and typing, and suggest locators as test code. Which command does this?

  1. A.npx playwright show-report
  2. B.npx playwright codegen https://example.com
  3. C.npx playwright install --with-deps
  4. D.npx playwright test --list
Show answer

Answer: B. npx playwright codegen https://example.com

codegen opens a browser plus an inspector window and writes test code as you interact, picking role, text or test-id locators where it can. It is a good starting point, but the generated code still needs proper assertions and clean-up.

5. Stepping through a failing test

Which option lets you run Playwright tests in an interactive window with a timeline, DOM snapshots for each step and watch mode?

  1. A.npx playwright test --reporter=dot
  2. B.npx playwright test --workers=1
  3. C.npx playwright test --ui
  4. D.npx playwright test --quiet
Show answer

Answer: C. npx playwright test --ui

UI mode shows each action with before and after DOM snapshots, lets you pick locators and re-runs tests on file changes. --debug, or setting PWDEBUG=1, opens the Playwright Inspector to step through line by line.

6. What a browser context gives you

In Playwright Test, each test gets its own browser context by default. What does this mean?

  1. A.Each test starts a new copy of the browser program, which is slow
  2. B.All tests share one logged-in session to save time
  3. C.Tests run in the same tab one after another in a fixed order
  4. D.Each test has its own cookies, storage and cache, like a fresh incognito profile
Show answer

Answer: D. Each test has its own cookies, storage and cache, like a fresh incognito profile

A browser context is a lightweight, isolated session inside one browser process. Fresh contexts per test stop state leaking between tests while staying fast, because launching a new browser for every test is not needed.

7. Grouping actions in the report

What does wrapping code in test.step() do in Playwright Test?

  1. A.It makes the actions inside run in parallel on separate workers
  2. B.It groups actions under a named step in reports and traces
  3. C.It retries the code inside the step until it passes
  4. D.It skips the code inside the step when running in CI
Show answer

Answer: B. It groups actions under a named step in reports and traces

test.step("Add item to cart", async () => {...}) shows a readable step in the HTML report, the trace viewer and UI mode. When a test fails, you can see which business step broke instead of scanning a long list of raw actions.

8. Checking many fields at once

A test checks ten fields on an order summary. You want to see every wrong field in one run, not stop at the first. Which feature helps?

  1. A.expect.soft()
  2. B.test.fixme()
  3. C.test.slow()
  4. D.expect.poll()
Show answer

Answer: A. expect.soft()

A soft assertion records a failure but lets the test continue, and the test is still marked failed at the end. It is handy for checking many independent values, but avoid it where later steps make no sense after an earlier failure.

9. Dragging a card to another column

Which Playwright call drags a Kanban card into the "Done" column?

  1. A.await card.dragTo(page.getByTestId('column-done'))
  2. B.await card.click(); await page.getByTestId('column-done').click()
  3. C.await card.fill('column-done')
  4. D.await page.keyboard.press('Drag')
Show answer

Answer: A. await card.dragTo(page.getByTestId('column-done'))

locator.dragTo(target) performs mouse down on the source, moves to the target and releases. For libraries that need finer movement, you can use page.mouse.down(), move() in steps and up() yourself.

10. Choosing from a native dropdown

How do you choose "Dubai" from a native <select> element labelled "City" in Playwright?

  1. A.await page.getByLabel('City').fill('Dubai')
  2. B.await page.getByLabel('City').selectOption('Dubai')
  3. C.await page.getByText('Dubai').check()
  4. D.await page.getByLabel('City').press('Dubai')
Show answer

Answer: B. await page.getByLabel('City').selectOption('Dubai')

selectOption works on <select> elements and accepts a value, a label or an index. Custom dropdowns built from divs are not <select>, so they need clicks on the trigger and the option instead.

11. Ticking a checkbox safely

Why is locator.check() better than locator.click() for making sure the "I agree" checkbox ends up ticked?

  1. A.check() is faster because it skips all actionability checks
  2. B.click() cannot be used on checkboxes in any browser
  3. C.check() does nothing if already ticked and verifies it is checked afterwards
  4. D.check() also submits the form after ticking the box
Show answer

Answer: C. check() does nothing if already ticked and verifies it is checked afterwards

click() toggles, so if the box was already ticked it becomes unticked. check() is idempotent: it only clicks when needed and then confirms the state changed, which makes the intent clear and the test more reliable.

12. Checking that a tab is active

The active tab has class="tab tab-lg active". Which Playwright assertion reliably checks that it is active?

  1. A.await expect(tab).toHaveClass('active')
  2. B.await expect(tab).toHaveClass(/\bactive\b/)
  3. C.await expect(tab).toHaveText('active')
  4. D.expect(await tab.getAttribute('class')).toBe('active')
Show answer

Answer: B. await expect(tab).toHaveClass(/\bactive\b/)

With a string, toHaveClass must match the full class attribute, so "active" alone fails when other classes are present. A regex matches one class within the list and still auto-retries. Reading the attribute once does not retry and also compares the full string.

Playwright intermediate interview questions medium

13. Flaky end-to-end test with fixed waits

A Playwright test clicks Save, waits 3 seconds and then checks for a success message. It fails randomly on CI. What is the better approach?

await page.getByRole('button', { name: 'Save' }).click()
await page.waitForTimeout(3000)
expect(await page.locator('.toast').isVisible()).toBe(true)
  1. A.Use await expect(page.getByRole('status')).toBeVisible(), which retries until it passes or times out
  2. B.Increase the wait to 10 seconds so slow CI machines have time
  3. C.Run the test with retries: 5 and ignore the failures
  4. D.Check the message with page.evaluate and document.querySelector
Show answer

Answer: A. Use await expect(page.getByRole('status')).toBeVisible(), which retries until it passes or times out

Web-first assertions such as toBeVisible keep checking the locator until the condition is true or the timeout is reached, so they adapt to fast and slow machines. A fixed wait is both slow and still too short sometimes, and isVisible() checks only once. Role-based locators also make tests closer to how users find elements.

14. Web-first assertions

In Playwright, which assertion waits and retries until the text appears or a timeout is reached?

// after clicking Save
const toast = page.getByRole('status')
  1. A.expect(await toast.textContent()).toBe('Saved')
  2. B.await expect(toast).toHaveText('Saved')
  3. C.expect(toast.innerText()).toEqual('Saved')
  4. D.if (toast) expect(true).toBe(true)
Show answer

Answer: B. await expect(toast).toHaveText('Saved')

await expect(locator).toHaveText() is a web-first assertion that retries until the condition holds. Reading textContent once and comparing it checks a single moment, so it fails if the toast appears a little later. Remember to await the assertion.

15. Speeding up logged-in tests

200 UI tests each log in through the login form, adding 6 seconds per test. What is the best improvement?

  1. A.Remove login from the app in test environments
  2. B.Increase the CI machine size
  3. C.Log in once through the API or a setup project, save the authenticated state (cookies or storage), and reuse it in tests
  4. D.Run tests one by one so the server is not loaded
Show answer

Answer: C. Log in once through the API or a setup project, save the authenticated state (cookies or storage), and reuse it in tests

Authenticating via the UI repeatedly is slow and adds flakiness. Playwright storageState or an API login fixture creates the session once and injects it. Keep one or two dedicated tests for the login UI itself.

16. The risk of first()

A test uses page.getByRole("row").first() to check the newest order. It passes locally but fails in CI where the table sorts differently. What is the best fix?

  1. A.Add a fixed wait so the table finishes sorting before first() is called
  2. B.Use last() instead of first() in the CI environment only
  3. C.Locate the row by its content, such as the order number, instead of its position
  4. D.Retry the test three times so that the right order appears
Show answer

Answer: C. Locate the row by its content, such as the order number, instead of its position

Position-based locators depend on order, which can change with data, sort settings or timing. Finding the row by something that identifies it, like the order ID created by the test, makes the test independent of order.

17. Checking a downloaded report

Clicking "Download CSV" starts a file download. What is the correct Playwright pattern?

const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download CSV' }).click()
const download = await downloadPromise
await download.saveAs('out/report.csv')
  1. A.This is correct: start waiting for the event before the click that triggers it
  2. B.Await waitForEvent first, then click, so the event is fully ready
  3. C.Downloads cannot be tested; check only that the button is visible
  4. D.Use page.goto() on the button's href and read the HTML response
Show answer

Answer: A. This is correct: start waiting for the event before the click that triggers it

Creating the wait promise before the click means the event cannot be missed, even if it fires very fast. Awaiting the wait before clicking would hang, because nothing triggers the download. Then download.path() or saveAs() lets you check the file contents.

18. A link that opens a new tab

Clicking "View invoice" opens the invoice in a new tab. How do you get that tab in Playwright?

  1. A.const popupPromise = page.waitForEvent('popup'); await link.click(); const popup = await popupPromise
  2. B.await link.click(); const popup = page; because the new tab replaces the current one
  3. C.await link.click(); await page.goBack() to reach the new tab's content
  4. D.New tabs cannot be controlled, so remove target=_blank before clicking
Show answer

Answer: A. const popupPromise = page.waitForEvent('popup'); await link.click(); const popup = await popupPromise

The popup event fires on the page that opened the new tab, and the result is a Page object you can use like any other. Start waiting before the click so the event is not missed, then await popup.waitForLoadState() if needed.

19. Trace settings for CI

With retries: 2 and trace: "on-first-retry", when is a trace recorded?

  1. A.For every test run, including those that pass first time
  2. B.Only on the final retry, after all earlier attempts have failed
  3. C.Only when a test fails once and is run again the first time
  4. D.Never in CI, because traces work only in local runs
Show answer

Answer: C. Only when a test fails once and is run again the first time

on-first-retry keeps runs fast because passing tests do not record traces, but any failure that is retried gets a full trace with DOM snapshots, network and console. retain-on-failure is another option when retries are not used.

20. Marking a known bug

A test fails because of a known bug that will be fixed next sprint. You want it to stay in the suite, run, and alert you when the bug is fixed. Which annotation fits?

  1. A.test.skip()
  2. B.test.fixme()
  3. C.test.slow()
  4. D.test.fail()
Show answer

Answer: D. test.fail()

test.fail() runs the test and expects it to fail; if it suddenly passes, Playwright reports that, so you know to remove the annotation. test.skip and test.fixme do not run it, and test.slow just triples the timeout.

21. Creating test data quickly

A UI test checks the order details page. Creating an order through the UI takes 40 seconds. What is a better way to set up the order?

  1. A.Click through the checkout flow once and share the order across all tests
  2. B.Create it through the API with Playwright's request fixture, then open the page
  3. C.Ask the developers to insert the order into the production database
  4. D.Skip the setup and assume an order already exists in the environment
Show answer

Answer: B. Create it through the API with Playwright's request fixture, then open the page

Setting up data via API is faster and less flaky, and it keeps each UI test focused on the screen it is testing. The request fixture shares baseURL and can share auth, so each test creates its own order independently.

22. Full text or part of it

A heading reads "Order #1043 confirmed". Which assertion passes?

  1. A.await expect(heading).toHaveText('confirmed')
  2. B.await expect(heading).toHaveValue('Order #1043 confirmed')
  3. C.await expect(heading).toHaveText('order #1043 CONFIRMED')
  4. D.await expect(heading).toContainText('confirmed')
Show answer

Answer: D. await expect(heading).toContainText('confirmed')

With a string, toHaveText must match the whole text (after whitespace normalisation) and is case-sensitive, so "confirmed" alone fails. toContainText matches a substring. toHaveValue is for form inputs, not headings. A regex like /confirmed/ also works with toHaveText.

23. Starting the app for the tests

In CI, the app must be started before the tests run and stopped after. What is the Playwright way to do this?

  1. A.Start the app in a beforeEach hook in every test file
  2. B.Use webServer in the config with the start command and URL to wait for
  3. C.Ask developers to keep a shared server running all the time
  4. D.Run the app inside page.evaluate() in the first test
Show answer

Answer: B. Use webServer in the config with the start command and URL to wait for

webServer: { command: "npm run start", url: "http://localhost:3000", reuseExistingServer: !process.env.CI } starts the app, waits until the URL responds, and shuts it down after the run. Locally it can reuse a server you already have running.

24. Automated accessibility checks

The team adds @axe-core/playwright to scan key pages for accessibility issues. What should they expect from it?

  1. A.It proves the page is fully compliant with WCAG once there are no violations
  2. B.It only checks colour contrast and nothing else on the page
  3. C.It finds many common issues, such as missing labels and low contrast, but not all
  4. D.It replaces the need for any screen reader or keyboard testing
Show answer

Answer: C. It finds many common issues, such as missing labels and low contrast, but not all

Automated rules catch a good share of issues, such as missing alt text, labels, contrast and ARIA misuse, cheaply on every run. Many WCAG criteria need human judgement, such as meaningful focus order or useful alt text, so manual checks are still needed.

Playwright interview questions for experienced candidates hard

25. Strict mode violation

This Playwright line fails with "strict mode violation: resolved to 2 elements". A Save button exists in the page and inside an open dialog. What is the best fix?

await page.locator('button', { hasText: 'Save' }).click()
  1. A.Use .first() so it always clicks the first match
  2. B.Use page.click() with force: true
  3. C.Scope it to the dialog and use a role locator: page.getByRole('dialog').getByRole('button', { name: 'Save', exact: true }).click()
  4. D.Turn off strict mode in the config
Show answer

Answer: C. Scope it to the dialog and use a role locator: page.getByRole('dialog').getByRole('button', { name: 'Save', exact: true }).click()

Strict mode protects you from clicking the wrong element. .first() hides the ambiguity and can click the background Save button when the layout changes. Scoping to the dialog and using an exact accessible name targets the element the user means. force: true skips actionability checks, which hides real bugs.

26. Hidden race in a test

What is wrong with this Playwright test?

test('shows cart count', async ({ page }) => {
  await page.getByRole('button', { name: 'Add to cart' }).click()
  expect(page.getByTestId('cart-count')).toHaveText('1')
})
  1. A.getByTestId does not exist in Playwright
  2. B.click() must be called twice
  3. C.The assertion is not awaited, so the test can finish before it is checked and pass or fail unpredictably
  4. D.toHaveText cannot compare numbers
Show answer

Answer: C. The assertion is not awaited, so the test can finish before it is checked and pass or fail unpredictably

Web-first assertions return a promise. Without await, the test function may end before the retrying assertion completes, so failures are missed or reported as unhandled errors. The eslint rule no-floating-promises catches this.

27. Reusing one page for speed

To save time, a framework reuses one browser page for all tests in a file. Failures now depend on test order and memory grows. What is the better design?

  1. A.Restart the CI machine after each file
  2. B.Use a fresh browser context per test (cheap in Playwright), and speed up setup with saved auth state and API seeding instead
  3. C.Reuse one page for the entire suite
  4. D.Clear cookies with JavaScript between tests and keep the page
Show answer

Answer: B. Use a fresh browser context per test (cheap in Playwright), and speed up setup with saved auth state and API seeding instead

Browser contexts are lightweight, isolated profiles that start in milliseconds, so isolation costs little. The real time savings come from skipping UI login and setting up data through APIs.

28. Testing a 15-minute session timeout

The app logs users out after 15 minutes of inactivity and shows a warning at 14 minutes. How do you test this quickly and reliably?

  1. A.Wait 15 real minutes with page.waitForTimeout(900_000)
  2. B.Change the CI machine's system clock forward by 15 minutes during the test
  3. C.Install the Playwright clock (page.clock.install()) and fast-forward time in the test
  4. D.Skip the test, because time-based behaviour cannot be automated
Show answer

Answer: C. Install the Playwright clock (page.clock.install()) and fast-forward time in the test

page.clock lets you control Date, timers and intervals in the page: install it, then use fastForward or runFor to jump ahead and assert the warning and logout. Real waiting is slow, and changing the OS clock affects everything else on the machine.

29. Replaying recorded API traffic

A third-party pricing API used by the page is slow and rate-limited in test environments. You want realistic responses without calling it every run. Which Playwright feature fits?

  1. A.Disable JavaScript so that the page never calls the API
  2. B.Record the traffic to a HAR file once and replay it with page.routeFromHAR()
  3. C.Use ignoreHTTPSErrors so the API calls fail faster
  4. D.Increase the test timeout to 5 minutes to allow for rate limits
Show answer

Answer: B. Record the traffic to a HAR file once and replay it with page.routeFromHAR()

routeFromHAR serves matching requests from a recorded HAR, and with update: true it can refresh the recording. This gives stable, realistic data. The trade-off is that recordings get stale, so keep a separate contract or integration check against the real API.

30. A five-second assertion in a long test

The test timeout is 60 seconds, but this assertion fails after about 5 seconds even though the message appears after 8 seconds. Why?

await page.getByRole('button', { name: 'Generate' }).click()
await expect(page.getByText('Report ready')).toBeVisible()
  1. A.The 60-second test timeout is split equally between all the steps in the test
  2. B.toBeVisible() never waits, so it fails right away when the text is missing
  3. C.expect has its own timeout, 5 seconds by default, apart from the test timeout
  4. D.Playwright stops waiting when the network is idle for 5 seconds
Show answer

Answer: C. expect has its own timeout, 5 seconds by default, apart from the test timeout

expect has its own timeout (default 5 s), configurable globally with expect: { timeout } or per call: toBeVisible({ timeout: 15_000 }). Raise it only where the app is known to be slow, rather than making every assertion wait longer.

31. A row that is not in the DOM

A table of 10,000 transactions uses virtual scrolling, so only about 30 rows exist in the DOM at a time. The test cannot find transaction TX-9876. What is the best approach?

  1. A.Increase the locator timeout to 2 minutes so that the row eventually loads
  2. B.Use the app's search or filter to bring it up, or scroll in steps until it renders
  3. C.Use page.evaluate() to read all 10,000 rows from the DOM at once
  4. D.Set the viewport height to 50,000 pixels so that every single row renders
Show answer

Answer: B. Use the app's search or filter to bring it up, or scroll in steps until it renders

Virtualised lists render only visible rows, so a far-off row simply does not exist yet; waiting longer will not create it. Narrowing the data via search or filter is the most reliable, and scrolling with a bounded loop is a fallback.

32. A variable that is undefined in evaluate

Why does this code throw "orderId is not defined"?

const orderId = 'ORD-77'
const status = await page.evaluate(() => {
  return window.app.getStatus(orderId)
})
  1. A.page.evaluate only accepts functions that take no data at all
  2. B.orderId must be declared with var instead of const to be visible
  3. C.window.app is not available until the test calls page.reload()
  4. D.The function runs inside the page, so test variables must be passed as an argument
Show answer

Answer: D. The function runs inside the page, so test variables must be passed as an argument

Playwright serialises the function and runs it in the browser, where test-side closures do not exist. Pass data explicitly: page.evaluate((id) => window.app.getStatus(id), orderId). Only serialisable values can be passed in and returned.

33. A click that does nothing

A server-rendered Next.js page shows the "Add to cart" button instantly. Playwright clicks it within milliseconds of load and sometimes nothing happens. Manual users never see this. What is the likely cause and best fix?

  1. A.The click lands before hydration adds handlers; wait for a ready signal first
  2. B.The button is covered by an overlay; use click({ force: true }) instead
  3. C.Playwright clicks too slowly; reduce the action timeout to 100 ms for this button
  4. D.The browser cache is stale; clear the cache before each test
Show answer

Answer: A. The click lands before hydration adds handlers; wait for a ready signal first

Server-rendered HTML looks ready before JavaScript has hydrated it, so early clicks are lost. Humans are slower, so they rarely notice. A reliable fix is a readiness marker the app sets after hydration, or asserting the result with a retry, rather than adding fixed waits.

34. Simulating a slow network everywhere

The team wants to check that loading skeletons show on slow networks in Chromium, Firefox and WebKit using Playwright. Why is CDP-based throttling a poor fit, and what works?

  1. A.CDP throttling works in all browsers, but it is too slow to use in CI pipelines
  2. B.CDP throttling is Chromium-only; delaying responses in page.route works in all browsers
  3. C.Throttling is impossible in tests, so skeleton screens must be tested manually
  4. D.Setting offline: true in the context gives the same effect as a slow network
Show answer

Answer: B. CDP throttling is Chromium-only; delaying responses in page.route works in all browsers

Network emulation via a CDP session exists only in Chromium. Adding a delay inside a route handler before route.continue() or fulfill() slows chosen requests in every browser, which is enough to see loading states. offline simply fails the requests.

35. Moving off a flaky Selenium suite

A Java team has 1,500 Selenium tests on a Grid, with a 12% flaky rate. Leadership wants to "switch to Playwright". What is the most sensible plan?

  1. A.Rewrite all 1,500 tests in Playwright TypeScript over one sprint
  2. B.Switch to Cypress, since it supports Java and multi-tab flows well
  3. C.Keep everything as is, and turn on three automatic retries
  4. D.Find flake causes, pilot Playwright for Java on a flaky area, and compare
Show answer

Answer: D. Find flake causes, pilot Playwright for Java on a flaky area, and compare

Flakiness often comes from waits, data and environments, which a new tool alone will not fix. A pilot gives real evidence on stability and speed, and Playwright for Java lets the team keep its language. Big-bang rewrites are risky, and retries only hide problems.

Ready to test yourself?

Take a 15-question Playwright MCQ test with a score at the end, or practise the full Playwright question bank and earn points on the leaderboard.