Playwright Interview Questions: From Basics to Advanced Automation Concepts

Parul Dhingra - Senior Quality Analyst
Parul Dhingra13+ Years ExperienceHire Me

Senior Quality Analyst

Updated: 1/12/2026

Playwright has rapidly become one of the most popular browser automation frameworks since its release by Microsoft in 2020. With over 74% of testers and developers expressing interest in learning Playwright and 4,484+ companies using it for testing and QA, interview questions about Playwright now range from basic setup to advanced architectural decisions. This comprehensive guide covers the essential questions you'll face, organized by difficulty and topic area.

Playwright Fundamentals

Q: What is Playwright and what makes it different from other automation frameworks?

Answer: Playwright is an open-source browser automation framework developed by Microsoft (released in 2020) that enables end-to-end testing of web applications across multiple browsers using a single API.

Key characteristics:

FeatureDescription
Multi-browser supportChromium, Firefox, WebKit (Safari) with single API
Multi-language supportJavaScript/TypeScript, Python, Java, .NET, C#
Auto-waitingBuilt-in waits for elements to be actionable
Browser contextsIsolated test environments without full browser restarts
Network controlRequest/response interception and mocking
Modern protocolUses DevTools Protocol instead of WebDriver

What makes it different:

  • Auto-waiting mechanism eliminates most explicit waits
  • Browser contexts provide lightweight isolation
  • Built-in support for API testing, network mocking, and visual comparisons
  • Trace viewer for powerful debugging
  • Native mobile viewport emulation

Playwright was created by the same team that originally built Puppeteer at Google, bringing their browser automation expertise to Microsoft.

Q: What types of testing does Playwright support?

Answer: Playwright supports various testing types:

1. End-to-End (E2E) Testing:

import { test, expect } from '@playwright/test';
 
test('user login flow', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/.*dashboard/);
});

2. API Testing:

test('API endpoint validation', async ({ request }) => {
  const response = await request.get('https://api.example.com/users');
  expect(response.ok()).toBeTruthy();
  const data = await response.json();
  expect(data).toHaveLength(10);
});

3. Visual Regression Testing:

test('visual comparison', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage.png');
});

4. Component Testing:

test('button component', async ({ mount }) => {
  const component = await mount(<MyButton />);
  await expect(component).toContainText('Click me');
});

5. Functional Testing:

  • Form validations, navigation flows, user interactions

6. Cross-browser Testing:

  • Single codebase runs across Chromium, Firefox, and WebKit

For more on different testing methodologies, see our guide on automated testing fundamentals.

Q: What are the advantages and limitations of Playwright?

Answer:

Advantages:

  • Auto-waiting: Eliminates most flakiness by waiting for elements to be actionable
  • Cross-browser support: One API for Chromium, Firefox, and WebKit
  • Fast execution: Browser contexts are faster than full browser instances
  • Rich feature set: Screenshots, videos, tracing, network interception built-in
  • Modern architecture: DevTools Protocol provides better control
  • TypeScript support: First-class TypeScript support with excellent IDE integration
  • API testing: Built-in request context for API testing without additional tools
  • Parallel execution: Native parallel test execution support
  • Trace viewer: Powerful debugging tool with timeline and screenshots

Limitations:

  • Learning curve: New concepts like fixtures and browser contexts
  • JavaScript/TypeScript focus: While other languages are supported, JS/TS has best support
  • No IE support: Only modern browsers (IE is not supported)
  • Resource intensive: Running multiple browsers can be memory-intensive
  • Newer framework: Smaller community compared to Selenium
  • Mobile limitations: Emulation only, not real device testing

Q: What are Playwright's supported browsers and languages?

Answer:

Browsers:

Browser EngineBrowsers CoveredNotes
ChromiumChrome, Edge, OperaLatest Chromium builds
FirefoxFirefoxLatest Firefox builds
WebKitSafariCross-platform WebKit

Languages:

// TypeScript/JavaScript (most popular)
import { test, expect } from '@playwright/test';
 
test('example', async ({ page }) => {
  await page.goto('https://example.com');
});
# Python
from playwright.sync_api import sync_playwright
 
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://example.com')
// Java
import com.microsoft.playwright.*;
 
Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("https://example.com");
// .NET/C#
using Microsoft.Playwright;
 
var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");

Platform support: Windows, macOS, Linux, Docker containers

Architecture and Browser Communication

Q: Explain the Playwright architecture and how it communicates with browsers.

Answer:

Playwright uses the DevTools Protocol (CDP for Chromium, custom protocols for Firefox/WebKit) rather than the WebDriver protocol used by Selenium.

Test Script (TypeScript/JavaScript)

    Playwright API

    DevTools Protocol

    Browser Engine (Chromium/Firefox/WebKit)

How it works:

  1. Test script calls Playwright API methods
  2. Playwright library translates to DevTools Protocol commands
  3. Protocol communicates directly with browser internals
  4. Browser executes actions and returns results
  5. Playwright processes results and continues execution

Key advantages over WebDriver:

  • Faster communication: Direct protocol vs HTTP requests
  • Better control: Access to browser internals
  • Rich events: Listen to console, network, page events
  • No driver management: Browser binaries bundled with Playwright

Browser Context hierarchy:

// Browser → Context → Page hierarchy
const browser = await chromium.launch();
const context = await browser.newContext(); // Isolated session
const page = await context.newPage();      // Tab within context

Q: What is the difference between Browser, BrowserContext, and Page in Playwright?

Answer:

Browser:

  • Represents the browser instance (Chromium, Firefox, or WebKit)
  • Heavy resource, created once and reused
  • Can contain multiple contexts
const browser = await chromium.launch({ headless: false });

BrowserContext:

  • Isolated incognito-like session within a browser
  • Has its own cookies, storage, cache
  • Lightweight, fast to create
  • Ideal for parallel test execution
  • Can contain multiple pages
const context = await browser.newContext({
  viewport: { width: 1920, height: 1080 },
  permissions: ['geolocation'],
  geolocation: { latitude: 37.7749, longitude: -122.4194 }
});

Page:

  • Represents a single tab within a context
  • Where you perform actual interactions
const page = await context.newPage();
await page.goto('https://example.com');

Hierarchy visualization:

Browser (Chrome instance)
├── BrowserContext 1 (User session 1)
│   ├── Page 1 (Tab 1)
│   └── Page 2 (Tab 2)
└── BrowserContext 2 (User session 2)
    ├── Page 1 (Tab 1)
    └── Page 2 (Tab 2)

Best practice for tests:

test('use context per test', async ({ page }) => {
  // Playwright Test automatically provides isolated context
  // Each test gets fresh context automatically
  await page.goto('https://example.com');
});

Browser contexts are much faster than launching new browser instances, making them perfect for parallel test execution. They provide complete isolation without the overhead of full browser launches.

Installation and Setup

