Exam Prep September 11, 2026 16 min read

Playwright Exam Questions & Answers: Beginner to Advanced

A comprehensive study guide with detailed, open-ended exam questions and expert answers covering every core Playwright topic — locators, assertions, API testing, CI/CD, configuration, and the new AI/MCP integration topics for 2026.

📊

Playwright demand grew 340% in QA job postings since 2024

Certification and exam-style questions are now standard in SDET hiring. This guide covers the exact topics tested — with answers detailed enough to study from.

Whether you are preparing for a formal Playwright certification, a technical screening, or a job interview, the questions below reflect the real topics that examiners and hiring managers test in 2026. Each question is open-ended and scenario-driven, followed by a detailed answer that explains both the what and the why.

The questions are organized into six topic areas: Locators & Selectors, Assertions & Auto-Waiting, Test Configuration & Fixtures, API Testing, CI/CD & Parallel Execution, and AI & MCP Integration. Study each section methodically, and use the code examples to reinforce your understanding.


Locators & Selectors

Locator strategy is the single most-tested topic on Playwright exams. Examiners want to know that you can choose the right locator for the right situation — and that you understand why Playwright recommends role-based and semantic locators over CSS selectors.

Q: What is the recommended locator strategy in Playwright, and why does the framework prefer it over CSS selectors?

Playwright recommends user-facing locators — methods like getByRole(), getByLabel(), getByPlaceholder(), and getByText(). These locators target elements the way a real user perceives the page: by visible label, accessible role, or displayed text, rather than by internal CSS class names or DOM structure.

The key advantage is resilience. CSS selectors like .btn-primary-v2 or #submit-form break when developers refactor class names or restructure the DOM. Role-based locators survive these changes because the user-visible behavior (a button labeled "Submit") rarely changes even when the underlying markup does.

Additionally, role-based locators enforce accessibility compliance. If getByRole('button', { name: 'Submit' }) fails, it often means the button lacks a proper accessible name — which is itself a bug that should be fixed.

recommended locator approach
// Preferred: role-based locator
await page.getByRole('button', { name: 'Submit' }).click();

// Preferred: label-based locator for form fields
await page.getByLabel('Email address').fill('user@example.com');

// Avoid: fragile CSS selector
await page.locator('#form-container > div:nth-child(3) > button.submit-btn').click();

Q: When would you use getByTestId() instead of getByRole(), and how do you configure it?

getByTestId() is the right choice when an element has no meaningful accessible role or visible text — for example, a container div used for layout, or a custom widget where ARIA roles are not yet implemented. It targets a data-testid attribute that is explicitly added for testing purposes.

The default attribute is data-testid, but you can customize it in your playwright.config.ts:

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

export default defineConfig({
  use: {
    testIdAttribute: 'data-cy', // custom attribute
  },
});

The trade-off is that data-testid attributes are invisible to users and provide no accessibility benefit. If an element can be found with getByRole() or getByLabel(), prefer those. Reserve getByTestId() for elements that genuinely lack a user-facing identifier.

Q: Explain how Playwright’s locator chaining and filtering work. Give a real-world example.

Playwright locators are lazy and composable. You can chain them using .locator() to narrow scope, or use .filter() to add conditions without changing the base query. The locator does not query the DOM until an action (click, fill, assert) is called.

Consider a product listing page with multiple cards. You need to click “Add to Cart” for a specific product:

locator chaining example
// Chain: narrow to a specific product card, then find the button
const productCard = page
  .getByRole('listitem')
  .filter({ hasText: 'Wireless Headphones' });

await productCard
  .getByRole('button', { name: 'Add to Cart' })
  .click();

// Filter with another locator
const row = page
  .getByRole('row')
  .filter({ has: page.getByText('Active') });

The .filter() method accepts hasText (string or regex) and has (another locator). This is far more readable than writing a complex CSS selector with :has() pseudo-classes and less likely to break when markup changes.

