45 questions - Beginner to experienced

Playwright interview questions and answers

Playwright rounds check whether you can write tests that pass for the right reason and stay stable in CI. Expect locators and auto-waiting first, then assertions, fixtures, authentication, network mocking, parallel runs and how you debug a flaky test.

Last updated:

Playwright basics

What is Playwright?

Playwright is an open-source browser automation and end-to-end testing framework from Microsoft. One API drives Chromium, Firefox and WebKit, and it ships with its own test runner, @playwright/test, with assertions, fixtures, parallel runs and reports built in.

Which languages and browsers does Playwright support?

It has official libraries for JavaScript/TypeScript, Python, Java and .NET. It runs Chromium (including Chrome and Edge channels), Firefox and WebKit, the engine behind Safari, on Windows, macOS and Linux, headless or headed.

How is Playwright different from Selenium?

Selenium talks to browsers through WebDriver drivers, and you usually add waits, a runner and reporting yourself. Playwright controls browsers directly, waits automatically for elements to be ready, isolates every test in a fresh browser context, and includes a runner, tracing and network mocking out of the box.

How is Playwright different from Cypress?

Cypress runs inside the browser alongside the app, which makes it simple but limits multiple tabs, other origins and non-Chromium engines. Playwright drives the browser from outside, so multiple tabs, several origins, iframes, downloads and WebKit testing are all supported.

What are Browser, BrowserContext and Page?

A Browser is a launched browser process. A BrowserContext is an isolated session inside it, like a fresh incognito profile with its own cookies and storage. A Page is a single tab in a context. Tests normally get a new context and page each, which keeps them independent.

How do you install Playwright and run tests?

Run npm init playwright@latest to create the config, an example test and the browsers. Run tests with npx playwright test, open the HTML report with npx playwright show-report, and use the --ui flag for the interactive UI mode.

npm init playwright@latest
npx playwright test
npx playwright test --ui
npx playwright show-report

What does a basic Playwright test look like?

Import test and expect, use the page fixture, act on locators and assert with web-first assertions. Each test gets its own isolated page.

import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Locators and auto-waiting

What is a locator in Playwright?

A locator describes how to find an element; it is not the element itself. It is resolved fresh every time you act on it, so it survives re-renders, and every action on it waits for the element to be ready.

Which locators should you prefer?

User-facing ones: getByRole with an accessible name, getByLabel, getByPlaceholder and getByText, then getByTestId for elements with no good accessible name. CSS and XPath tied to layout or class names break when the design changes, so use them last.

page.getByRole('button', { name: 'Add to cart' });
page.getByLabel('Email');
page.getByTestId('order-total');

What is auto-waiting?

Before an action such as click or fill, Playwright waits until the element is attached, visible, stable, enabled and able to receive events. That removes most manual sleeps and the timing failures they cause.

What is strict mode for locators?

Actions on a locator fail if it matches more than one element, instead of silently clicking the first. That forces precise locators. When several matches are expected, narrow it with filter, nth, first or last.

How do you narrow down a locator?

Chain locators to search inside a parent, and use filter with hasText or has to keep only matching items. For example, find the list row that contains a product name, then the button inside that row.

await page
  .getByRole('listitem')
  .filter({ hasText: 'Wireless Mouse' })
  .getByRole('button', { name: 'Remove' })
  .click();

Why should you avoid page.waitForTimeout?

A fixed sleep either wastes time or is too short on a slow CI machine, so tests become slow and flaky. Wait for a condition instead: a web-first assertion, a locator action, or a specific response with waitForResponse.

How do you work with elements inside an iframe?

Use frameLocator to point into the frame, then use normal locators inside it. Playwright waits for the frame and its content just as it does on the main page.

await page
  .frameLocator('#payment-frame')
  .getByLabel('Card number')
  .fill('4111111111111111');

How do you handle elements in shadow DOM?