Q: How do you install and configure Playwright?

Answer:

Installation:

# Using npm
npm init playwright@latest
 
# Using yarn
yarn create playwright
 
# Using pnpm
pnpm create playwright

What the installation includes:

  • Playwright Test runner
  • Browser binaries (Chromium, Firefox, WebKit)
  • Example tests
  • Configuration file
  • GitHub Actions workflow (optional)

Manual installation:

npm install -D @playwright/test
npx playwright install

Configuration file (playwright.config.ts):

import { defineConfig, devices } from '@playwright/test';
 
export default defineConfig({
  testDir: './tests',
 
  // Maximum time one test can run
  timeout: 30 * 1000,
 
  // Maximum time for entire test run
  globalTimeout: 60 * 60 * 1000,
 
  // Retry failed tests
  retries: process.env.CI ? 2 : 0,
 
  // Parallel execution
  workers: process.env.CI ? 2 : undefined,
 
  // Reporter configuration
  reporter: [
    ['html'],
    ['junit', { outputFile: 'results.xml' }]
  ],
 
  use: {
    // Base URL
    baseURL: 'http://localhost:3000',
 
    // Browser options
    headless: true,
    viewport: { width: 1280, height: 720 },
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },
 
  // Multiple browser configurations
  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 5'] },
    },
  ],
 
  // Web server configuration
  webServer: {
    command: 'npm run start',
    port: 3000,
    timeout: 120 * 1000,
    reuseExistingServer: !process.env.CI,
  },
});

Q: What is the project configuration in Playwright and why use it?

Answer:

Projects in Playwright allow you to run the same tests across different configurations (browsers, viewports, devices) without code duplication.

Common project configurations:

export default defineConfig({
  projects: [
    // Desktop browsers
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
 
    // Mobile emulation
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 13'] },
    },
 
    // Tablet emulation
    {
      name: 'iPad',
      use: { ...devices['iPad Pro'] },
    },
 
    // Custom configurations
    {
      name: 'logged-in',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'auth.json', // Reuse authentication
      },
    },
  ],
});

Running specific projects:

# Run all projects
npx playwright test
 
# Run specific project
npx playwright test --project=chromium
 
# Run multiple projects
npx playwright test --project=chromium --project=firefox

Benefits:

  • Cross-browser testing with single codebase
  • Device emulation without code changes
  • Environment-specific configurations
  • Parallel execution across configurations

Locators and Selectors

Q: What locator strategies does Playwright support and which are recommended?

Answer:

Playwright provides user-facing locators that create resilient tests. For more details, see our comprehensive guide on Playwright locators and selectors.

Recommended locators (priority order):

LocatorSyntaxUse Case
Rolepage.getByRole('button', { name: 'Submit' })Accessibility-focused, most resilient
Labelpage.getByLabel('Email address')Form inputs with labels
Placeholderpage.getByPlaceholder('Enter email')Inputs with placeholders
Textpage.getByText('Welcome back')Text content
Test IDpage.getByTestId('submit-button')Stable test identifiers
Alt textpage.getByAltText('Profile picture')Images with alt attributes
Titlepage.getByTitle('Close dialog')Elements with title attributes

CSS and XPath (use when necessary):

// CSS Selector
await page.locator('button.submit-btn').click();
 
// XPath
await page.locator('xpath=//button[@class="submit-btn"]').click();

Examples:

// Role-based (recommended)
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
 
// Label-based (forms)
await page.getByLabel('Password').fill('secret123');
 
// Text-based
await page.getByText('Welcome back').waitFor();
 
// Test ID (for dynamic content)
await page.getByTestId('product-123').click();
 
// Chaining locators
await page
  .getByRole('listitem')
  .filter({ hasText: 'Product 1' })
  .getByRole('button', { name: 'Add to cart' })
  .click();

Locator filtering:

// Filter by text
await page.getByRole('listitem').filter({ hasText: 'Apple' });
 
// Filter by another locator
await page
  .getByRole('listitem')
  .filter({ has: page.getByRole('heading', { name: 'Product' }) });
 
// nth element
await page.getByRole('button').nth(2);
 
// first and last
await page.getByRole('listitem').first();
await page.getByRole('listitem').last();

Playwright's role-based locators align with accessibility best practices. If you can't locate an element by role, it might indicate an accessibility issue in your application.

Q: What are Playwright's locator methods and how do they differ?

Answer:

Core locator methods:

// page.locator() - Most flexible, supports CSS and XPath
const button = page.locator('button.submit');
const link = page.locator('xpath=//a[@href="/login"]');
 
// page.getByRole() - Recommended for interactive elements
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('link', { name: 'Learn more' });
await page.getByRole('textbox', { name: 'Email' });
 
// page.getByText() - For text content
await page.getByText('Welcome back');
await page.getByText(/hello/i); // Regex support
 
// page.getByLabel() - For form inputs
await page.getByLabel('Email address');
 
// page.getByPlaceholder() - For inputs with placeholders
await page.getByPlaceholder('Enter your email');
 
// page.getByTestId() - For data-testid attributes
await page.getByTestId('submit-button');

Differences:

MethodReturnsAuto-waitsStrict Mode
locator()LocatorYesYes (throws if multiple)
$()ElementHandleNoNo (returns first)
$$()ElementHandle[]NoNo

Strict mode:

// Throws error if multiple elements match
await page.getByRole('button').click(); // Error if 2+ buttons
 
// Solutions:
await page.getByRole('button', { name: 'Submit' }).click(); // More specific
await page.getByRole('button').first().click(); // Get first
await page.getByRole('button').nth(1).click(); // Get specific index

Best practices:

  1. Prefer getByRole() for resilience and accessibility
  2. Use getByTestId() for dynamic content that lacks stable attributes
  3. Avoid $() and $$() in favor of locator() for auto-waiting
  4. Chain locators for complex scenarios

Q: How do you handle dynamic selectors and elements in Playwright?

Answer:

1. Use flexible locators:

// Bad: Brittle CSS selector
await page.locator('div.container > div:nth-child(2) > button').click();
 
// Good: User-facing locator
await page.getByRole('button', { name: 'Submit' }).click();

2. Text-based matching with regex:

// Exact match
await page.getByText('Product: Apple iPhone');
 
// Partial match with regex
await page.getByText(/Product:.*iPhone/);
 
// Case-insensitive
await page.getByText(/submit/i);

3. Filter by attributes:

// Filter by data attributes
await page.locator('[data-product-id="123"]');
 
// Filter by multiple attributes
await page.locator('button[type="submit"][disabled]');

4. Wait for dynamic elements:

// Wait for element to appear
await page.waitForSelector('.dynamic-content', { state: 'visible' });
 
// Wait for element with specific text
await page.getByText('Loading complete').waitFor();
 
// Wait for network to be idle
await page.waitForLoadState('networkidle');