Q: What is the difference between page.locator() and page.$()? Why does Playwright discourage page.$()?

page.$() (and page.$$()) are ElementHandle-based methods inherited from Playwright’s Puppeteer roots. They return a direct reference to a DOM node at the moment they are called. If the DOM changes afterwards — elements re-render, get removed and re-added — the handle becomes stale and throws errors.

page.locator() returns a Locator object that does not immediately query the DOM. Instead, it re-queries every time an action is performed. This means locators automatically handle dynamic content, re-renders, and navigation without going stale.

The practical consequence: never use page.$() in Playwright tests. Locators are the idiomatic API, and every Playwright feature (auto-waiting, retry assertions, web-first assertions) is built around them. ElementHandles bypass all of these protections.

Assertions & Auto-Waiting

Assertions and auto-waiting are what make Playwright tests inherently less flaky than Selenium or Cypress tests. Exam questions in this area test whether you understand the retry mechanism and know when to use web-first assertions versus generic assertions.

Q: How does Playwright’s auto-waiting mechanism work, and what conditions must be met before an action executes?

When you call an action like page.click(), Playwright does not execute immediately. Instead, it enters an auto-wait loop that checks a series of actionability conditions. The element must be:

  • Attached to the DOM (present in the document)
  • Visible (not hidden by CSS, zero-size, or offscreen)
  • Stable (not animating — bounding box stays constant across two consecutive animation frames)
  • Enabled (no disabled attribute)
  • Not obscured (receives the event at the target point — no overlay blocking it)

If any condition is not met, Playwright retries until the actionTimeout (default 30 seconds) expires. This eliminates the need for sleep(), waitForTimeout(), and explicit wait helpers that plague other frameworks.

Exam tip: Different actions check different subsets. fill() checks attached, visible, enabled, and editable. click() additionally checks stable and not-obscured. Know which conditions apply to which actions.

Q: What is the difference between web-first assertions and generic assertions in Playwright? Give examples of each.

Web-first assertions (from expect(locator)) automatically retry until the condition is met or the timeout expires. They are designed for testing dynamic web content where the state may not be immediately available:

web-first assertions (retry automatically)
// Retries until the element has this text
await expect(page.getByRole('heading')).toHaveText('Dashboard');

// Retries until the element is visible
await expect(page.getByRole('alert')).toBeVisible();

// Retries until the URL matches
await expect(page).toHaveURL(/dashboard/);

Generic assertions (from expect(value)) run once and either pass or fail immediately. Use these for non-DOM values like API responses, computed variables, or array lengths:

generic assertions (no retry)
// No retry — value is already resolved
const count = await page.getByRole('listitem').count();
expect(count).toBeGreaterThan(0);

// API response body
const body = await response.json();
expect(body.status).toBe('success');

The exam rule: If you are asserting against a locator or page object, always use web-first assertions. If you are asserting against a plain JavaScript value, use generic assertions. Mixing them up is one of the most common sources of flaky tests.

Q: How do you assert that an element does NOT exist on the page? What pitfalls should you avoid?

Use toHaveCount(0) or not.toBeVisible() on a web-first assertion. Both retry automatically:

asserting absence
// Preferred: assert zero matches
await expect(page.getByRole('dialog')).toHaveCount(0);

// Also valid: assert not visible
await expect(page.getByText('Error')).not.toBeVisible();

The common pitfall is using expect(await page.$('.error')).toBeNull(). This is a generic assertion on an ElementHandle — it runs once, has no retry logic, and will produce false positives if the element appears a few milliseconds after your check. Always use the web-first expect(locator) form for DOM assertions.

Q: How would you set custom timeout values for assertions versus actions, and where should each be configured?

Playwright separates action timeout (how long to wait for click(), fill(), etc.) from assertion timeout (how long web-first assertions retry). Both can be configured at three levels:

timeout configuration
// 1. Global in playwright.config.ts
export default defineConfig({
  use: {
    actionTimeout: 10_000,  // 10s for actions
  },
  expect: {
    timeout: 5_000,        // 5s for assertions
  },
});

