Top 20 Playwright Interview Questions & Answers (2026 Edition)
Table of Contents
Foundational Concepts
Q1.What is Playwright, and how does it differ from Selenium?
Answer:
Playwright is a modern, open-source automation framework developed by Microsoft for end-to-end testing of web applications. It supports Chromium, Firefox, and WebKit browsers via a single API.
Key Differences from Selenium:
- Architecture: Selenium uses the WebDriver protocol (HTTP requests) to communicate with browsers, which can be slow and flaky. Playwright communicates directly with the browser using a WebSocket connection (Chrome DevTools Protocol), making it significantly faster and more reliable.
- Auto-waiting: Playwright automatically waits for elements to be actionable (visible, enabled, stable) before interacting with them. Selenium often requires explicit `WebDriverWait` statements.
- Browser Contexts: Playwright introduces isolated "Browser Contexts" that act like incognito windows, allowing parallel execution with zero cross-test interference in milliseconds.
- Network Interception: Playwright has built-in, native support for mocking and intercepting network requests without third-party tools.
Q2.Which programming languages and browsers does Playwright support?
Answer:
- Languages: TypeScript/JavaScript (primary/native), Python, Java, and C# (.NET).
- Browsers: Chromium (Chrome, Edge), Firefox, and WebKit (Safari engine). Playwright downloads these specific browser binaries natively ensuring complete cross-browser coverage.
Locators & Auto-Waiting
Q3.Explain Playwright's "Auto-Waiting" mechanism.
Answer:
Auto-waiting is a core feature designed to eliminate test flakiness. Before Playwright performs an action (like `click()`, `fill()`, or `check()`), it automatically waits for a series of actionability checks to pass.
The element must be:
- Attached to the DOM
- Visible (not hidden by CSS)
- Stable (not animating or moving)
- Receives Events (unobscured by other elements like modals)
- Enabled (not disabled)
Interview Tip: Mention that because of auto-waiting, you almost never need to use page.waitForTimeout(5000) (hard sleeps) in Playwright, which is a common anti-pattern in Selenium.
Q4.What are the recommended locators in Playwright (Best Practices)?
Answer:
Playwright strongly recommends using user-facing locators (accessibility locators) because they mirror how users interact with the page, making tests highly resilient to DOM/CSS changes.
getByRole(): The absolute best locator. Finds elements by ARIA role, ARIA attributes, and accessible name (e.g.,page.getByRole('button', { name: 'Submit' })).getByText(): Finds non-interactive elements containing specific text.getByLabel(): Essential for finding form inputs based on their associated `<label>`.getByTestId(): When UI is complex, usedata-testidattributes (highly resilient to refactors).
CSS and XPath selectors (page.locator('.btn-primary')) are considered the lowest priority and should only be used when necessary because they tie tests strictly to the DOM structure.
Q5.How do you handle a scenario where an element is present in the DOM but hidden?
Answer:
If an element is hidden, Playwright's default auto-waiting for actions like `click()` will timeout because the element fails the "Visible" check. However, if you just want to assert its presence or text, you can do so.
If you must interact with a hidden element (e.g., triggering a hidden file input), you can bypass actionability checks using the force: true option:
await page.getByRole('button', { name: 'Hidden Actions' }).click({ force: true });Alternatively, to write assertions regarding hidden elements, use: await expect(locator).toBeHidden().
Advanced Features & Automation
Q6.How does Playwright handle iframes and shadow DOM?
Answer:
Shadow DOM: One of Playwright's greatest advantages is that its locators pierce Shadow DOM natively. Unlike Selenium, you don't need complex JavaScript executors; page.locator() and page.getByRole() just work inside Web Components automatically.
iFrames: You use the frameLocator() API to enter an iframe before locating elements within it.
const frame = page.frameLocator('#payment-frame');
await frame.getByRole('textbox', { name: 'Card Number' }).fill('4111');Q7.Explain how you would mock a Network Request/API call in Playwright.
Answer:
Playwright uses page.route() to intercept network traffic. This is extremely useful to test how the UI handles API errors or specific JSON payloads without touching the real backend.
await page.route('**/api/v1/users', async route => {
// Fulfill the route with a mock JSON response
const json = [{ id: 1, name: 'Alice (Mocked)' }];
await route.fulfill({ json });
});
// The UI will now render the mocked user instead of hitting the real backend.You can also use route.abort() to test how the UI handles network failures.
Q8.How do you handle multiple tabs or windows?
Answer:
When a button clicks opens a link in a new tab (target="_blank"), Playwright handles this via the context.waitForEvent('page') promise.
// Start waiting for new page before clicking
const pagePromise = context.waitForEvent('page');
await page.getByText('Open in new tab').click();
// Await the promise to get the new tab object
const newPage = await pagePromise;
await newPage.waitForLoadState();
await expect(newPage).toHaveTitle('New Document');Architecture & CI/CD
Q9.What is the difference between Soft Assertions and hard assertions?
Answer:
Hard Assertions (expect()): If an assertion fails, the test immediately stops execution and throws an error. This is standard.
Soft Assertions (expect.soft()): If a soft assertion fails, the test logs the failure but continues to execute the rest of the test steps. Playwright will then mark the test as failed at the very end. This is useful when you want to check multiple independent UI elements (e.g., checking 5 different headers) without a single failure aborting the entire check.
Q10.How does Playwright achieve global state setup (like bypassing Login for every test)?
Answer:
Playwright supports saving and reusing browser state (cookies, local storage, session storage). This is a game-changer for CI/CD speed.
- You write a "Setup" script that logs in via UI or API.
- You use
page.context().storageState({ path: 'state.json' })to save the authentication tokens. - In your
playwright.config.ts, you configure your test projects to use thisstate.jsonvia theuse: { storageState: 'state.json' }property.
As a result, hundreds of parallel tests boot up already authenticated without ever executing the UI login flow.
Expert Deep Dive
Q11.How to enter values in the input text field sequentially and validate it?
Answer:
Use `pressSequentially()` to simulate character-by-character typing (triggering keydown/keypress/keyup events). Then validate with `toHaveValue()` Web-First assertion.
const searchInput = page.getByRole('searchbox');
// Type sequentially with delay (simulates human typing)
await searchInput.pressSequentially('playwright', { delay: 100 });
// Verify the value
await expect(searchInput).toHaveValue('playwright');
// Verify autocomplete suggestions appear
await expect(page.getByRole('listbox')).toBeVisible();
await expect(page.getByRole('option', { name: /playwright/i })).toBeVisible();
// Select the first suggestion
await page.getByRole('option').first().click();
// Verify final state
await expect(searchInput).toHaveValue('Playwright Testing');Interview Tip: The `delay` option in `pressSequentially()` controls milliseconds between keystrokes. Setting a realistic delay (50-150ms) makes the typing feel human to JavaScript event handlers that might throttle or debounce rapid input. For OTP or PIN inputs that listen for individual key events, this is essential.
Q12.What is a Playwright Test Runner?
Answer:
`@playwright/test` is Playwright's purpose-built test runner. It provides parallel execution, fixtures, web-first assertions, tracing, retries, multiple reporters, and built-in TypeScript support — all in one package.
Interview Tip: Before `@playwright/test`, teams combined Playwright with Jest or Mocha. This worked but missed out on web-first assertions (auto-retrying), the fixture system, and integrated tracing. The native runner was purpose-built for browser automation concerns. It also includes `playwright/test` which provides the `test`, `expect`, and `defineConfig` exports that are the foundation of every test file.
Q13.What are Fixtures in Playwright?
Answer:
Fixtures are Playwright's dependency injection mechanism. They provide isolated, reusable setup/teardown environments to tests. Built-in fixtures include `page`, `context`, `browser`, `request`. You can extend them with custom fixtures for your application.
// Built-in fixtures available in every test:
test('built-in fixtures', async ({
page, // Fresh Page in an isolated BrowserContext
context, // The BrowserContext for this test
browser, // The shared Browser instance
request, // APIRequestContext for HTTP calls
browserName // 'chromium' | 'firefox' | 'webkit'
}) => {
console.log(`Running on: ${browserName}`);
await page.goto('/');
});Interview Tip: See Question 5 for a deep dive on custom fixtures. Key properties of fixtures: 1) Lazy — only created if test requests them. 2) Isolated — each test gets its own instance. 3) Composable — fixtures can depend on other fixtures. 4) Scoped — can be 'test' or 'worker' scoped.
Q14.What is the difference between Browser and Browser Context in Playwright?
Answer:
A `Browser` is the physical browser process (Chromium.exe). A `BrowserContext` is a lightweight isolated session within that browser (like an incognito window). Multiple contexts share one browser process but have zero shared state.
// Browser is expensive to create - shared across tests in a worker
const browser = await chromium.launch(); // Launches browser process
// BrowserContext is cheap - created per test automatically
const context1 = await browser.newContext(); // User A session (own cookies/storage)
const context2 = await browser.newContext(); // User B session (completely isolated)
const context3 = await browser.newContext({ storageState: 'admin.json' }); // Admin
// Each context can have multiple pages (tabs)
const page1 = await context1.newPage();
const page2 = await context1.newPage(); // Second tab in User A's session
// Contexts are independent:
await context1.addCookies([{ name: 'session', value: 'user-a', domain: 'app.com' }]);
const cookiesInContext2 = await context2.cookies(); // [] - empty, no leakageInterview Tip: The Browser → Context → Page hierarchy is what makes Playwright's multi-user testing possible. One browser process can simultaneously run tests for User A, User B, and User C — each in completely isolated contexts — without launching 3 browser processes.
Q15.How do you handle waiting / synchronization issues in Playwright?
Answer:
Use the hierarchy: (1) Auto-waiting (built-in, free). (2) Web-First assertions (`expect().toBeVisible()`). (3) Explicit waits (`waitFor`, `waitForResponse`). Never use `waitForTimeout` (sleep).
// ✅ Tier 1: Auto-waiting (automatic for all actions)
await page.getByRole('button', { name: 'Load' }).click(); // Waits for button
// ✅ Tier 2: Web-First assertion (retries until true)
await expect(page.locator('.results')).toBeVisible({ timeout: 10000 });
// ✅ Tier 3: Wait for specific network response
await page.waitForResponse(res =>
res.url().includes('/api/data') && res.status() === 200
);
// ✅ Tier 3: Wait for URL change after navigation
await page.waitForURL('**/dashboard');
// ✅ Tier 3: Wait for element state change
await page.locator('#spinner').waitFor({ state: 'hidden' });
// ❌ NEVER: Hard sleep (unreliable, slow)
await page.waitForTimeout(3000); // Don't do this!Interview Tip: The mental model: Playwright should always wait for SOMETHING SPECIFIC (element state, network call, URL change), never for an arbitrary amount of time. If you're reaching for `waitForTimeout`, ask yourself 'what condition am I actually waiting for?' and wait for that condition instead.
Q16.How do you run tests in parallel or in multiple browsers/devices/contexts?
Answer:
Configure `projects` in `playwright.config.ts` to run your test suite across multiple browsers and devices. Each project is an independent test run with its own browser and configuration.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
fullyParallel: true,
projects: [
// Desktop browsers
{ name: 'Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
// Mobile emulation
{ name: 'Mobile iOS', use: { ...devices['iPhone 14'] } },
{ name: 'Mobile Android', use: { ...devices['Pixel 7'] } },
// Custom viewport
{ name: 'HD', use: { viewport: { width: 1920, height: 1080 } } },
],
});
// Run specific project:
// npx playwright test --project=Firefox
// npx playwright test --project="Mobile iOS"Interview Tip: Playwright bundles 50+ device descriptors (phones, tablets, laptops) with correct viewport, user-agent, touch events, and device pixel ratio. Running across Chrome, Firefox, and WebKit (Safari) with one command ensures cross-browser compatibility without managing multiple browser installations.
Q17.How do you run Playwright tests headlessly vs headed?
Answer:
Playwright runs headless by default (no visible browser). Pass `--headed` CLI flag or set `headless: false` in config for a visible browser. Use `--debug` for headed mode with the Inspector attached.
// CLI options
// npx playwright test → headless (default, for CI)
// npx playwright test --headed → headed (see browser)
// npx playwright test --debug → headed + Playwright Inspector
// PWDEBUG=1 npx playwright test → same as --debug
// Config-based
export default defineConfig({
use: {
headless: true, // default
// headless: false, // for debugging sessions
},
});
// Environment-aware (common pattern)
export default defineConfig({
use: {
headless: process.env.CI === 'true', // headless in CI, headed locally
},
});Interview Tip: The `--debug` flag is more powerful than `--headed` alone: it also attaches the Playwright Inspector (a GUI for stepping through tests), enables step-by-step execution, and automatically applies `slowMo` to make actions visible. It's the recommended debugging mode.
Q18.What testing frameworks does TypeScript support in Playwright?
Answer:
Playwright natively supports TypeScript with zero configuration. Write `.ts` test files and run them directly — `@playwright/test` handles transpilation automatically via esbuild.
// TypeScript test - no tsconfig or setup needed!
import { test, expect, Page } from '@playwright/test';
// Full type safety on all Playwright APIs
async function login(page: Page, email: string, password: string) {
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign In' }).click();
}
test('typed test', async ({ page }) => {
await login(page, 'admin@test.com', 'secret');
await expect(page).toHaveURL('/dashboard');
});
// Custom typed fixture
import { test as base } from '@playwright/test';
type MyFixtures = { adminPage: Page };
const test2 = base.extend<MyFixtures>({
adminPage: async ({ page }, use) => {
await login(page, 'admin@test.com', 'secret');
await use(page);
}
});Interview Tip: Playwright uses esbuild to transpile TypeScript on-the-fly (no `tsc` step required). This is significantly faster than traditional TypeScript compilation. You get full IntelliSense, type-checking in your IDE, and typed custom fixtures — all the benefits of TypeScript with none of the build overhead.
Q19.How to slow down test execution in Playwright?
Answer:
Use the `slowMo` option in launch options to add a fixed delay (in ms) between every Playwright action. Use `--debug` flag or `page.pause()` for step-through debugging.
// playwright.config.ts - add slowMo for debugging
export default defineConfig({
use: {
launchOptions: {
slowMo: 500, // 500ms pause after each action (not in CI!)
},
headless: false, // See the slowdown visually
},
});
// Better: Only slow down in development
export default defineConfig({
use: {
launchOptions: {
slowMo: process.env.SLOWMO ? parseInt(process.env.SLOWMO) : 0,
},
},
});
// Run with: SLOWMO=1000 npx playwright test --headed
// Alternative: pause at a specific point
test('debug specific step', async ({ page }) => {
await page.goto('/checkout');
await page.pause(); // Opens Inspector here, then continue manually
await page.getByRole('button', { name: 'Pay' }).click();
});Interview Tip: Remove `slowMo` before committing code to the repository — a `slowMo: 1000` with 100 tests adds 100+ seconds to your CI pipeline for no benefit. Use environment variables to toggle it on/off, keeping your pipeline fast while enabling it locally for observation.
Q20.What is the importance of getByRole locators in Playwright?
Answer:
`getByRole` is Playwright's top-priority locator because it targets elements the same way users and screen readers do — by their ARIA role and accessible name. It's the most resilient locator to HTML refactoring.
// Buttons, links, inputs by role
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.getByRole('link', { name: 'Privacy Policy' }).click();
await page.getByRole('textbox', { name: 'Search' }).fill('laptop');
await page.getByRole('checkbox', { name: 'Remember me' }).check();
await page.getByRole('combobox', { name: 'Country' }).selectOption('US');
// Headings by level
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Dashboard');
// Tables
await expect(page.getByRole('columnheader', { name: 'Price' })).toBeVisible();
await page.getByRole('row', { name: /Alice/ }).getByRole('button', { name: 'Edit' }).click();
// Hidden option (filter out hidden elements - default behavior)
await page.getByRole('button', { name: 'Submit', includeHidden: false }).click();Interview Tip: ARIA roles are part of the W3C accessibility spec. Every HTML element has an implicit role (button → 'button', a href → 'link', input → 'textbox'). `getByRole` uses the accessibility tree (not the DOM), making it immune to class name changes, HTML restructuring, and CSS refactoring. If `getByRole` can't find your button, a screen reader user can't either — a dual benefit.