5. Handle dynamic IDs:

// Bad: Hardcoded ID
await page.locator('#product-12345').click();
 
// Good: Use data-testid or partial match
await page.locator('[data-testid="product-item"]').click();
await page.locator('[id^="product-"]').click(); // Starts with

6. Use test IDs for stability:

<!-- Add data-testid in HTML -->
<button data-testid="checkout-btn">Checkout</button>
// Reference in tests
await page.getByTestId('checkout-btn').click();

Auto-Waiting and Synchronization

Q: What is Playwright's auto-waiting mechanism and how does it work?

Answer:

Playwright automatically waits for elements to be actionable before performing actions, eliminating most explicit waits and reducing test flakiness.

What "actionable" means:

Before clicking, filling, or interacting with an element, Playwright ensures:

  1. Attached: Element is attached to the DOM
  2. Visible: Element is visible (not display: none or visibility: hidden)
  3. Stable: Element is not animating or moving
  4. Receives events: Element can receive pointer events (not covered by another element)
  5. Enabled: Element is not disabled

Auto-waiting example:

// Playwright automatically waits for all conditions
await page.getByRole('button', { name: 'Submit' }).click();
 
// Behind the scenes, Playwright:
// 1. Waits for button to be attached to DOM
// 2. Waits for button to be visible
// 3. Waits for button to be stable (not animating)
// 4. Waits for button to be enabled
// 5. Waits for button to receive events
// 6. Then clicks

Comparison with Selenium:

// Selenium (requires explicit waits)
WebDriverWait wait = new WebDriverWait(driver, 10);
WebElement button = wait.until(
  ExpectedConditions.elementToBeClickable(By.id("submit"))
);
button.click();
 
// Playwright (auto-waits)
await page.getByRole('button', { name: 'Submit' }).click();

Auto-waiting for navigation:

// Waits for navigation to complete
await page.click('a[href="/products"]');
 
// Waits for specific URL
await expect(page).toHaveURL('https://example.com/products');
 
// Waits for load state
await page.waitForLoadState('networkidle');

Actions with built-in auto-waiting:

  • click()
  • fill()
  • type()
  • selectOption()
  • check()
  • uncheck()
  • hover()
  • focus()
  • press()

Auto-waiting is one of Playwright's most powerful features. It eliminates the need for arbitrary sleep() statements and most explicit waits, making tests more reliable and maintainable.

Q: When and how do you use explicit waits in Playwright?

Answer:

While auto-waiting handles most scenarios, explicit waits are needed for specific conditions.

1. Wait for selector with states:

// Wait for element to be visible
await page.waitForSelector('.notification', { state: 'visible' });
 
// Wait for element to be hidden
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
 
// Wait for element to be detached from DOM
await page.waitForSelector('.modal', { state: 'detached' });

2. Wait for load states:

// Wait for DOMContentLoaded
await page.waitForLoadState('domcontentloaded');
 
// Wait for all resources to load
await page.waitForLoadState('load');
 
// Wait for network to be idle (no network activity for 500ms)
await page.waitForLoadState('networkidle');

3. Wait for specific timeout:

// Wait for 2 seconds
await page.waitForTimeout(2000);
 
// Note: Avoid this when possible, prefer condition-based waits

4. Wait for function:

// Wait for custom condition
await page.waitForFunction(() => {
  return document.querySelectorAll('.product-item').length > 10;
});
 
// With arguments
await page.waitForFunction(
  (minPrice) => {
    const prices = Array.from(document.querySelectorAll('.price'));
    return prices.some(p => parseFloat(p.textContent) < minPrice);
  },
  50 // Argument passed to function
);

5. Wait for response:

// Wait for specific API response
const responsePromise = page.waitForResponse(
  response => response.url().includes('/api/products') && response.status() === 200
);
await page.click('button.load-more');
await responsePromise;

6. Wait for request:

// Wait for specific request
await page.waitForRequest(request =>
  request.url().includes('/api/analytics')
);

7. Wait for event:

// Wait for console message
const messagePromise = page.waitForEvent('console');
await page.click('button.log');
const message = await messagePromise;
 
// Wait for dialog
const dialogPromise = page.waitForEvent('dialog');
await page.click('button.alert');
const dialog = await dialogPromise;
await dialog.accept();

Best practices:

  • Prefer auto-waiting over explicit waits
  • Use condition-based waits (waitForSelector, waitForFunction) over waitForTimeout
  • Use waitForLoadState('networkidle') for SPAs with dynamic content
  • Combine waits with assertions for reliability

Assertions and Validations

Q: What assertion methods does Playwright provide?

Answer:

Playwright uses the expect assertion library (based on Jest expect) with auto-waiting for assertions.

Page assertions:

import { expect } from '@playwright/test';
 
// URL assertions
await expect(page).toHaveURL('https://example.com/products');
await expect(page).toHaveURL(/.*products/); // Regex
 
// Title assertions
await expect(page).toHaveTitle('Products | Example Store');
await expect(page).toHaveTitle(/Products/);

Element assertions:

// Visibility
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
await expect(page.locator('.error-message')).toBeHidden();
 
// Enabled/Disabled
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
 
// Checked state
await expect(page.getByRole('checkbox')).toBeChecked();
await expect(page.getByRole('checkbox')).not.toBeChecked();
 
// Text content
await expect(page.locator('.heading')).toHaveText('Welcome');
await expect(page.locator('.heading')).toContainText('Welcome');
 
// Attribute
await expect(page.locator('button')).toHaveAttribute('type', 'submit');
await expect(page.locator('a')).toHaveAttribute('href', /.*products/);
 
// Class
await expect(page.locator('div')).toHaveClass('active');
await expect(page.locator('div')).toHaveClass(/.*active.*/);
 
// Count
await expect(page.locator('.product-item')).toHaveCount(10);
 
// Value (for inputs)
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');

Screenshot assertions:

// Visual comparison
await expect(page).toHaveScreenshot('homepage.png');
await expect(page.locator('.header')).toHaveScreenshot('header.png');

API response assertions:

const response = await page.request.get('https://api.example.com/users');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toHaveLength(10);
expect(data[0]).toHaveProperty('name');

Custom assertions:

// Extend expect
expect.extend({
  async toHaveProductCount(locator, expected) {
    const count = await locator.count();
    const pass = count === expected;
    return {
      pass,
      message: () => `Expected ${expected} products, got ${count}`,
    };
  },
});
 
// Use custom assertion
await expect(page.locator('.product')).toHaveProductCount(5);

Q: What is the difference between hard and soft assertions in Playwright?

Answer:

Hard assertions (default):

  • Test stops immediately when assertion fails
  • Subsequent assertions are not executed
  • Recommended for most scenarios