// 2. Per-test override
test('slow page load', async ({ page }) => {
  test.slow(); // triples all timeouts
  await expect(page.getByText('Loaded')).toBeVisible({ timeout: 15_000 });
});

// 3. Per-action override
await page.getByRole('button').click({ timeout: 5_000 });

The precedence is: per-call > per-test > config. Keep global timeouts reasonable (5–10 seconds) and use per-call overrides only for known slow operations like file uploads or third-party OAuth redirects.

Test Configuration & Fixtures

Configuration and fixtures questions test your ability to architect a scalable test suite. Examiners want to see that you understand test isolation, dependency injection, and how Playwright’s fixture system differs from traditional setup/teardown.

Q: What is a Playwright fixture, and how does it differ from beforeEach/afterEach hooks?

A fixture is Playwright’s dependency injection system. Rather than imperatively setting up state in beforeEach and tearing it down in afterEach, you declare what a test needs and Playwright provides it.

The key differences are:

  • Lazy initialization: Fixtures are only created when a test actually requests them. If test A needs a database connection but test B does not, the connection is never opened for test B.
  • Automatic teardown: Everything after yield (or await use()) runs as cleanup, guaranteed — even if the test throws.
  • Composability: Fixtures can depend on other fixtures, forming a dependency graph. Hooks cannot express these relationships.
  • Scoping: Fixtures can be scoped to 'test' (per-test) or 'worker' (shared across tests in a worker). Hooks are always per-test or per-file.
custom fixture example
import { test as base } from '@playwright/test';

type MyFixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<MyFixtures>({
  authenticatedPage: async ({ page }, use) => {
    // Setup: log in
    await page.goto('/login');
    await page.getByLabel('Email').fill('admin@test.com');
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', { name: 'Sign in' }).click();

    await use(page); // hand page to test

    // Teardown: runs automatically
    await page.goto('/logout');
  },
});

Q: Explain Playwright’s project configuration. How would you run the same tests across Chromium, Firefox, and WebKit?

Playwright uses the projects array in playwright.config.ts to define multiple test configurations that run against the same test files. Each project specifies a browser, viewport, device emulation, or any combination of use options:

multi-browser project config
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 7'] },
    },
  ],
});

Run all projects with npx playwright test, or target one with npx playwright test --project=firefox. Projects can also have dependencies — for example, a “setup” project that creates auth state before browser-specific projects run.

Q: How does Playwright achieve test isolation, and what is the relationship between browser contexts and pages?

Every Playwright test gets a fresh BrowserContext, which is essentially an incognito profile. Each context has its own cookies, localStorage, session storage, and cache — completely isolated from other contexts. The page fixture creates a new page within that context.

This means:

  • Tests never share state. One test’s login cookies cannot leak into another test.
  • Context creation is fast — milliseconds, not seconds — because it does not launch a new browser process. All contexts share the same browser instance.
  • You can create multiple contexts in one test to simulate multi-user scenarios (e.g., testing real-time chat between two users).
multi-user test with two contexts
test('two users can chat', async ({ browser }) => {
  const aliceCtx = await browser.newContext();
  const bobCtx   = await browser.newContext();

  const alice = await aliceCtx.newPage();
  const bob   = await bobCtx.newPage();

  // Each user has completely isolated cookies/state
  await alice.goto('/chat');
  await bob.goto('/chat');
});

Q: How do you reuse authentication state across multiple tests without logging in each time?

Playwright supports storage state — you can save a logged-in context’s cookies and localStorage to a JSON file, then load it into subsequent tests. The recommended pattern uses a setup project:

auth setup project
// auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('admin@test.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.context().storageState({ path: '.auth/user.json' });
});

// playwright.config.ts
projects: [
  { name: 'setup', testMatch: '**/*.setup.ts' },
  {
    name: 'tests',
    dependencies: ['setup'],
    use: { storageState: '.auth/user.json' },
  },
]

