Exit course
Chapter 3 of 8 Intermediate 8 min read Updated

Playwright Interview Questions

On this page
  1. Junior level
  2. Mid level
  3. Senior level

Playwright interviews tend to go deeper than Selenium ones, because the framework does more for you and interviewers want to know whether you understand what it is doing. The examples use Playwright Test with TypeScript (version 1.65 at the time of writing).

Junior level#

How is Playwright different from Selenium under the hood?#

Selenium sends one HTTP request per command to a separate driver process (W3C WebDriver). Playwright keeps one long-lived WebSocket connection open to the browser and drives it through the browser's own protocol: CDP for Chromium, and patched builds of Firefox and WebKit that Playwright ships itself. Because the client and the browser stay connected, Playwright receives events (navigation, network, DOM changes) as they happen and can wait for the right moment without polling from the test side.

  • Follow-up: "What does that cost you?" Playwright's Firefox and WebKit are Playwright's own builds, not the branded Firefox and Safari your users run. Branded Chrome and Edge are available through channel. The full trade-off is on the Playwright vs Selenium comparison.
  • Red flag: "Playwright is just a faster Selenium." The execution model, the waiting model and the isolation model are all different.

What is auto-waiting?#

Before an action, Playwright runs actionability checks and retries until they pass or the timeout expires. For click() the element must be visible, stable (not animating), able to receive events (not covered by another element) and enabled. For fill() it must be visible, enabled and editable.

  • Follow-up: "So you never need explicit waits?" You still wait for application state, but you do it with web-first assertions (next question), not sleeps.
  • Red flag: await page.waitForTimeout(3000) in a test. The Playwright docs say it should only be used for debugging.

Which locators do you prefer?#

User-facing ones first: getByRole, getByLabel, getByPlaceholder, getByText, then getByTestId as the stable fallback. CSS and XPath are possible but tie the test to markup.

await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByLabel('Email').fill('qa@example.com');
await page.getByTestId('order-summary').getByRole('row').first();

Two properties to mention: locators are lazy (resolved at the moment of the action, so no stale elements) and strict (an action on a locator that matches several elements throws instead of silently picking the first).

  • Follow-up: "The strict-mode error fires because there are two Save buttons. Fix it." Scope the locator to a container (dialog.getByRole('button', ...)) or use filter({ hasText }). Reaching for .first() without thinking is a mild red flag.

What is a web-first assertion?#

An assertion on a locator that retries until it passes or times out (5 seconds by default):

// Retries until the text matches
await expect(page.getByTestId('cart-count')).toHaveText('3');

// Does NOT retry: reads the text once, then compares
expect(await page.getByTestId('cart-count').textContent()).toBe('3');

The second form is the single most common source of flaky Playwright tests. If you can explain why, you have answered a mid-level question at junior level.

Mid level#

Explain browser, browser context and page.#

A browser is the process. A browser context is an isolated, incognito-like session inside it with its own cookies, storage and cache; creating one takes milliseconds. A page is a tab in a context. Playwright Test gives every test a fresh context, which is why tests do not leak login state into each other, and why you can simulate two users chatting with two contexts in one test.

What are fixtures and why would you write your own?#

Fixtures are Playwright Test's dependency injection: a test declares what it needs ({ page, request }) and the runner sets it up and tears it down. Custom fixtures replace beforeEach chains:

import { test as base, expect } from '@playwright/test';
import { CartPage } from './pages/cart-page';

type Fixtures = { cartPage: CartPage };

export const test = base.extend<Fixtures>({
  cartPage: async ({ page }, use) => {
    const cartPage = new CartPage(page);
    await cartPage.goto();
    await use(cartPage);        // the test runs here
    await cartPage.emptyCart(); // teardown
  },
});
export { expect };
  • Follow-up: "Test scope versus worker scope?" A test-scoped fixture is built for every test; a worker-scoped fixture ({ scope: 'worker' }) is built once per worker process, for example one test account per worker.
  • Red flag: Page objects stored in module-level variables shared across tests.

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

Log in once in a setup project, save the browser state to a file with storageState, and make the other projects depend on it:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});
// auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.E2E_USER!);
  await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
  • Follow-up: "Tests modify the user's settings in parallel." Then each worker needs its own account, which is the worker-scoped fixture from the previous answer.
  • Also expected: keep playwright/.auth out of git.