test('hard assertions', async ({ page }) => {
  await page.goto('https://example.com');
 
  // If this fails, test stops here
  await expect(page).toHaveTitle('Products');
 
  // This won't execute if title assertion fails
  await expect(page.locator('.product')).toHaveCount(10);
});

Soft assertions:

  • Test continues even if assertion fails
  • All soft assertion failures are reported at the end
  • Useful for collecting multiple validation failures
test('soft assertions', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Use expect.soft() for soft assertions
  await expect.soft(page).toHaveTitle('Products');
  await expect.soft(page.locator('.product')).toHaveCount(10);
  await expect.soft(page.locator('.header')).toBeVisible();
 
  // All failures are collected and reported at the end
});

Output example:

  1) [chromium] › example.spec.ts:3:1 › soft assertions

     Error: expect.soft(received).toHaveTitle(expected)
     Expected: "Products"
     Received: "Home"

     Error: expect.soft(received).toHaveCount(expected)
     Expected: 10
     Received: 5

When to use soft assertions:

  • Validating multiple independent properties
  • Checking multiple elements on a page
  • Collecting all validation failures for comprehensive reporting
  • UI validation tests where you want to see all issues

When to use hard assertions:

  • Critical validations that must pass for subsequent steps
  • Login flows, navigation, data setup
  • Most functional tests

API Testing and Network Interception

Q: How do you perform API testing in Playwright?

Answer:

Playwright has built-in support for API testing using the request context. For comprehensive coverage, see our guide on Playwright API testing and mocking.

Basic API testing:

import { test, expect } from '@playwright/test';
 
test('API endpoint validation', async ({ request }) => {
  // GET request
  const response = await request.get('https://api.example.com/users');
 
  // Status validation
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);
 
  // Response body validation
  const users = await response.json();
  expect(users).toHaveLength(10);
  expect(users[0]).toHaveProperty('name');
  expect(users[0]).toHaveProperty('email');
});

POST request:

test('create user via API', async ({ request }) => {
  const response = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com',
      role: 'admin'
    },
    headers: {
      'Authorization': 'Bearer token123',
      'Content-Type': 'application/json'
    }
  });
 
  expect(response.status()).toBe(201);
  const user = await response.json();
  expect(user.id).toBeDefined();
  expect(user.name).toBe('John Doe');
});

Other HTTP methods:

// PUT request
await request.put('https://api.example.com/users/123', {
  data: { name: 'Jane Doe' }
});
 
// PATCH request
await request.patch('https://api.example.com/users/123', {
  data: { role: 'moderator' }
});
 
// DELETE request
await request.delete('https://api.example.com/users/123');
 
// HEAD request
const response = await request.head('https://api.example.com/users');
expect(response.status()).toBe(200);

API testing with setup:

test.describe('User API', () => {
  let apiContext;
 
  test.beforeAll(async ({ playwright }) => {
    // Create API context with base URL and auth
    apiContext = await playwright.request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        'Authorization': 'Bearer token123',
        'Accept': 'application/json'
      }
    });
  });
 
  test.afterAll(async () => {
    await apiContext.dispose();
  });
 
  test('get users', async () => {
    const response = await apiContext.get('/users');
    expect(response.ok()).toBeTruthy();
  });
 
  test('create user', async () => {
    const response = await apiContext.post('/users', {
      data: { name: 'Test User' }
    });
    expect(response.status()).toBe(201);
  });
});

Combining API and UI testing:

test('setup data via API, validate via UI', async ({ request, page }) => {
  // Create product via API
  const response = await request.post('https://api.example.com/products', {
    data: {
      name: 'Test Product',
      price: 99.99,
      stock: 100
    }
  });
  const product = await response.json();
 
  // Verify product appears in UI
  await page.goto('https://example.com/products');
  await expect(page.getByText('Test Product')).toBeVisible();
  await expect(page.getByText('$99.99')).toBeVisible();
});

Q: How do you intercept and mock network requests in Playwright?

Answer:

Network interception allows you to modify requests/responses, mock API calls, and test error scenarios.

Intercept and modify responses:

test('mock API response', async ({ page }) => {
  // Intercept specific route
  await page.route('**/api/products', async (route) => {
    // Fulfill with mock data
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Mock Product 1', price: 10 },
        { id: 2, name: 'Mock Product 2', price: 20 }
      ])
    });
  });
 
  await page.goto('https://example.com/products');
 
  // Verify mock data is displayed
  await expect(page.getByText('Mock Product 1')).toBeVisible();
});

Modify existing responses:

test('modify API response', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    // Get original response
    const response = await route.fetch();
    const json = await response.json();
 
    // Modify response
    json[0].name = 'Modified Name';
 
    // Fulfill with modified data
    await route.fulfill({
      response,
      body: JSON.stringify(json)
    });
  });
 
  await page.goto('https://example.com/users');
});

Abort requests:

test('block analytics', async ({ page }) => {
  // Block specific requests
  await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
  await page.route('**/google-analytics.com/**', route => route.abort());
 
  await page.goto('https://example.com');
});

Simulate network errors:

test('handle API errors', async ({ page }) => {
  await page.route('**/api/products', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal server error' })
    });
  });
 
  await page.goto('https://example.com/products');
 
  // Verify error handling
  await expect(page.getByText('Failed to load products')).toBeVisible();
});

Capture and log requests:

test('log all API requests', async ({ page }) => {
  // Listen to all requests
  page.on('request', request => {
    if (request.url().includes('/api/')) {
      console.log('API Request:', request.method(), request.url());
    }
  });
 
  // Listen to responses
  page.on('response', response => {
    if (response.url().includes('/api/')) {
      console.log('API Response:', response.status(), response.url());
    }
  });
 
  await page.goto('https://example.com');
});

Wait for specific responses:

test('wait for API call', async ({ page }) => {
  const responsePromise = page.waitForResponse(
    response => response.url().includes('/api/products') && response.status() === 200
  );
 
  await page.click('button.load-products');
  const response = await responsePromise;
 
  const products = await response.json();
  expect(products).toHaveLength(10);
});

Visual Testing and Screenshots

Q: How do you perform visual regression testing in Playwright?

Answer:

Playwright has built-in visual comparison capabilities that compare screenshots pixel-by-pixel.

Basic screenshot comparison:

test('homepage visual test', async ({ page }) => {
  await page.goto('https://example.com');
 
  // First run: generates reference screenshot
  // Subsequent runs: compares against reference
  await expect(page).toHaveScreenshot('homepage.png');
});

Element screenshot:

test('header visual test', async ({ page }) => {
  await page.goto('https://example.com');
 
  const header = page.locator('header');
  await expect(header).toHaveScreenshot('header.png');
});

Screenshot options:

test('visual test with options', async ({ page }) => {
  await page.goto('https://example.com');
 
  await expect(page).toHaveScreenshot('homepage.png', {
    // Maximum pixel difference threshold (0-1)
    maxDiffPixels: 100,
 
    // Maximum pixel difference ratio (0-1)
    maxDiffPixelRatio: 0.05,
 
    // Mask dynamic areas
    mask: [page.locator('.timestamp')],
 
    // Full page screenshot (including scrollable content)
    fullPage: true,
 
    // Clip to specific area
    clip: { x: 0, y: 0, width: 1280, height: 720 },
 
    // Hide specific elements
    maskColor: '#FF00FF'
  });
});

Masking dynamic content:

test('mask dynamic elements', async ({ page }) => {
  await page.goto('https://example.com');
 
  await expect(page).toHaveScreenshot('page.png', {
    mask: [
      page.locator('.timestamp'),
      page.locator('.user-avatar'),
      page.locator('.ad-banner')
    ]
  });
});

Updating screenshots:

# Update all screenshots
npx playwright test --update-snapshots
 
# Update specific test
npx playwright test visual.spec.ts --update-snapshots
 
# Update for specific project
npx playwright test --project=chromium --update-snapshots

Cross-browser visual testing:

// Screenshots are stored per project
// tests/visual.spec.ts-snapshots/homepage-chromium.png
// tests/visual.spec.ts-snapshots/homepage-firefox.png
// tests/visual.spec.ts-snapshots/homepage-webkit.png
 
test('cross-browser visual test', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage.png');
});

Configuration in playwright.config.ts:

export default defineConfig({
  use: {
    screenshot: {
      mode: 'only-on-failure',
      fullPage: true
    }
  },
 
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      threshold: 0.2
    }
  }
});

Q: How do you capture screenshots and videos in Playwright?

Answer:

Manual screenshots:

test('capture screenshot', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Full page screenshot
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
 
  // Viewport screenshot
  await page.screenshot({ path: 'viewport.png' });
 
  // Element screenshot
  const element = page.locator('header');
  await element.screenshot({ path: 'header.png' });
});

Automatic screenshots on failure:

// playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    // OR
    screenshot: 'on',  // Always capture
    screenshot: 'off', // Never capture
  }
});

Video recording:

// playwright.config.ts
export default defineConfig({
  use: {
    video: 'retain-on-failure',
    // OR
    video: 'on',                    // Always record
    video: 'off',                   // Never record
    video: 'on-first-retry',        // Record only retries
 
    videoSize: { width: 1280, height: 720 }
  }
});

Access video path in test:

test('access video', async ({ page }, testInfo) => {
  await page.goto('https://example.com');
  // Test actions...
 
  // Video path available after test
  const videoPath = await page.video().path();
  console.log('Video saved to:', videoPath);
});

Save videos in custom location:

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus) {
    const videoPath = await page.video().path();
    await testInfo.attach('video', { path: videoPath, contentType: 'video/webm' });
  }
});

Fixtures and Test Organization

Q: What are fixtures in Playwright and how do you use them?

Answer:

Fixtures are Playwright's way of setting up and tearing down test dependencies. They provide a clean way to share setup logic across tests.

Built-in fixtures:

import { test } from '@playwright/test';
 
test('use built-in fixtures', async ({ page, context, browser }) => {
  // 'page' - isolated Page instance
  // 'context' - isolated BrowserContext
  // 'browser' - Browser instance
 
  await page.goto('https://example.com');
});

Custom fixtures:

// fixtures.ts
import { test as base } from '@playwright/test';
 
// Define custom fixtures
type MyFixtures = {
  authenticatedPage: Page;
  testUser: { email: string; password: string };
};
 
export const test = base.extend<MyFixtures>({
  // Simple fixture
  testUser: async ({}, use) => {
    const user = {
      email: 'test@example.com',
      password: 'password123'
    };
    await use(user);
  },
 
  // Fixture that depends on other fixtures
  authenticatedPage: async ({ page, testUser }, use) => {
    // Setup: Login
    await page.goto('https://example.com/login');
    await page.fill('[name="email"]', testUser.email);
    await page.fill('[name="password"]', testUser.password);
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
 
    // Provide authenticated page to test
    await use(page);
 
    // Teardown: Logout
    await page.click('button.logout');
  }
});

Using custom fixtures:

import { test } from './fixtures';
 
test('access dashboard', async ({ authenticatedPage }) => {
  // Page is already logged in
  await authenticatedPage.goto('https://example.com/dashboard');
  await expect(authenticatedPage.locator('h1')).toHaveText('Dashboard');
});

Worker-scoped fixtures:

// Shared across tests in same worker
export const test = base.extend({
  workerStorageState: [async ({ browser }, use) => {
    // Setup once per worker
    const page = await browser.newPage();
    await page.goto('https://example.com/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
 
    // Save authentication state
    await page.context().storageState({ path: 'auth.json' });
    await page.close();
 
    await use('auth.json');
  }, { scope: 'worker' }],
 
  page: async ({ page, workerStorageState }, use) => {
    // All pages in this worker reuse auth state
    await use(page);
  }
});

Fixture with options:

export const test = base.extend<{ apiURL: string }>({
  apiURL: ['https://api.example.com', { option: true }],
 
  request: async ({ apiURL }, use) => {
    const context = await request.newContext({ baseURL: apiURL });
    await use(context);
    await context.dispose();
  }
});
 
// Override in test file
test.use({ apiURL: 'https://staging.api.example.com' });

Q: How do you organize tests with hooks in Playwright?

Answer:

Test hooks:

import { test, expect } from '@playwright/test';
 
// Runs before each test
test.beforeEach(async ({ page }) => {
  await page.goto('https://example.com');
  // Login, setup, etc.
});
 
// Runs after each test
test.afterEach(async ({ page }) => {
  // Cleanup, logout, etc.
});
 
// Runs once before all tests in file
test.beforeAll(async ({ browser }) => {
  // One-time setup
});
 
// Runs once after all tests in file
test.afterAll(async ({ browser }) => {
  // One-time cleanup
});

Grouped tests with describe:

test.describe('Product page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/products');
  });
 
  test('displays products', async ({ page }) => {
    await expect(page.locator('.product')).toHaveCount(10);
  });
 
  test('filters products', async ({ page }) => {
    await page.click('button.filter-electronics');
    await expect(page.locator('.product')).toHaveCount(5);
  });
});

Nested describe blocks:

test.describe('Shopping cart', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com');
  });
 
  test.describe('Empty cart', () => {
    test('shows empty message', async ({ page }) => {
      await page.goto('https://example.com/cart');
      await expect(page.getByText('Your cart is empty')).toBeVisible();
    });
  });
 
  test.describe('With items', () => {
    test.beforeEach(async ({ page }) => {
      // Add items to cart
      await page.goto('https://example.com/products');
      await page.click('.product:first-child button.add-to-cart');
    });
 
    test('shows cart count', async ({ page }) => {
      await expect(page.locator('.cart-count')).toHaveText('1');
    });
  });
});