The setup project runs once, saves the auth state, and every test in the “tests” project starts already logged in. This can cut total test suite time by 30–50% for apps with slow authentication flows.

Practice Tests Course

Test Your Knowledge with 190+ Exam-Style Questions

You have studied the concepts above. Now prove you know them under timed, exam-like conditions. Our practice tests course covers every topic in this article — plus dozens more — with detailed explanations for every answer.

  • 190+ questions across locators, assertions, fixtures, API, CI/CD, and AI/MCP
  • Real exam format with detailed explanations for every option
  • Track your accuracy per topic to focus your weak areas
  • 5.0 rating from 131 learners — updated for 2026
Start Practicing Now →

API Testing

Playwright is not just a browser automation tool — its built-in APIRequestContext makes it a legitimate API testing framework. Exam questions test whether you can write standalone API tests and combine API calls with UI flows for efficient test setup.

Q: How do you perform standalone API testing in Playwright without launching a browser?

Playwright provides an APIRequestContext accessible through the request fixture. When you use request instead of page, Playwright does not launch a browser at all — making API tests extremely fast:

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

test('GET /api/users returns 200', async ({ request }) => {
  const response = await request.get('/api/users');

  expect(response.status()).toBe(200);
  expect(response.ok()).toBeTruthy();

  const body = await response.json();
  expect(body.users.length).toBeGreaterThan(0);
});

test('POST /api/users creates user', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: {
      name: 'Jane Doe',
      email: 'jane@example.com',
    },
  });

  expect(response.status()).toBe(201);
});

You can set a baseURL in config so tests use relative paths. The request fixture supports GET, POST, PUT, PATCH, DELETE, and HEAD methods, plus custom headers, form data, and multipart uploads.

Q: Explain how page.route() works for network interception. When would you use it in a test?

page.route() intercepts network requests matching a URL pattern and lets you fulfill (return mock data), abort (block the request), or continue (modify and forward) the request:

network interception
// Mock API response
await page.route('**/api/products', async (route) => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([
      { id: 1, name: 'Mock Product', price: 9.99 },
    ]),
  });
});

// Block analytics/ads
await page.route('**/*google-analytics*', route => route.abort());

// Modify request headers
await page.route('**/api/**', async (route, request) => {
  await route.continue({
    headers: { ...request.headers(), 'X-Test-Mode': 'true' },
  });
});

Common use cases include: mocking slow or flaky third-party APIs, testing error states (return 500), testing edge cases with specific data shapes, speeding up tests by blocking images and analytics, and verifying that the UI sends correct request payloads.

Q: How would you use API calls to set up test data before running a UI test?

The most efficient pattern is to use request for data setup and page only for the UI flow you are actually testing. This avoids navigating through multiple UI screens just to create prerequisite data:

api-assisted test setup
test('user can edit their profile', async ({ page, request }) => {
  // Setup via API: create user and get auth token
  const createRes = await request.post('/api/users', {
    data: { name: 'Test User', email: 'test@example.com' },
  });
  const { id } = await createRes.json();

  // UI test: only test the edit flow
  await page.goto(`/users/${id}/edit`);
  await page.getByLabel('Name').fill('Updated Name');
  await page.getByRole('button', { name: 'Save' }).click();

  await expect(page.getByText('Updated Name')).toBeVisible();

  // Teardown via API
  await request.delete(`/api/users/${id}`);
});

This pattern makes tests faster (API calls are sub-second vs. multi-second UI navigation), more focused (each test validates one UI flow), and more reliable (API setup is deterministic — no chance of a stray UI element breaking your setup).

CI/CD & Parallel Execution

Running tests locally is table stakes. Examiners want to know that you can configure Playwright in a CI pipeline, manage parallel execution, and debug failures from artifacts.

Q: How does Playwright’s parallel execution work? What are workers and how do they relate to test files?