Playwright CSS and role locators pierce open shadow roots by default, so most components work without special syntax. XPath does not pierce shadow DOM, which is one more reason to prefer role and text locators.

Assertions

What are web-first assertions?

Assertions like expect(locator).toBeVisible() or toHaveText() retry until the condition is true or the timeout ends. They match how pages update asynchronously, unlike a one-time check of a value you read earlier.

await expect(page.getByText('Saved')).toBeVisible();
await expect(page.getByTestId('cart-count')).toHaveText('3');
await expect(page).toHaveURL(/\/orders\/\d+/);

What is the difference between expect(locator).toHaveText and expect(await locator.textContent())?

The first retries until the text matches. The second reads the text once and compares immediately, so it fails if the page has not updated yet. Use the web-first form for anything on the page.

What are soft assertions?

expect.soft records a failure but lets the test continue, so you can see several problems from one run, such as checking many fields on a form. The test is still marked failed at the end.

How do you wait for a value that is not on the page?

Use expect.poll to retry a function until its result matches, for example polling an API until a job status becomes completed. await expect(async () => { ... }).toPass() retries a whole block of code in the same way.

await expect.poll(async () => {
  const res = await request.get('/api/jobs/42');
  return (await res.json()).status;
}).toBe('completed');

How do you check that an element has disappeared?

Use expect(locator).toBeHidden(), or toHaveCount(0) for a list, which retries until the element is gone or hidden. A spinner or toast that disappears after loading is the usual case.

How does visual comparison work in Playwright?

expect(page).toHaveScreenshot() saves a baseline image on the first run and compares later runs pixel by pixel within a threshold. Run it in a consistent environment, such as the same Docker image, because fonts and rendering differ across operating systems.

Test runner, fixtures and structure

What are fixtures in Playwright Test?

Fixtures set up what a test needs and tear it down afterwards. Built-in ones include page, context, browser and request. You can define your own, such as a logged-in page or test data, and a test receives only the fixtures it asks for.

How do you create a custom fixture?

Extend the base test with test.extend. Code before use() runs as setup, use() hands the value to the test, and code after it runs as cleanup.

import { test as base } from '@playwright/test';

export const test = base.extend<{ todoPage: TodoPage }>({
  todoPage: async ({ page }, use) => {
    const todoPage = new TodoPage(page);
    await todoPage.goto();
    await use(todoPage);
    await todoPage.removeAll();
  },
});

What do beforeEach, afterEach, beforeAll and afterAll do?

beforeEach and afterEach run around every test in the file or describe block, for example to open a starting page. beforeAll and afterAll run once per worker for the group, and are used for expensive shared setup.

How do you group, skip or mark tests?

Use test.describe to group tests. test.skip skips a test, conditionally if you pass a condition. test.fixme marks a known broken test, test.slow triples its timeout, and test.only runs just that test while debugging; never commit test.only.

What is test.step used for?

test.step names a block of actions inside a test, such as "Add item to cart". The steps appear in the HTML report and trace viewer, which makes long tests much easier to read when they fail.

How do you implement the Page Object Model in Playwright?

Create a class per page or component that holds its locators and actions, and keep assertions in the tests. Pass the page into the constructor, and expose the class through a fixture so tests stay short and readable.

export class LoginPage {
  constructor(private page: Page) {}
  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Sign in' }).click();
  }
}

What are projects in playwright.config?

Projects run the same tests under different settings, such as Chromium, Firefox, WebKit or a mobile device profile. They can also depend on each other, which is the recommended way to run a setup project, for example logging in, before the main tests.

How are timeouts configured?

There is a per-test timeout (30 seconds by default), an expect timeout for assertions (5 seconds by default), and optional action and navigation timeouts. Set them in playwright.config, per describe block, or per call, and raise them only for genuinely slow steps.

Authentication, network and browser features

How do you avoid logging in through the UI in every test?