Conditional hooks:

test.beforeEach(async ({ page, browserName }) => {
  // Skip setup for specific browser
  if (browserName === 'webkit') {
    test.skip();
  }
 
  await page.goto('https://example.com');
});

Test metadata and annotations:

test('critical user flow', {
  tag: '@smoke',
  annotation: { type: 'issue', description: 'https://github.com/org/repo/issues/123' }
}, async ({ page }) => {
  // Test implementation
});
 
// Run tests by tag
// npx playwright test --grep @smoke

Parallel Execution and Performance

Q: How does Playwright handle parallel test execution?

Answer:

Playwright runs tests in parallel by default using worker processes for maximum efficiency.

Default parallel execution:

// playwright.config.ts
export default defineConfig({
  // Number of parallel workers
  workers: process.env.CI ? 2 : undefined, // undefined = CPU cores
 
  // Run tests within a file in parallel
  fullyParallel: true,
});

Worker configuration:

# Disable parallelism (single worker)
npx playwright test --workers=1
 
# Specific number of workers
npx playwright test --workers=4
 
# Use 50% of CPU cores
npx playwright test --workers=50%

Test execution modes:

// Default: Tests in single file run sequentially
test.describe('Sequential tests', () => {
  test('test 1', async ({ page }) => { /* ... */ });
  test('test 2', async ({ page }) => { /* ... */ });
  // test 2 runs after test 1 completes
});
 
// Parallel mode within file
test.describe.configure({ mode: 'parallel' });
test.describe('Parallel tests', () => {
  test('test 1', async ({ page }) => { /* ... */ });
  test('test 2', async ({ page }) => { /* ... */ });
  // test 1 and test 2 run in different workers
});
 
// Serial mode (runs sequentially even if fully parallel enabled)
test.describe.configure({ mode: 'serial' });
test.describe('Serial tests', () => {
  test('test 1', async ({ page }) => { /* ... */ });
  test('test 2', async ({ page }) => { /* ... */ });
  // Strictly sequential, test 2 waits for test 1
});

Worker isolation:

// Each worker gets:
// - Its own browser instance
// - Isolated browser contexts
// - Separate process memory
// - No shared state between workers
 
test('isolated test 1', async ({ page }) => {
  // Runs in worker 1
  await page.goto('https://example.com');
});
 
test('isolated test 2', async ({ page }) => {
  // Runs in worker 2 (completely isolated from test 1)
  await page.goto('https://example.com');
});

Sharding for CI/CD:

// playwright.config.ts
export default defineConfig({
  // Split tests across multiple machines
  shard: process.env.CI ? { current: 1, total: 3 } : undefined,
});
# Run in 3 parallel CI jobs
npx playwright test --shard=1/3  # Machine 1
npx playwright test --shard=2/3  # Machine 2
npx playwright test --shard=3/3  # Machine 3

Limit failures to save resources:

export default defineConfig({
  // Stop after N failures
  maxFailures: process.env.CI ? 10 : undefined,
});
npx playwright test --max-failures=5

Q: How do you optimize test performance in Playwright?

Answer:

1. Reuse authentication state:

// Global setup (auth.setup.ts)
import { test as setup } from '@playwright/test';
 
setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('[name="email"]', 'user@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');
 
  // Save signed-in state
  await page.context().storageState({ path: 'auth.json' });
});
 
// playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'auth.json',
      },
      dependencies: ['setup'],
    },
  ],
});
 
// All tests now start authenticated
test('access dashboard', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  // Already logged in!
});

2. Use API for test data setup:

test('verify product in UI', async ({ request, page }) => {
  // Create product via API (fast)
  const response = await request.post('/api/products', {
    data: { name: 'Test Product', price: 99 }
  });
  const product = await response.json();
 
  // Verify in UI (only when needed)
  await page.goto(`/products/${product.id}`);
  await expect(page.locator('h1')).toHaveText('Test Product');
});

3. Use browser contexts efficiently:

// Slow: New browser per test
test('slow approach', async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  // Test...
  await browser.close();
});
 
// Fast: Reuse browser, new context per test
test('fast approach', async ({ page }) => {
  // Playwright Test automatically provides isolated context
  // Browser is reused across tests
});

4. Parallelize within files:

test.describe.configure({ mode: 'parallel' });
 
test.describe('Independent tests', () => {
  test('test 1', async ({ page }) => { /* ... */ });
  test('test 2', async ({ page }) => { /* ... */ });
  test('test 3', async ({ page }) => { /* ... */ });
  // All run in parallel
});

5. Skip unnecessary waits:

// Bad: Arbitrary timeout
await page.waitForTimeout(5000);
 
// Good: Condition-based wait
await page.waitForLoadState('networkidle');
 
// Better: Auto-waiting assertion
await expect(page.locator('.content')).toBeVisible();

6. Optimize network:

// Block unnecessary resources
test.beforeEach(async ({ page }) => {
  await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => route.abort());
  await page.route('**/analytics.js', route => route.abort());
});

7. Use headless mode:

export default defineConfig({
  use: {
    headless: true, // Faster than headed mode
  },
});

8. Configure timeouts appropriately:

export default defineConfig({
  timeout: 30 * 1000,         // Test timeout
  expect: {
    timeout: 5 * 1000,        // Assertion timeout
  },
  use: {
    actionTimeout: 10 * 1000, // Action timeout
    navigationTimeout: 30 * 1000,
  },
});

CI/CD Integration

Q: How do you integrate Playwright with CI/CD pipelines?

Answer:

Playwright provides excellent CI/CD integration with minimal configuration.

GitHub Actions:

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
 
    - uses: actions/setup-node@v3
      with:
        node-version: 18
 
    - name: Install dependencies
      run: npm ci
 
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
 
    - name: Run Playwright tests
      run: npx playwright test
 
    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Parallel execution across multiple jobs:

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: 18
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report-${{ matrix.shardIndex }}
        path: playwright-report/

GitLab CI:

# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.40.0-jammy
 
stages:
  - test
 
playwright-tests:
  stage: test
  script:
    - npm ci
    - npx playwright test
  artifacts:
    when: always
    paths:
      - playwright-report/
    expire_in: 30 days
  only:
    - main
    - merge_requests

Jenkins:

pipeline {
  agent {
    docker {
      image 'mcr.microsoft.com/playwright:v1.40.0-jammy'
    }
  }
  stages {
    stage('Install Dependencies') {
      steps {
        sh 'npm ci'
      }
    }
    stage('Run Tests') {
      steps {
        sh 'npx playwright test'
      }
    }
  }
  post {
    always {
      publishHTML([
        reportDir: 'playwright-report',
        reportFiles: 'index.html',
        reportName: 'Playwright Report'
      ])
    }
  }
}

Docker container:

FROM mcr.microsoft.com/playwright:v1.40.0-jammy
 