Playwright runs tests in parallel using workers. Each worker is a separate Node.js process that picks up test files from a shared queue. By default, the number of workers equals half the CPU cores (configurable via workers in config or --workers CLI flag).

Key rules for parallelism:

  • Tests in different files run in parallel across workers by default.
  • Tests within the same file run sequentially by default. You can opt in to intra-file parallelism with test.describe.configure({ mode: 'parallel' }).
  • Use test.describe.serial when tests in a file must run in order (e.g., create → edit → delete). If one fails, the rest are skipped.
  • In CI, set workers: 1 for resource-constrained environments, or use --shard=1/4 to distribute across multiple CI machines.
sharding across ci machines
# GitHub Actions: 4 parallel jobs
strategy:
  matrix:
    shard: [1/4, 2/4, 3/4, 4/4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}

Q: What artifacts should you collect from CI runs, and how do you configure Playwright to generate them?

There are three essential artifacts for debugging CI failures:

  • Traces: A complete recording of network requests, DOM snapshots, console logs, and action timeline. Open with npx playwright show-trace trace.zip — this is the single most powerful debugging tool.
  • Screenshots: Captured at the moment of failure. Useful for quick visual confirmation.
  • Videos: Full screen recording of the test run. Helpful for understanding timing-related failures.
artifact configuration
export default defineConfig({
  use: {
    trace: 'on-first-retry',     // record trace on retry
    screenshot: 'only-on-failure', // screenshot on fail
    video: 'retain-on-failure',    // video on fail
  },
  retries: 2, // retry failed tests twice
});

The 'on-first-retry' strategy for traces is the recommended default. Recording traces for every test is expensive (increases run time and storage). Recording only on retry means you get detailed debugging data for flaky and failing tests without slowing down passing tests.

Q: Walk through a complete GitHub Actions workflow for running Playwright tests on every pull request.

A production-ready CI workflow includes: installing dependencies, installing browsers, running tests with sharding, uploading artifacts, and publishing the HTML report:

.github/workflows/playwright.yml
name: Playwright Tests
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1/3, 2/3, 3/3]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: report-${{ strategy.job-index }}
          path: playwright-report/
          retention-days: 14

Key points examiners look for: npx playwright install --with-deps installs browser binaries and their OS-level dependencies (fonts, libraries). The if: !cancelled() condition ensures artifacts are uploaded even when tests fail — which is precisely when you need them.

Q: What causes flaky tests in CI and how does Playwright’s retry mechanism help?

The most common causes of CI flakiness are:

  • Timing issues: CI machines are slower than developer laptops. Elements take longer to render, animations take longer to complete, and network responses are slower.
  • Shared state: Tests modify a shared database or API without proper cleanup, causing order-dependent failures.
  • Non-deterministic data: Tests depend on timestamps, random IDs, or external APIs that return different results.
  • Resource contention: Too many parallel workers for the CI machine’s CPU/memory.

Playwright’s retries config retries a failed test in a fresh worker with a clean browser context. This catches timing-related flakes. However, retries are a bandage, not a cure. If a test fails and passes on retry, investigate the root cause: add web-first assertions, eliminate waitForTimeout(), fix shared state, or mock non-deterministic APIs.

AI & MCP Integration

AI-powered testing with the Model Context Protocol (MCP) is the newest exam topic for 2026. These questions test your understanding of how AI agents interact with Playwright to generate, debug, and maintain tests.

Q: What is the Playwright MCP Server, and how does it enable AI-driven test automation?

The Playwright MCP Server is a bridge between AI models (like Claude) and a live browser session. MCP stands for Model Context Protocol — an open standard that lets AI agents call tools, read application state, and take actions through a structured interface.

When connected to the Playwright MCP Server, an AI agent can:

  • Navigate to URLs and interact with the live application
  • Read the accessibility tree (not raw HTML) to understand page structure
  • Take screenshots to visually verify state
  • Execute Playwright commands (click, fill, assert) through the MCP tool interface
  • Generate test code based on observed application behavior