Log in once in a setup project, save cookies and local storage with storageState, and start other tests from that saved state. Tests then begin already signed in, which saves time and avoids repeated login failures.

// auth.setup.ts
setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  // ...fill and submit...
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

// playwright.config.ts: use: { storageState: 'playwright/.auth/user.json' }

How do you mock or modify network requests?

page.route intercepts requests that match a URL pattern. You can fulfil them with fake data, abort them to test failures, or continue them with changed headers. This makes tests independent of slow or unstable back-end services.

await page.route('**/api/products', (route) =>
  route.fulfill({ json: [{ id: 1, name: 'Test product' }] }),
);

How do you wait for an API response triggered by a click?

Start waiting before the click, then await both, so you do not miss a fast response. waitForResponse can take a URL pattern or a function that checks the response.

const responsePromise = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Place order' }).click();
const response = await responsePromise;
expect(response.status()).toBe(201);

How do you test APIs with Playwright?

Use the request fixture or request.newContext to send HTTP calls and assert on status and JSON. It is handy for creating test data quickly through the API before checking the UI.

How do you handle a new tab or popup?

Wait for the popup event while performing the click that opens it, then work with the returned page like any other. For links that open in a new tab, the context page event works the same way.

const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open invoice' }).click();
const invoice = await popupPromise;
await expect(invoice).toHaveTitle(/Invoice/);

How do you handle alerts and confirm dialogs?

Playwright dismisses dialogs automatically unless you register a handler. Listen for the dialog event before the action and call accept or dismiss, optionally checking the message first.

page.once('dialog', (dialog) => dialog.accept());
await page.getByRole('button', { name: 'Delete' }).click();

How do you upload and download files?

For uploads, call setInputFiles on the file input with a path or an in-memory buffer. For downloads, wait for the download event while clicking, then save it with saveAs or read its path to check the content.

How do you test on mobile viewports?

Use built-in device descriptors such as devices["Pixel 7"] or devices["iPhone 15"] in a project. They set the viewport, user agent, touch support and device scale factor, which is emulation rather than a real phone.

Parallel runs, CI and debugging

How does Playwright run tests in parallel?

Test files run in parallel across worker processes by default; tests inside one file run in order unless you enable fullyParallel. Each worker has its own browser, and each test its own context, so tests must not depend on each other.

What is sharding?

Sharding splits the test suite across several machines with --shard, for example --shard=1/4 on the first of four CI jobs. The blob reports from each shard can then be merged into one HTML report.

What are retries, and when are they appropriate?

Retries rerun failed tests a set number of times, usually only on CI. A test that fails and then passes is reported as flaky. Retries keep pipelines moving, but flaky tests should still be fixed, not hidden.

What is the trace viewer?

A trace records each action with DOM snapshots, screenshots, console logs and network calls. Setting trace to on-first-retry in the config records traces for failed CI tests, and npx playwright show-trace opens them step by step.

How do you debug a failing test locally?

Run it with --debug to open the Playwright Inspector and step through actions, use UI mode to watch and rerun, or add await page.pause() at the point of interest. The trace viewer helps when the failure happens only on CI.

What is codegen?

npx playwright codegen opens a browser and records your clicks and typing as test code with suggested locators. It is a fast way to discover good locators, but recorded tests still need assertions and clean-up by hand.

How do you run Playwright in CI?

Install dependencies, run npx playwright install --with-deps to get browsers and system libraries, run the tests, and upload the HTML report and traces as artefacts. The official Docker image already contains the browsers.

What causes flaky Playwright tests, and how do you fix them?

Common causes are fixed sleeps, assertions that read a value once, shared test data between parallel tests, and dependence on real back-end timing. Fix them with web-first assertions, unique data per test, network mocking or waitForResponse, and by reading the trace of the failed retry.

Continue with another topic

Practise live

CrackInterviewAI listens to the live interview and gives a structured answer on screen for coding, system design, HR and project questions. Download for Windows.