How do you mock or modify network traffic?#

With page.route() (or context.route() for all pages):

await page.route('**/api/products', route =>
  route.fulfill({ json: [{ id: 1, name: 'Test product', price: 9.99 }] }),
);
await page.goto('/shop');
await expect(page.getByRole('listitem')).toHaveCount(1);

route.continue() lets you change headers or body on the way out, route.abort() simulates a failed request, and route.fetch() fetches the real response so you can modify it.

  • Follow-up: "When should you not mock?" In the handful of end-to-end tests whose job is to prove the front end and back end actually work together.

Can Playwright test APIs as well?#

Yes, through the built-in request fixture (an APIRequestContext). The common use in UI suites is setting up state quickly instead of clicking through screens:

test('shows a newly created order', async ({ page, request }) => {
  const response = await request.post('/api/orders', { data: { sku: 'TSHIRT-M', qty: 2 } });
  expect(response.ok()).toBeTruthy();
  const { id } = await response.json();

  await page.goto(`/orders/${id}`);
  await expect(page.getByRole('heading', { name: `Order ${id}` })).toBeVisible();
});

page.request shares cookies with the page's context, so API calls made through it are authenticated as the logged-in user.

  • Follow-up: "Would you replace your API test suite with this?" For a few checks, fine; for contract and load testing, a dedicated tool is the better fit.

A test failed only in CI. How do you debug it?#

Turn on traces (trace: 'on-first-retry' in the config) and open the failed run's trace with npx playwright show-trace trace.zip. The trace viewer shows every action with a DOM snapshot before and after, the console, the network log and the source line. Locally, npx playwright test --ui (UI mode) and --debug (Inspector) are the tools.

  • Red flag: "I add console.log and re-run CI until it passes."

Senior level#

How does Playwright parallelise, and how do you scale beyond one machine?#

Playwright Test runs test files in parallel across worker processes; tests inside one file run in order unless you set fullyParallel: true or test.describe.configure({ mode: 'parallel' }). After any test failure the worker is shut down and replaced, so the next test starts clean.

Beyond one machine you shard: each CI job runs a slice, writes a blob report, and a final job merges them.

npx playwright test --shard=1/4 --reporter=blob
# after all shards finish, with the blob reports in one folder
npx playwright merge-reports --reporter html ./all-blob-reports
  • Follow-up: "What about mode: 'serial'?" It exists for tests that depend on each other: if one fails, the rest are skipped and the group retries together. The docs discourage it; isolated tests are better.
  • Follow-up: "Your sharded run takes as long as its slowest shard. What now?" Split large files, because Playwright shards tests (with fullyParallel) or files (without it).

How do you run the same suite against browsers you don't have locally?#

Point Playwright at a remote browser endpoint. Playwright Test supports this through connectOptions in a project's use block; on TestingBot the endpoint is a WebSocket URL with the browser in the query string:

use: {
  connectOptions: {
    wsEndpoint: `wss://cloud.testingbot.com/playwright?key=${process.env.TB_KEY}` +
      `&secret=${process.env.TB_SECRET}&browserName=edge&browserVersion=latest`,
  },
},

The test code does not change. The Playwright Test on TestingBot guide has a full multi-browser config, and the Playwright course walks through it end to end.

How do you keep flaky tests from eroding trust in the suite?#

A strong answer combines tooling and process:

  • retries: 2 on CI so a flaky failure is reported as flaky in the HTML report rather than blocking the merge, while still visible.

  • npx playwright test --repeat-each=50 path/to/spec.ts to reproduce.

  • expect.poll() or expect(...).toPass() for conditions that no built-in assertion covers, instead of sleeps.

  • page.clock to control time-dependent UI (countdowns, session expiry) instead of waiting for real time.

  • A rule that a test flagged flaky gets an owner and a deadline.

  • Red flag: "We set retries to 5 and the pipeline is green." The bugs are still there.

When would you choose Cypress or Selenium over Playwright?#

There is no single right answer; interviewers check that you can argue both sides. Selenium: wide language support, existing large suites, a W3C standard with vendor-maintained drivers, and real branded browsers. Cypress: an in-browser runner with an excellent interactive debugger, popular with front-end teams. Playwright: multiple tabs, origins and users in one test, built-in tracing and sharding. The Cypress vs Playwright page lists the differences in detail.