WORKDIR /app
 
COPY package*.json ./
RUN npm ci
 
COPY . .
 
CMD ["npx", "playwright", "test"]

CI-specific configuration:

// playwright.config.ts
export default defineConfig({
  // Limit workers in CI
  workers: process.env.CI ? 2 : undefined,
 
  // Enable retries in CI
  retries: process.env.CI ? 2 : 0,
 
  // Use different reporter for CI
  reporter: process.env.CI
    ? [['junit', { outputFile: 'results.xml' }], ['html']]
    : [['html']],
 
  use: {
    // Always run headless in CI
    headless: true,
 
    // Capture traces on first retry
    trace: 'on-first-retry',
 
    // Capture screenshots on failure
    screenshot: 'only-on-failure',
 
    // Capture video on failure
    video: 'retain-on-failure',
 
    // Base URL from environment
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
  },
});

Advanced Scenarios

Q: How do you handle multiple tabs and windows in Playwright?

Answer:

Opening new tabs:

test('handle new tab', async ({ context, page }) => {
  await page.goto('https://example.com');
 
  // Wait for new page to open
  const [newPage] = await Promise.all([
    context.waitForEvent('page'),
    page.click('a[target="_blank"]') // Click link that opens new tab
  ]);
 
  // Wait for new page to load
  await newPage.waitForLoadState();
 
  // Work with new page
  await expect(newPage).toHaveURL(/.*products/);
  await newPage.click('button.add-to-cart');
 
  // Switch back to original page
  await page.bringToFront();
});

Opening new window programmatically:

test('open new window', async ({ context, page }) => {
  await page.goto('https://example.com');
 
  // Open new tab
  const newPage = await context.newPage();
  await newPage.goto('https://example.com/products');
 
  // Work with multiple pages
  await newPage.click('button.filter');
  await page.click('button.logout');
 
  // Close new page
  await newPage.close();
});

Managing multiple pages:

test('work with multiple tabs', async ({ context, page }) => {
  await page.goto('https://example.com');
 
  const page2 = await context.newPage();
  await page2.goto('https://example.com/products');
 
  const page3 = await context.newPage();
  await page3.goto('https://example.com/cart');
 
  // Get all pages
  const pages = context.pages();
  console.log('Total pages:', pages.length);
 
  // Close all except first
  for (let i = 1; i < pages.length; i++) {
    await pages[i].close();
  }
});

Q: How do you handle iframes in Playwright?

Answer:

Accessing iframe content:

test('interact with iframe', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Get frame by selector
  const frame = page.frameLocator('iframe[name="payment-form"]');
 
  // Interact with elements inside iframe
  await frame.locator('#card-number').fill('4111111111111111');
  await frame.locator('#expiry').fill('12/25');
  await frame.locator('#cvv').fill('123');
  await frame.locator('button[type="submit"]').click();
});

Multiple iframe methods:

// Method 1: frameLocator (recommended)
const frame = page.frameLocator('iframe#my-frame');
await frame.locator('button').click();
 
// Method 2: frame by name
const frame = page.frame('frame-name');
await frame.locator('button').click();
 
// Method 3: frame by URL
const frame = page.frame({ url: /.*checkout.*/ });
await frame.locator('button').click();

Nested iframes:

test('nested iframes', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Access nested iframe
  const parentFrame = page.frameLocator('iframe#parent');
  const childFrame = parentFrame.frameLocator('iframe#child');
 
  await childFrame.locator('input').fill('text');
});

Q: How do you handle file uploads and downloads in Playwright?

Answer:

File uploads:

test('upload file', async ({ page }) => {
  await page.goto('https://example.com/upload');
 
  // Upload single file
  await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
 
  // Upload multiple files
  await page.setInputFiles('input[type="file"]', [
    'path/to/file1.pdf',
    'path/to/file2.pdf'
  ]);
 
  // Upload from buffer
  await page.setInputFiles('input[type="file"]', {
    name: 'test.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('File content')
  });
 
  // Remove files
  await page.setInputFiles('input[type="file"]', []);
});

File downloads:

test('download file', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Wait for download
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('a#download-link')
  ]);
 
  // Get download details
  console.log('Filename:', download.suggestedFilename());
 
  // Save to specific path
  await download.saveAs('downloads/' + download.suggestedFilename());
 
  // Or get readable stream
  const stream = await download.createReadStream();
 
  // Delete downloaded file
  await download.delete();
});

Q: How do you handle authentication and cookies in Playwright?

Answer:

Save and reuse authentication state:

// Login once and save state
test('save auth', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('[name="email"]', 'user@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');
 
  // Save cookies and localStorage
  await page.context().storageState({ path: 'auth.json' });
});
 
// Reuse auth state in other tests
test.use({ storageState: 'auth.json' });
 
test('access protected page', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  // Already authenticated!
});

Manual cookie management:

test('set cookies', async ({ context, page }) => {
  // Add cookies
  await context.addCookies([
    {
      name: 'session',
      value: 'abc123',
      domain: 'example.com',
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: 'Lax'
    }
  ]);
 
  await page.goto('https://example.com/dashboard');
 
  // Get cookies
  const cookies = await context.cookies();
  console.log('Cookies:', cookies);
 
  // Clear cookies
  await context.clearCookies();
});

HTTP authentication:

test('basic auth', async ({ page }) => {
  await page.goto('https://example.com', {
    waitUntil: 'networkidle'
  });
 
  // Handle basic auth dialog
  await page.authenticate({
    username: 'admin',
    password: 'password123'
  });
});
 
// Or set in context
const context = await browser.newContext({
  httpCredentials: {
    username: 'admin',
    password: 'password123'
  }
});

Q: How do you handle geolocation and permissions in Playwright?

Answer:

Geolocation:

test('set geolocation', async ({ context, page }) => {
  // Set geolocation
  await context.setGeolocation({
    latitude: 37.7749,
    longitude: -122.4194
  });
 
  // Grant geolocation permission
  await context.grantPermissions(['geolocation']);
 
  await page.goto('https://example.com/map');
 
  // Verify location is used
  await expect(page.locator('.location')).toContainText('San Francisco');
});
 
// Or set in context creation
const context = await browser.newContext({
  geolocation: { latitude: 37.7749, longitude: -122.4194 },
  permissions: ['geolocation']
});

Permissions:

test('manage permissions', async ({ context, page }) => {
  // Grant permissions
  await context.grantPermissions(['notifications', 'camera', 'microphone']);
 
  // Grant for specific origin
  await context.grantPermissions(['clipboard-read'], {
    origin: 'https://example.com'
  });
 
  await page.goto('https://example.com');
 
  // Clear permissions
  await context.clearPermissions();
});

Available permissions:

  • geolocation
  • notifications
  • camera
  • microphone
  • clipboard-read
  • clipboard-write

