50+ Playwright interview questions for 2026: beginner to advanced. Covers locators, auto-wait, fixtures, POM, API testing, network mocking, CI/CD, tracing, and component testing.
Whether you're a QA engineer targeting your first automation role or an SDET preparing for a senior position, Playwright questions come up in almost every modern testing interview. This guide covers 50+ Playwright interview questions tiered from beginner to advanced, each with a clear answer and a TypeScript code snippet where it matters. Topics include locators, auto-wait, fixtures, POM, network interception, CI/CD integration, tracing, mobile emulation, and component testing, organized the way interviewers actually structure these rounds.
Playwright has become the go-to browser automation framework for QA engineers and SDETs, and interviews reflect that shift. If you're preparing for a role in automation testing, chances are the interviewer is already comfortable with Playwright and will probe deeper than basic API questions. This guide covers 50+ questions organized by difficulty - Beginner, Intermediate, and Advanced - so you know exactly where you stand and what to study next.
Key Takeaways
Playwright supports Chromium, Firefox, and WebKit through one API, enabling cross-browser testing without separate drivers
Auto-waiting is the single biggest difference from Selenium. Understand it thoroughly before your interview
POM + fixtures is the design pattern combination interviewers ask most at mid-to-senior level
Network interception, tracing, and component testing are the advanced topics that separate strong candidates
Brush up on JavaScript interview questions too - Playwright interviews frequently pivot to async/await, Promises, and closure fundamentals.
Beginner questions filter for the fundamentals. Interviewers at this tier want to know whether you can explain Playwright's architecture, set up a project, write a working test, and articulate why auto-waiting matters. You don't need to have memorized the API - you need to show you understand the reasoning behind the design choices.
What interviewers actually ask at this level
Beginner questions filter for fundamentals: can you explain Playwright's architecture, set up a project, write a basic test, and understand why Playwright's auto-waiting model matters vs. explicit sleeps in Selenium.
Playwright is an open-source end-to-end testing framework developed by Microsoft. It automates Chromium, Firefox, and WebKit through a single consistent API. It solves three problems older tools couldn't: flaky tests caused by manual waits, lack of reliable cross-browser support in one framework, and no built-in parallel execution. Companies including Amazon, Microsoft, Apple, and NVIDIA use it in production test pipelines (TestDino, 2026).
Citation Capsule
According to TestDino's 2026 analysis, Playwright commands 45.1% adoption among QA professionals, ahead of Selenium (22.1%) and Cypress (14.4%), with 52 million weekly npm downloads and a 94% retention rate among teams that adopt it (TestDino, 2026). The State of JS 2024 survey confirmed this shift: Playwright overtook Cypress in professional usage for the first time (3,674 vs. 3,603 respondents).
Run the init wizard and it handles everything: installs @playwright/test, creates playwright.config.ts, scaffolds example tests, and optionally generates a GitHub Actions workflow.
npm init playwright@latestThe generated config gives you a solid starting point for any project:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});See the official Playwright documentation for the full setup reference.
Interview tip: Interviewers often ask what fullyParallel: true does vs. the default. By default, test files run in parallel but tests within a file run serially. fullyParallel: true gives every individual test its own worker.
Playwright supports Chromium (Chrome and Edge), Firefox, and WebKit (Safari). Unlike Selenium's WebDriver protocol, Playwright communicates over a persistent WebSocket connection using the Chrome DevTools Protocol (CDP) for Chromium and custom protocol adaptations for Firefox and WebKit. This gives it lower latency and access to browser internals that WebDriver doesn't expose, including network interception at the protocol level.
Before Playwright acts on an element, it checks a set of actionability conditions internally: the element must be attached to the DOM, visible, stable (not animating), enabled, and not covered by another element. Only after all checks pass does the action proceed. This eliminates an entire class of timing bugs that required Thread.sleep() or WebDriverWait in Selenium.
In practice, this means you can write assertions and actions without manual waits, and Playwright handles the timing for you:
// Playwright auto-waits before clicking - no explicit wait needed
await page.getByRole('button', { name: 'Submit' }).click();
// Playwright also auto-waits on assertions
await expect(page.getByText('Success')).toBeVisible();Interview tip: Interviewers commonly ask "what is auto-waiting" as a follow-up to "why Playwright over Selenium." The answer above is the exact level of depth expected.
A selector is a raw query string (CSS, XPath, or text) that targets a DOM node directly. A locator is a higher-level Playwright abstraction that wraps that query and adds auto-retry and actionability checks on every use. Unlike a selector, a locator doesn't resolve immediately. It re-evaluates the DOM fresh each time you call an action or assertion on it, making it naturally resilient to re-renders and dynamic content.
// Selector (legacy - direct DOM query, less resilient)
const el = await page.$('.submit-btn');
// Locator (preferred - auto-retries, actionability checks built in)
const btn = page.getByRole('button', { name: 'Submit' });
await btn.click();In priority order: getByRole() (ARIA-based, accessibility-first), getByText(), getByLabel(), getByPlaceholder(), getByTestId() (uses data-testid), and finally CSS or XPath as a last resort. The preference for role-based locators aligns tests with how assistive technologies see the page, making them more stable and semantically meaningful.
// Best: role + accessible name
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
// Good: visible text
await page.getByText('Sign in').click();
// Avoid: brittle CSS
await page.locator('.btn.btn-primary.submit').click();Playwright models the browser with three layers. A Browser is the browser process itself. A BrowserContext is an isolated session within that browser, functionally equivalent to a fresh incognito window with its own cookies, localStorage, and auth state. A Page is a single tab within a context. Tests should use separate contexts, not separate browsers, for isolation - it's faster and still fully isolated.
const browser = await chromium.launch();
const context = await browser.newContext(); // fresh session - no shared state
const page = await context.newPage(); // single tab in that sessionimport { test, expect } from '@playwright/test';
test('homepage has correct title', async ({ page }) => {
await page.goto('https://playwright.dev');
await expect(page).toHaveTitle(/Playwright/);
});Run headless: npx playwright test. Run headed: npx playwright test --headed. Run a specific file: npx playwright test tests/homepage.spec.ts.
Headless mode runs the browser without a visible GUI. It's faster and ideal for CI pipelines. Headed mode shows the browser window, useful for debugging failures locally and recording with Codegen. Set it globally in config or per-test via the headless option.
// playwright.config.ts - disable headless for local debugging
use: { headless: false }Playwright Test provides four lifecycle hooks. beforeAll runs once per describe block before any tests (use for expensive setup like database seeding). beforeEach runs before every individual test (use for navigation or state setup). afterEach runs after every test (use for cleanup). afterAll runs once after all tests complete (use for teardown).
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('logs in successfully', async ({ page }) => {
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
});expect() assertions in Playwright are async and retry until the condition passes or the timeout expires (default: 5 seconds). You don't need to wait for elements manually before asserting on them. Key assertions: toBeVisible(), toHaveText(), toHaveURL(), toBeEnabled(), toHaveValue(), toContainText().
await expect(page.getByRole('heading')).toHaveText('Welcome back');
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page).toHaveURL('/dashboard');// Native <select> element
await page.getByLabel('Country').selectOption('India');
await page.getByLabel('Tags').selectOption(['react', 'typescript']);
// Custom dropdown (not a <select>)
await page.getByRole('combobox', { name: 'Framework' }).click();
await page.getByRole('option', { name: 'Playwright' }).click();// Full page
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Specific element
await page.getByRole('table').screenshot({ path: 'table.png' });Codegen is Playwright's built-in test recorder. Running npx playwright codegen https://example.com opens the browser with a recording toolbar. As you interact with the page, Playwright generates the corresponding TypeScript test code in real time. It's useful for bootstrapping new tests and understanding which locators Playwright would choose, but generated code always needs review before going to production.
projects: [
{ name: 'Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
],For broader browser and rendering coverage, senior frontend interview topics covers cross-browser compatibility questions that often come up alongside Playwright rounds.
Intermediate questions shift from "do you know the API?" to "can you design something maintainable?" POM structure, fixtures, network interception, auth patterns, and parallel execution are the dominant themes. If you can sketch a fixture dependency chain on a whiteboard, you're ahead of most candidates at this level.
What interviewers actually ask at this level
Mid-level and senior interviews pivot to design: how do you structure tests for maintainability, handle auth without repeating login flows, mock network calls, and run tests in parallel safely. Expect to be asked to sketch a POM class or explain a fixture dependency chain.
POM separates three concerns: what elements exist on a page, what actions that page supports, and what the test actually asserts. Each page gets its own class. Tests call methods on page objects rather than raw locators. This keeps test code readable and confines locator changes to one file when the UI changes.
// pages/LoginPage.ts
import { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/login');
}
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: 'Login' }).click();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('user logs in', async ({ page }) => {
const login = new LoginPage(page);
await login.navigate();
await login.login('user@test.com', 'secret');
await expect(page).toHaveURL('/dashboard');
});As a result, when the login form selector changes, you update LoginPage.ts in one place rather than hunting through test files.
Fixtures are Playwright's dependency injection system. A fixture declares what it provides, and the test runner wires it up automatically, but only when a test requests it. Unlike beforeEach, fixtures compose: fixture B can depend on fixture A, and teardown runs in reverse order automatically. beforeEach runs for every test in scope regardless of whether the test needs it. Fixtures are opt-in and lazy.
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
type Fixtures = { loginPage: LoginPage };
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
const lp = new LoginPage(page);
await lp.navigate();
await use(lp); // test runs here
// teardown goes after use()
},
});
// auth.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('login page has email field', async ({ loginPage, page }) => {
await expect(page.getByLabel('Email')).toBeVisible();
});Playwright's storageState saves cookies and localStorage to a JSON file after one login, then reuses it in subsequent tests, avoiding a full browser login per test. This is the recommended pattern for any suite with more than a handful of authenticated tests.
// global-setup.ts - runs once before the suite
import { chromium } from '@playwright/test';
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Email').fill('admin@test.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Login' }).click();
await page.context().storageState({ path: 'auth.json' });
await browser.close();
}
export default globalSetup;
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup',
use: { storageState: 'auth.json' },
});page.route() intercepts matching requests before they hit the network. route.fulfill() returns a mock response. route.abort() blocks the request entirely. route.continue() passes it through with optional modifications. This lets you test loading states, error conditions, and flaky API responses deterministically, without a real backend.
// Mock a failed API call
await page.route('/api/users', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Something went wrong')).toBeVisible();// File upload
await page.getByLabel('Upload file').setInputFiles('path/to/file.pdf');
// Multiple files
await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf']);
// File download - register promise before the click
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const download = await downloadPromise;
await download.saveAs('downloads/' + download.suggestedFilename());const frame = page.frameLocator('#payment-iframe');
await frame.getByLabel('Card number').fill('4111111111111111');
await frame.getByRole('button', { name: 'Pay' }).click();frameLocator() returns a FrameLocator that auto-waits for the iframe to load. No manual wait for the frame is needed - Playwright handles it.
// Wait for new tab opened by a click
const pagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: 'Open in new tab' }).click();
const newPage = await pagePromise;
await newPage.waitForLoadState();
await expect(newPage).toHaveTitle('New Tab Title');The key pattern: register waitForEvent('page') before the action that triggers the new tab. Awaiting it after gives you the new Page object to interact with.
Playwright Test runs test files in parallel by default - each file in its own worker process. Within a file, tests run serially. Setting fullyParallel: true makes each individual test run in its own worker. Control worker count with workers: 4 (number) or workers: '50%' (percentage of CPU cores). Test isolation is guaranteed because each test gets a fresh browser context.
Sharding splits the test suite across multiple machines. --shard=1/3 runs the first third of tests, --shard=2/3 the second third, and so on. In CI, you'd spin up three parallel jobs, each running a different shard, and merge HTML reports afterward. Use sharding when the suite is too large for one machine's workers to finish within your CI time budget.
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});In practice, retries can mask underlying flakiness. A test that passes on its second attempt hides a real timing or state problem. Use test.info().retry to detect retries and log them. Retries are a safety net, not a substitute for fixing the root cause.
// playwright.config.ts - multiple reporters simultaneously
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'results.xml' }],
['json', { outputFile: 'results.json' }],
],For Allure: install allure-playwright, add ['allure-playwright'] to the reporters array, then run allure generate ./allure-results && allure open. The HTML reporter is sufficient for most teams - open it with npx playwright show-report.
Trace Viewer is Playwright's time-travel debugger. It records screenshots, DOM snapshots, network requests, and console logs for each action in a test. Enable it in config, then open locally with npx playwright show-trace trace.zip. In CI, upload the trace as a build artifact on any failure.
use: {
trace: 'on-first-retry', // record on retry (CI-friendly)
// trace: 'retain-on-failure', // alternative: record all, keep only failures
}Running PWDEBUG=1 npx playwright test opens the Playwright Inspector alongside the browser. You can step through test actions one at a time, see which element each action targets, and inspect the full action log. It's the fastest way to debug a specific failing step without parsing trace files. You can also pause a test programmatically with await page.pause().
Playwright's built-in request fixture provides APIRequestContext, an HTTP client that shares auth state (cookies, headers) with the browser context. Use it for API-level setup and teardown or for full API test suites running alongside UI tests.
test('creates a user via API', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@test.com' },
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body.email).toBe('alice@test.com');
});test('API: create and retrieve user', async ({ request }) => {
// Step 1: create
const createRes = await request.post('/api/users', {
data: { name: 'Bob' },
});
const { id } = await createRes.json();
// Step 2: fetch the created resource
const getRes = await request.get(`/api/users/${id}`);
expect(getRes.status()).toBe(200);
const user = await getRes.json();
expect(user.name).toBe('Bob');
});test('homepage matches snapshot', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
// Update baselines: npx playwright test --update-snapshotsOn first run, Playwright saves the baseline image. On subsequent runs, it diffs pixel-by-pixel and fails if the difference exceeds the threshold. Configure sensitivity via maxDiffPixels or maxDiffPixelRatio in the config.
// playwright.config.ts - add as a project
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
// Or per-test for full control
test('mobile layout', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 13'],
locale: 'en-US',
geolocation: { latitude: 28.6, longitude: 77.2 },
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('/');
});test('smoke: homepage loads', { tag: '@smoke' }, async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Stack Interview/);
});Run tagged tests: npx playwright test --grep @smoke. Use test.skip(), test.fixme(), and test.fail() to manage known failures without deleting tests from the suite.
Naive approach (brittle):
await page.waitForTimeout(2000); // never do this in production testsOne pattern that consistently surfaces in senior interviews is the question of when to use waitForResponse vs. a locator assertion. The answer reveals whether a candidate thinks in terms of symptoms (DOM state) vs. causes (network state). Waiting for the network event is more precise; waiting for the DOM state is more resilient to API changes.
Optimized approach:
// Wait for a specific network request to complete
await page.waitForResponse('/api/data');
// Wait for a DOM state change
await page.waitForSelector('.data-loaded', { state: 'visible' });
// Best: use locator assertions (auto-retry built in)
await expect(page.getByRole('table')).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(10);// Register the handler BEFORE the action that triggers the dialog
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', { name: 'Delete' }).click();
// Dismiss
page.on('dialog', dialog => dialog.dismiss());
// Prompt with input
page.on('dialog', async dialog => {
await dialog.accept('My input text');
});The registration-before-trigger pattern is the most common mistake at this level. If you register the handler after the click, Playwright may miss the dialog entirely.
The async patterns behind network interception - Promises, event loop, and stream handling - come up alongside Playwright in interviews. Node.js interview questions covers that ground in depth.
Advanced questions test architectural thinking. At this level, interviewers aren't checking whether you know the API - they're checking whether you can make good design decisions under real-world constraints: large suites, flaky infrastructure, CI time budgets, and accessibility requirements.
What interviewers actually ask at this level
Senior and staff interviews test whether you can design a test architecture from scratch: custom fixture hierarchies, global setup/teardown, CI pipeline optimization, component testing strategy, and accessibility. Expect open-ended design questions, not just API trivia.
For large suites, fixtures compose naturally. A base fixture provides page objects; an auth fixture depends on the login page fixture; a dashboard fixture depends on auth being complete. Teardown is automatic and runs in reverse dependency order.
// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type AppFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: void;
};
export const test = base.extend<AppFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticatedPage: async ({ page, loginPage }, use) => {
await loginPage.navigate();
await loginPage.login(process.env.TEST_EMAIL!, process.env.TEST_PASSWORD!);
await use(); // test runs
},
dashboardPage: async ({ page, authenticatedPage }, use) => {
await use(new DashboardPage(page));
},
});name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
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
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30The --with-deps flag installs OS-level browser dependencies needed on fresh Linux runners. Uploading the HTML report only on failure keeps CI logs clean and storage costs low.
FROM mcr.microsoft.com/playwright:v1.44.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx playwright testMicrosoft publishes the official mcr.microsoft.com/playwright image with all browser dependencies pre-installed. No --with-deps flag needed - the image handles it. Pin to a specific Playwright version tag to avoid unexpected browser updates breaking your pipeline.
Playwright's component testing (@playwright/experimental-ct-react) mounts individual React, Vue, or Svelte components in a real browser - without running a full dev server. It's faster than full E2E tests and more realistic than JSDOM-based unit tests (Jest, Vitest). Use it for component-level interaction tests where you need actual browser rendering behavior.
// Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('button emits click', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button onClick={() => { clicked = true; }} label="Go" />
);
await component.getByRole('button').click();
expect(clicked).toBe(true);
});Playwright supports three accessibility testing approaches. First, ARIA-based locators (getByRole, getByLabel) catch accessibility tree problems at the locator level - if the role is wrong, the test fails. Second, page.accessibility.snapshot() captures and asserts on the full accessibility tree. Third, integrate axe-core for WCAG rule validation:
import AxeBuilder from '@axe-core/playwright';
test('homepage passes WCAG 2.1 AA', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});const context = await browser.newContext({
locale: 'fr-FR',
timezoneId: 'Europe/Paris',
geolocation: { latitude: 48.8566, longitude: 2.3522 }, // Paris
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('/');
// Date formats, currency, and language content now render as French localeWhy does this matter for your interview? It shows you understand that internationalization bugs are browser-context bugs - not just translation bugs - and that Playwright surfaces them without device farms.
// Modify request headers before they leave the browser
await page.route('/api/**', route => {
const headers = { ...route.request().headers(), 'x-test-id': 'playwright' };
route.continue({ headers });
});
// Intercept and log all API calls without blocking them
await page.route('/api/**', async route => {
console.log('API call:', route.request().method(), route.request().url());
await route.continue();
});import { readFileSync } from 'fs';
test('CSV download contains correct data', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
const filePath = await download.path();
const content = readFileSync(filePath!, 'utf8');
expect(content).toContain('Name,Email');
expect(download.suggestedFilename()).toBe('users.csv');
});// .env.test - never commit credentials to version control
// TEST_EMAIL=qa@company.com
// TEST_PASSWORD=secret
// playwright.config.ts
import { config } from 'dotenv';
config({ path: '.env.test' });
// In tests - access via process.env
await loginPage.login(process.env.TEST_EMAIL!, process.env.TEST_PASSWORD!);In CI, store secrets as encrypted environment variables (GitHub Secrets, etc.) and inject them at runtime. Never hardcode credentials in test files.
There's no single correct answer, but these principles hold across teams that manage large Playwright suites:
tests/auth/, tests/checkout/, tests/admin/playwright.config.ts projects to separate smoke, regression, and full suite runs@smoke for fast CI feedback (target: under 5 minutes)fixtures/data/ as TypeScript constants - never hardcoded inlinePERSONAL EXPERIENCE -
In practice, the most common source of flakiness in Playwright suites isn't Playwright itself - it's shared browser state leaking between tests. Adding a storageState reset to beforeEach caught 80% of our intermittent failures on a 300-test suite.
In practice, a structured approach to flakiness looks like this:
1. Enable retries: 2 in CI to surface flakiness without silencing it
2. Use --repeat-each=10 to reproduce intermittent failures locally
3. Check for race conditions: replace waitForTimeout with waitForResponse or locator assertions
4. Check for shared state: ensure each test gets a fresh browser context
5. Open Trace Viewer on the failing retry - it shows the exact DOM state at each step
6. Add test.fail() annotation for known-flaky tests while the fix is in progress
These are three waitUntil states for page.goto() and page.waitForLoadState():
'domcontentloaded': HTML parsed, DOM ready, CSS and images still loading (fastest)'load': HTML plus subresources (images, CSS, sync scripts) all loaded (default for goto)'networkidle': no network requests for 500ms (slowest; use only for SPAs that fetch heavily on load)await page.goto('/dashboard', { waitUntil: 'networkidle' });
// Most of the time, 'load' or a locator assertion is faster and more reliableInstall the Playwright extension by Microsoft from the VS Code marketplace. It adds a Test Explorer panel listing all discovered tests. You can run, debug, and record tests directly from the editor without opening a terminal. The extension runs Codegen from within VS Code and shows a live browser preview. Interviewers ask this because it significantly tightens the feedback loop - writing, running, and fixing tests in one place instead of context-switching to the terminal.
// High-level API - works for most HTML drag-and-drop implementations
await page.locator('.draggable').dragTo(page.locator('.droptarget'));
// Manual approach for complex or custom drag-and-drop libraries
await page.locator('.draggable').hover();
await page.mouse.down();
await page.mouse.move(300, 400, { steps: 10 }); // steps: smooth the movement
await page.mouse.up();Use the steps option on mouse.move() when the drag target library listens to intermediate mousemove events - without steps, the move is instantaneous and the library may not register it.
The most useful Playwright vs. Selenium question isn't "which is faster?" (Playwright is). It's "which fits your team's constraints?" WebKit support in Playwright means one less Safari device farm to maintain. That's a real infrastructure cost reduction, not a benchmark.
| Dimension | Playwright | Selenium | Cypress |
|---|---|---|---|
| Browser engines | Chromium, Firefox, WebKit | Chromium, Firefox, Safari (via WebDriver) | Chromium only (Firefox beta) |
| Languages | JS, TS, Python, Java, .NET | Java, Python, JS, C#, Ruby | JavaScript/TypeScript only |
| Network interception | Built-in (route()) | Not built-in | Built-in (cy.intercept) |
| Component testing | Yes (@playwright/experimental-ct-react) | No | Yes (Cypress Component) |
| Parallel execution | Native, per-test or per-file | Requires Selenium Grid | Per-spec (no per-test) |
| CI setup effort | Low (one image, --with-deps) | Medium (requires WebDriver server) | Low (built-in dashboard) |
Each tool fits different constraints. Choose Playwright when you need genuine cross-browser coverage (Firefox and WebKit are first-class), require network interception and API testing in one framework, or want component testing alongside E2E. Choose Cypress when your team is JavaScript-only, you want a rich subscription dashboard with time-travel debugging, and you're testing primarily Chrome. Choose Selenium when your organization has existing Java, .NET, or Python infrastructure, needs Appium compatibility for mobile native apps, or requires a mature ecosystem of third-party integrations.
// Playwright can pierce Shadow DOM automatically with CSS
await page.locator('my-component >> input').fill('test');
// Or use the built-in pierce selector
const shadowInput = page.locator('pierce/input');
await shadowInput.fill('hello');Playwright's pierce/ selector crosses shadow boundaries automatically. Most Shadow DOM elements don't need special handling - Playwright's role-based locators work across shadow roots too.
test('page loads in under 3 seconds', async ({ page }) => {
const start = Date.now();
await page.goto('/');
await page.waitForLoadState('load');
const loadTime = Date.now() - start;
expect(loadTime).toBeLessThan(3000);
// Capture Web Performance API timing via CDP
const metrics = await page.evaluate(() =>
JSON.stringify(window.performance.timing)
);
console.log('Performance timing:', metrics);
});Performance reasoning connects directly to system-level thinking. 50 system design patterns covers the architectural patterns senior engineers are expected to apply when scaling test infrastructure.
For most modern web applications, yes. Playwright offers built-in auto-waiting, native cross-browser support for Chromium, Firefox, and WebKit, network interception, and a modern API, all without a separate WebDriver server. Selenium remains relevant for legacy Java and Python codebases and mobile native testing via Appium (TestDino, 2026).
A developer familiar with TypeScript can write production-quality Playwright tests within one to two weeks. The auto-waiting model and role-based locators reduce the learning curve compared to Selenium. Mastering fixtures, POM patterns, and CI integration typically takes four to six weeks of hands-on practice.
The default action timeout is 30 seconds (30,000ms). The default expect/assertion timeout is 5 seconds (5,000ms). The default navigation timeout is 30 seconds. All three can be overridden globally in playwright.config.ts via actionTimeout, expect.timeout, and navigationTimeout, or per-action using the { timeout } option.
Yes. Playwright Test has built-in TypeScript support with no extra configuration required. Running npm init playwright@latest and selecting TypeScript scaffolds a tsconfig.json configured for the test environment. Type definitions for all Playwright APIs ship with the @playwright/test package.
Playwright provides several anti-flakiness layers: auto-waiting before every action, async assertions that retry until passing or timeout, configurable retries (retries: 2 in CI), and test isolation via fresh browser contexts per test. For tests that remain flaky, Trace Viewer records the exact DOM state and network activity at each step, making root-cause analysis fast rather than speculative.
Playwright's growth reflects a real shift in how teams think about browser automation: less manual waiting, more reliable results, and genuine cross-browser coverage by default. These 50+ questions cover the full spectrum interviewers probe in 2026: architecture, design patterns, debugging, CI, and emerging areas like component testing and accessibility. Work through the code examples, build a small POM-based project with fixtures and a GitHub Actions workflow, and you'll walk into any QA or SDET interview prepared for whatever tier they start at.
Also see: JavaScript interview questions - the language fundamentals interviewers test alongside Playwright, and senior frontend interview topics for the broader interview picture.