The AI uses the accessibility tree rather than CSS selectors because it mirrors how users perceive the page. This produces tests that use getByRole() and getByLabel() by default — exactly what Playwright recommends.

Q: How would you use Claude AI to generate Playwright tests for an existing application?

The workflow involves three steps:

  1. Describe the user flow in plain language: “Test the login flow: go to /login, enter valid credentials, verify the dashboard loads with the user’s name in the header.”
  2. Claude reads the application via MCP: The AI navigates to the page, reads the accessibility tree to identify form fields, buttons, and headings, and generates idiomatic Playwright TypeScript that uses role-based locators.
  3. Review and integrate: The generated test is not blindly committed. You review the locator choices, add assertions for edge cases, integrate into your existing Page Object Model, and run it in CI.

The key exam insight: AI-generated tests are a starting point, not a finished product. The value is in the 80% time savings on initial test creation, but human review is essential for edge cases, negative tests, and architectural consistency with the existing test suite.

Q: What are self-healing locators, and how do AI agents maintain test stability?

Self-healing locators are an AI-powered technique where, when a locator fails because the application’s UI changed, the AI agent automatically proposes an updated locator. Instead of the test failing permanently, the agent:

  1. Detects the locator failure and captures the current page state (accessibility tree + screenshot)
  2. Analyzes the change: did the element’s role change? Did it move? Did the text update?
  3. Proposes a new locator based on the updated accessibility tree
  4. Optionally creates a pull request with the updated locator for human review

This is significant because locator maintenance is the #1 cost in test automation. Studies show that 40–60% of test maintenance time is spent updating broken selectors. Self-healing locators can reduce this by an order of magnitude while keeping humans in the review loop for safety.


Practice Tests Course

Master Every Topic with 190+ Practice Questions

This article covers key exam topics in depth. To truly prepare, you need hands-on practice with timed, exam-style questions that test your knowledge under pressure.

  • 190+ questions covering every topic in this article — and more
  • Real exam format: MCQ with detailed explanations
  • Track your progress across all topic areas
  • Updated for 2026 with MCP AI Agents coverage
Start Practicing Now →

5.0 rating • 131 learners • Udemy 30-day money-back guarantee.

Frequently Asked Questions

How difficult is the Playwright certification exam?

The Playwright certification exam is intermediate-level. It tests practical knowledge of locators, assertions, fixtures, API testing, configuration, and CI/CD. If you have 3–6 months of hands-on Playwright experience and study the official docs plus practice questions, most candidates pass on the first attempt. The AI/MCP section is newer and requires focused study.

What topics are covered on the Playwright exam?

Playwright exams cover six core areas: locator strategies (getByRole, getByTestId, CSS/XPath), assertions and auto-waiting, test configuration and fixtures, API testing with request context, CI/CD integration and parallel execution, and advanced topics like network interception, tracing, and AI/MCP integration. This article covers all six with detailed answers.

How should I study for a Playwright certification?

Start with the official Playwright documentation, then build a real project using TypeScript with Page Object Model. Practice writing tests without relying on codegen. Take timed practice exams to simulate real conditions. Focus on areas where you score lowest and review the detailed explanations for each question.

Are Playwright exam questions multiple choice or open-ended?

Most Playwright certification exams use multiple-choice questions (MCQ) with 4 options. However, some exams include scenario-based questions where you must identify the correct code snippet or predict test behavior. Understanding the concepts deeply — not just memorizing syntax — is essential for both formats.

How many questions should I practice before taking the exam?

Aim for at least 150–200 practice questions across all topic areas. This gives you enough coverage to encounter every concept variation. Track your accuracy per topic and focus extra time on areas below 80%. Timed practice sessions (60–90 minutes) build the exam-day stamina needed to maintain focus.


Asim Noaman
Asim Noaman
Senior QA Automation Engineer & AI Testing Specialist