Debugging and Troubleshooting

Q: What debugging tools does Playwright provide?

Answer:

1. Playwright Inspector:

# Run tests in debug mode
npx playwright test --debug
 
# Debug specific test
npx playwright test example.spec.ts --debug
 
# Debug from specific line
npx playwright test --debug-on-line=25

Features:

  • Step through test execution
  • Inspect locators in real-time
  • View action logs
  • Evaluate expressions

2. Trace Viewer:

// playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry', // or 'on', 'off', 'retain-on-failure'
  },
});
# View trace
npx playwright show-trace trace.zip

Features:

  • Timeline of actions
  • Screenshots at each step
  • Network activity
  • Console logs
  • Source code
  • Metadata

3. Pause execution:

test('debug test', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Pause test execution
  await page.pause();
 
  // Continue in inspector
  await page.click('button');
});

4. Verbose logging:

# Debug protocol
DEBUG=pw:api npx playwright test
 
# Debug browser
DEBUG=pw:browser npx playwright test
 
# All debug output
DEBUG=pw:* npx playwright test

5. Headed mode:

# Run tests with visible browser
npx playwright test --headed
 
# Slow down execution
npx playwright test --headed --slow-mo=1000

6. Screenshots and videos:

test('debug with media', async ({ page }) => {
  await page.goto('https://example.com');
 
  // Take screenshot at specific point
  await page.screenshot({ path: 'debug.png' });
 
  // Highlight element
  await page.locator('button').highlight();
});

7. Console logs:

test('capture console', async ({ page }) => {
  // Listen to console
  page.on('console', msg => {
    console.log('Browser console:', msg.type(), msg.text());
  });
 
  await page.goto('https://example.com');
});

Q: How do you handle flaky tests in Playwright?

Answer:

1. Use auto-waiting (built-in):

// Playwright automatically waits for elements to be actionable
await page.click('button'); // Waits for button to be visible, enabled, stable

2. Configure retries:

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
 
  // Or per test
  test.describe('flaky suite', () => {
    test.describe.configure({ retries: 3 });
 
    test('flaky test', async ({ page }) => {
      // Test logic
    });
  });
});

3. Wait for specific conditions:

// Wait for network to be idle
await page.waitForLoadState('networkidle');
 
// Wait for element state
await page.waitForSelector('.content', { state: 'visible' });
 
// Wait for response
await page.waitForResponse(response =>
  response.url().includes('/api/data') && response.status() === 200
);

4. Use web-first assertions (auto-retry):

// These assertions auto-retry until condition is met
await expect(page.locator('.status')).toHaveText('Complete');
await expect(page.locator('.count')).toHaveText('10');

5. Increase timeouts for slow operations:

// Increase timeout for specific action
await page.click('button.slow-operation', { timeout: 60000 });
 
// Increase assertion timeout
await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });

6. Isolate tests:

// Each test gets fresh context (automatic in Playwright Test)
test('isolated test', async ({ page }) => {
  // No state from previous tests
});

7. Handle animations:

// Disable animations
await page.addInitScript(() => {
  document.addEventListener('DOMContentLoaded', () => {
    const style = document.createElement('style');
    style.innerHTML = `
      *, *::before, *::after {
        animation-duration: 0s !important;
        transition-duration: 0s !important;
      }
    `;
    document.head.appendChild(style);
  });
});

Playwright vs Other Frameworks

Q: What are the key differences between Playwright, Selenium, and Cypress?

Answer:

For a detailed comparison with Selenium, see our Selenium interview questions.

FeaturePlaywrightSeleniumCypress
ProtocolDevTools ProtocolWebDriver ProtocolCustom (runs in browser)
Auto-waitingBuilt-inManual waits neededBuilt-in
Browser supportChromium, Firefox, WebKitAll browsers including IEChromium, Firefox, Edge
Language supportJS/TS, Python, Java, .NETJava, Python, C#, Ruby, JSJavaScript/TypeScript only
ArchitectureOut-of-processOut-of-processIn-browser
API testingBuilt-inRequires RestAssured/otherRequires cy.request
Visual testingBuilt-inRequires pluginsRequires plugins
Network interceptionBuilt-inLimitedBuilt-in
Multi-tab supportNativeSupportedLimited
Parallel executionBuilt-inRequires GridPaid feature
Mobile testingEmulationReal devices (Appium)Emulation only
CI/CD integrationExcellentGoodExcellent
Learning curveModerateSteepEasy
Community sizeGrowingLargestLarge
Release year202020042017

When to use Playwright:

  • Modern web applications with SPAs
  • Need cross-browser testing (including WebKit)
  • API testing alongside UI testing
  • Visual regression testing
  • Multiple programming languages
  • Fast, reliable test execution

When to use Selenium:

  • Legacy browser support (IE)
  • Existing Selenium infrastructure
  • Mobile testing with Appium
  • Largest community and resources

When to use Cypress:

  • JavaScript-only teams
  • Developer-focused testing
  • Time-travel debugging preference
  • Excellent documentation and DX

For more on modern automation frameworks, see our Cypress complete guide.

Q: Why choose Playwright over Selenium?

Answer:

Advantages of Playwright:

1. Auto-waiting eliminates flakiness:

// Playwright: Auto-waits
await page.click('button');
 
// Selenium: Manual waits
WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.elementToBeClickable(By.id("button"))).click();

2. Better debugging tools:

  • Trace Viewer with timeline
  • Built-in video recording
  • Screenshot comparison
  • Playwright Inspector

3. Faster execution:

  • Browser contexts vs full browsers
  • Parallel execution out-of-the-box
  • No WebDriver overhead

4. Modern features:

  • Network interception
  • API testing
  • Visual comparisons
  • Component testing

5. Single API for all browsers:

// Same code works across Chromium, Firefox, WebKit
for (const browserType of ['chromium', 'firefox', 'webkit']) {
  const browser = await playwright[browserType].launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
}

6. Better mobile emulation:

const iPhone = devices['iPhone 13'];
const context = await browser.newContext({
  ...iPhone,
});

When Selenium is still better:

  • IE or older browser support needed
  • Existing Selenium Grid infrastructure
  • Real mobile device testing (with Appium)
  • Team has deep Selenium expertise

Quiz on Playwright Interview

Your Score: 0/10

Question: What protocol does Playwright use to communicate with browsers?

Continue Reading

Frequently Asked Questions (FAQs) / People Also Ask (PAA)

What experience level do I need to get a Playwright testing job?

How should I prepare for a Playwright interview as a beginner?

What salary can I expect for a Playwright automation engineer role?

Should I learn Playwright or Selenium for better job prospects?

What are the most common mistakes candidates make in Playwright interviews?

How important is TypeScript knowledge for Playwright roles?

What portfolio projects should I build to demonstrate Playwright skills?

How do I transition from manual testing to Playwright automation?