interview prep•45 min read•Updated Jan 25, 2026

Playwright Interview Preparation

Comprehensive interview preparation guide covering 40+ Playwright questions, from basics to advanced framework design patterns.

Tool:PlaywrightLevel:intermediateDomain:QA Engineering

Introduction

Playwright is Microsoft's modern, fast, and reliable end-to-end testing framework. This comprehensive guide covers 40+ interview questions across all difficulty levels, from fundamental concepts to advanced testing patterns with TypeScript.

šŸ’” What You'll Learn:
  • Playwright architecture and key differences from Selenium
  • Auto-waiting mechanism and why it's superior
  • Browser contexts, pages, and isolation
  • Modern locator strategies and best practices
  • API testing with Playwright
  • Advanced features: network interception, authentication, fixtures
šŸŽÆ Why Playwright is the Future:
  • Auto-wait: No more explicit waits or sleep statements
  • Browser contexts: Fast, isolated test execution
  • Modern APIs: Clean, intuitive TypeScript/JavaScript APIs
  • Built-in features: Screenshots, videos, network interception
  • Cross-browser: Chromium, Firefox, WebKit out of the box
  • Developer-friendly: Amazing debugging tools and codegen

1. Playwright Fundamentals

Q1: What is Playwright and how does it differ from Selenium?

Playwright is a modern end-to-end testing framework created by Microsoft. It's built by the same team that created Puppeteer at Google.

Playwright Advantages

  • Auto-wait: Automatic waiting for elements
  • Browser contexts: Fast parallel execution
  • Network control: Built-in request/response interception
  • Modern APIs: async/await, Promise-based
  • Single API: Same code for all browsers
  • Speed: 2-3x faster than Selenium

Selenium Characteristics

  • Manual waits: Need explicit/implicit waits
  • Browser instances: Heavier resource usage
  • External tools: Need additional libraries
  • Synchronous: Traditional blocking calls
  • Different drivers: Browser-specific code
  • Mature: Longer history, larger community
TypeScript - Basic Playwright Test
import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {
  // Navigate to URL
  await page.goto('https://www.example.com');
  
  // Auto-wait and click
  await page.click('text=Get Started');
  
  // Auto-wait for element and assert
  await expect(page.locator('h1')).toHaveText('Welcome');
  
  // No explicit waits needed!
});

Q2: Explain Playwright's architecture and key components.

Architecture Overview
Playwright Test Runner
        ↓
Test Fixtures (page, context, browser)
        ↓
Playwright Library
        ↓
Browser Server (CDP/WebSocket)
        ↓
Actual Browser (Chromium/Firefox/WebKit)
ComponentDescription
BrowserRepresents a browser instance (Chromium, Firefox, WebKit)
BrowserContextIsolated incognito-like session with own cookies/cache
PageSingle tab/page within a context
Frameiframe within a page
LocatorRepresents element query (auto-waits, auto-retries)
FixturesBuilt-in test setup/teardown (page, context, browser)
TypeScript - Architecture Example
import { test, chromium } from '@playwright/test';

test('architecture demo', async () => {
  // 1. Launch Browser
  const browser = await chromium.launch();
  
  // 2. Create isolated BrowserContext
  const context = await browser.newContext();
  
  // 3. Create Page in context
  const page = await context.newPage();
  
  // 4. Navigate and interact
  await page.goto('https://example.com');
  
  // 5. Cleanup (automatic with fixtures)
  await context.close();
  await browser.close();
});

// Better: Use fixtures (auto-cleanup)
test('with fixtures', async ({ page }) => {
  // page fixture provides isolated context automatically
  await page.goto('https://example.com');
  // Auto-cleanup after test
});

Q3: What is BrowserContext and why is it important?

BrowserContext is an isolated incognito-like session within a browser. Each context has its own cookies, localStorage, and cache.

šŸ’” Key Benefits:
  • Isolation: Tests don't interfere with each other
  • Speed: Faster than launching new browsers
  • Parallel: Multiple contexts run simultaneously
  • Authentication: Save/reuse authentication state
  • Resource-efficient: Share browser instance
TypeScript - BrowserContext Usage
import { test, chromium } from '@playwright/test';

// Multiple isolated contexts in same browser
test('multiple contexts', async () => {
  const browser = await chromium.launch();
  
  // Context 1: Regular user
  const context1 = await browser.newContext();
  const page1 = await context1.newPage();
  await page1.goto('https://example.com');
  // Login as user1, set cookies
  
  // Context 2: Admin user (completely isolated)
  const context2 = await browser.newContext();
  const page2 = await context2.newPage();
  await page2.goto('https://example.com');
  // Login as admin, different cookies
  
  // Both sessions are independent!
  
  await browser.close();
});

// Context with custom settings
test('custom context', async () => {
  const browser = await chromium.launch();
  
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    locale: 'en-US',
    timezoneId: 'America/New_York',
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
    permissions: ['geolocation'],
    userAgent: 'Custom User Agent'
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
  
  await browser.close();
});

Q4: How does auto-waiting work in Playwright?

Playwright performs a range of actionability checks before every action. This eliminates the need for explicit waits.

ActionAutomatic Checks
click()Visible, Enabled, Stable, Receives Events, Not Covered
fill()Visible, Enabled, Editable
check()Visible, Enabled, Not Already Checked
selectOption()Visible, Enabled
hover()Visible, Stable
TypeScript - Auto-waiting Examples
import { test } from '@playwright/test';

test('auto-waiting demo', async ({ page }) => {
  await page.goto('https://example.com');
  
  // āœ… Playwright automatically waits for:
  // - Element to be visible
  // - Element to be enabled
  // - Element to be stable (not animating)
  // - Element to receive events (not covered by other elements)
  await page.click('button#submit');
  
  // āœ… Waits for input to be visible and editable
  await page.fill('input[name="email"]', 'user@example.com');
  
  // āœ… Waits for dropdown to be visible and enabled
  await page.selectOption('select#country', 'USA');
  
  // āŒ Selenium equivalent (manual waits needed):
  // WebDriverWait wait = new WebDriverWait(driver, 10);
  // WebElement element = wait.until(
  //   ExpectedConditions.elementToBeClickable(By.id("submit"))
  // );
  // element.click();
});

// Custom timeout (default is 30 seconds)
test('custom timeout', async ({ page }) => {
  // Override default timeout
  await page.click('button', { timeout: 5000 });
  
  // Set page-level timeout
  page.setDefaultTimeout(60000); // 60 seconds
});
šŸ’” Auto-Retry Mechanism:

Playwright retries actions until they pass or timeout. For example, if an element is covered by a loading spinner, Playwright keeps retrying until the spinner disappears.

2. Locators & Element Selection

Q5: What are Playwright locators and how are they different from Selenium?

Playwright Locators are auto-retrying, auto-waiting element queries. They don't find elements immediately but represent a query that executes when needed.

TypeScript - Locator Strategies
import { test } from '@playwright/test';

test('locator strategies', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 1. Text locator (recommended for user-facing text)
  await page.locator('text=Sign In').click();
  await page.locator('text="Exact Match"').click();
  
  // 2. CSS Selector
  await page.locator('#username').fill('admin');
  await page.locator('.btn-primary').click();
  await page.locator('input[type="email"]').fill('user@example.com');
  
  // 3. XPath (use sparingly)
  await page.locator('xpath=//button[@type="submit"]').click();
  
  // 4. Role-based (accessibility-friendly, recommended)
  await page.locator('role=button[name="Submit"]').click();
  await page.locator('role=textbox[name="Email"]').fill('test@example.com');
  await page.locator('role=link[name="Home"]').click();
  
  // 5. Test ID (best for testing)
  await page.locator('[data-testid="login-button"]').click();
  
  // 6. Chaining locators
  await page.locator('.form-group')
             .locator('input[name="username"]')
             .fill('admin');
  
  // 7. Filtering
  await page.locator('button')
             .filter({ hasText: 'Submit' })
             .click();
});

Playwright Locators

  • Lazy evaluation (not found immediately)
  • Auto-retry and auto-wait
  • Strict mode (fails if multiple matches)
  • Built-in assertions

Selenium Elements

  • Eager evaluation (found immediately)
  • Manual waits required
  • Returns first match silently
  • Need separate assertion library

Q6: What are the best practices for writing resilient locators?

šŸ’” Locator Priority (Best to Worst):
  1. Role-based: role=button[name="Submit"] - Accessibility-friendly
  2. Test ID: [data-testid="submit-btn"] - Explicit test hooks
  3. User-visible text: text=Sign In - User-centric
  4. CSS Selector: #id, .class - Fast and readable
  5. XPath: Last resort for complex scenarios
TypeScript - Recommended Patterns
import { test, expect } from '@playwright/test';

test('resilient locators', async ({ page }) => {
  await page.goto('https://example.com');
  
  // āœ… GOOD: Role-based (accessible, semantic)
  await page.getByRole('button', { name: 'Submit' }).click();
  await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
  await page.getByRole('link', { name: 'Sign Up' }).click();
  
  // āœ… GOOD: Test ID (explicit, stable)
  await page.getByTestId('login-form').isVisible();
  await page.getByTestId('submit-button').click();
  
  // āœ… GOOD: User-visible text
  await page.getByText('Welcome Back').isVisible();
  await page.getByLabel('Username').fill('admin');
  
  // āœ… GOOD: Placeholder text
  await page.getByPlaceholder('Enter email').fill('user@example.com');
  
  // āœ… GOOD: Alt text for images
  await page.getByAltText('Company Logo').isVisible();
  
  // āœ… GOOD: Title attribute
  await page.getByTitle('Close').click();
  
  // āš ļø OKAY: CSS with stable attributes
  await page.locator('[data-test="username"]').fill('admin');
  
  // āŒ AVOID: Fragile CSS (depends on structure)
  await page.locator('.form > div:nth-child(2) > input').fill('test');
  
  // āŒ AVOID: Absolute XPath
  await page.locator('xpath=/html/body/div[1]/form/input').fill('test');
});

// Combining locators for precision
test('combining locators', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Find button within specific section
  await page.locator('section.login')
             .getByRole('button', { name: 'Submit' })
             .click();
  
  // Filter by text
  await page.getByRole('button')
             .filter({ hasText: 'Submit' })
             .click();
  
  // Filter by not having text
  await page.locator('button')
             .filter({ hasNotText: 'Cancel' })
             .first()
             .click();
}

Q7: How do you handle dynamic elements in Playwright?

TypeScript - Dynamic Elements
import { test, expect } from '@playwright/test';

test('dynamic elements', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 1. Wait for element to appear
  await page.waitForSelector('.dynamic-content', { state: 'visible' });
  
  // 2. Wait for loading to disappear
  await page.waitForSelector('.loading-spinner', { state: 'hidden' });
  
  // 3. Wait for network to be idle
  await page.waitForLoadState('networkidle');
  
  // 4. Wait for specific text
  await page.waitForSelector('text=Data loaded successfully');
  
  // 5. Custom wait condition
  await page.waitForFunction(() => {
    return document.querySelectorAll('.item').length > 10;
  });
  
  // 6. Wait for URL change
  await page.waitForURL('**/dashboard');
  
  // 7. Handle element that may or may not exist
  const errorMessage = await page.locator('.error-message').count();
  if (errorMessage > 0) {
    console.log(await page.locator('.error-message').textContent());
  }
  
  // 8. Wait for element to be stable (not animating)
  await page.locator('.animated-element').click({ force: false });
  // Playwright automatically waits for animations to finish
});

// Advanced: Polling for condition
test('polling example', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Wait up to 30 seconds for condition
  await expect(async () => {
    const status = await page.locator('#status').textContent();
    expect(status).toBe('Complete');
  }).toPass({ timeout: 30000, intervals: [1000] });
});

3. Actions & User Interactions

Q8: What are the common user interaction methods in Playwright?

TypeScript - User Actions
import { test } from '@playwright/test';

test('user interactions', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Click
  await page.click('button#submit');
  await page.locator('text=Login').click();
  
  // Double click
  await page.dblclick('button#edit');
  
  // Right click
  await page.click('button#menu', { button: 'right' });
  
  // Fill input (clears first, then types)
  await page.fill('input[name="email"]', 'user@example.com');
  
  // Type (doesn't clear, just types)
  await page.locator('input#search').type('playwright');
  
  // Press keyboard keys
  await page.press('input#search', 'Enter');
  await page.keyboard.press('Control+A');
  
  // Check/uncheck checkbox
  await page.check('input[type="checkbox"]');
  await page.uncheck('input[type="checkbox"]');
  
  // Select dropdown
  await page.selectOption('select#country', 'USA');
  await page.selectOption('select#country', { label: 'United States' });
  await page.selectOption('select#country', { value: 'us' });
  
  // Hover
  await page.hover('button#menu');
  
  // Focus
  await page.focus('input#username');
  
  // Drag and drop
  await page.dragAndDrop('#source', '#target');
  
  // Upload file
  await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
  
  // Multiple files
  await page.setInputFiles('input[type="file"]', [
    'file1.pdf',
    'file2.png'
  ]);
});

Q9: How do you handle frames and iframes?

TypeScript - Frame Handling
import { test } from '@playwright/test';

test('handling frames', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Method 1: By name or URL
  const frame = page.frame({ name: 'iframe-name' });
  await frame?.click('button#submit');
  
  // Method 2: By URL pattern
  const frame2 = page.frame({ url: /.*login.*/ });
  await frame2?.fill('input#username', 'admin');
  
  // Method 3: Using frameLocator (recommended)
  const frameLocator = page.frameLocator('iframe#payment-frame');
  await frameLocator.locator('input#card-number').fill('4111111111111111');
  await frameLocator.locator('button#pay').click();
  
  // Nested frames
  const parentFrame = page.frameLocator('iframe#parent');
  const childFrame = parentFrame.frameLocator('iframe#child');
  await childFrame.locator('button').click();
  
  // Check if frame exists
  const frames = page.frames();
  console.log(`Total frames: ${frames.length}`);
  
  // No need to switch back to main content (unlike Selenium)
  await page.click('button#main-page-button');
});

4. Assertions & Expectations

Q10: How do assertions work in Playwright?

Playwright has built-in web-first assertions that auto-retry until the condition is met or timeout.

TypeScript - Assertions
import { test, expect } from '@playwright/test';

test('assertions', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Element visibility
  await expect(page.locator('h1')).toBeVisible();
  await expect(page.locator('.loading')).toBeHidden();
  
  // Text content
  await expect(page.locator('h1')).toHaveText('Welcome');
  await expect(page.locator('h1')).toContainText('Wel');
  
  // Count
  await expect(page.locator('.item')).toHaveCount(5);
  
  // Attribute
  await expect(page.locator('button')).toHaveAttribute('disabled', '');
  await expect(page.locator('input')).toHaveAttribute('type', 'email');
  
  // CSS class
  await expect(page.locator('button')).toHaveClass('btn-primary');
  await expect(page.locator('button')).toHaveClass(/btn-/);
  
  // Value (for inputs)
  await expect(page.locator('input#email')).toHaveValue('user@example.com');
  
  // URL
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page).toHaveURL(/.*dashboard.*/);
  
  // Title
  await expect(page).toHaveTitle('Dashboard - MyApp');
  await expect(page).toHaveTitle(/Dashboard/);
  
  // Enabled/Disabled
  await expect(page.locator('button')).toBeEnabled();
  await expect(page.locator('button')).toBeDisabled();
  
  // Checked (checkbox/radio)
  await expect(page.locator('input[type="checkbox"]')).toBeChecked();
  await expect(page.locator('input[type="checkbox"]')).not.toBeChecked();
  
  // Editable
  await expect(page.locator('input')).toBeEditable();
  
  // Empty
  await expect(page.locator('input')).toBeEmpty();
  
  // Focus
  await expect(page.locator('input')).toBeFocused();
  
  // Screenshot comparison
  await expect(page).toHaveScreenshot('homepage.png');
  await expect(page.locator('.header')).toHaveScreenshot();
});

// Soft assertions (continues even if fails)
test('soft assertions', async ({ page }) => {
  await page.goto('https://example.com');
  
  await expect.soft(page.locator('h1')).toHaveText('Wrong Text');
  await expect.soft(page.locator('h2')).toBeVisible();
  
  // Test continues, reports all failures at end
});

// Custom timeout
test('custom timeout', async ({ page }) => {
  await page.goto('https://example.com');
  
  await expect(page.locator('.slow-element'))
    .toBeVisible({ timeout: 60000 });
});
šŸ’” Auto-Retry Assertions:

Unlike traditional assertions, Playwright assertions automatically retry until they pass or timeout. This eliminates flaky tests caused by timing issues.

Q11: What's the difference between web-first assertions and generic assertions?

Web-First Assertions

  • Auto-retry: Keep trying until pass/timeout
  • Use for: UI elements, page state
  • Example: expect(locator).toBeVisible()
  • Recommended for most cases

Generic Assertions

  • Immediate: Check once, no retry
  • Use for: Variables, API responses
  • Example: expect(value).toBe(5)
  • Standard Jest/Jasmine assertions
TypeScript - Comparison
import { test, expect } from '@playwright/test';

test('assertion types', async ({ page }) => {
  await page.goto('https://example.com');
  
  // āœ… Web-first (auto-retry)
  await expect(page.locator('h1')).toHaveText('Welcome');
  // Retries for 30s (default timeout)
  
  // āœ… Generic (immediate)
  const count = await page.locator('.item').count();
  expect(count).toBe(5);
  // Checks immediately, no retry
  
  // āŒ Wrong: Using generic assertion on locator
  const text = await page.locator('h1').textContent();
  expect(text).toBe('Welcome');
  // Not recommended: Doesn't retry, can be flaky
  
  // āœ… Correct: Use web-first assertion
  await expect(page.locator('h1')).toHaveText('Welcome');
});

5. Network Interception & API Testing

Q12: How do you intercept network requests in Playwright?

Playwright provides powerful network interception for monitoring, modifying, or mocking HTTP requests and responses.

TypeScript - Network Interception
import { test } from '@playwright/test';

test('network interception', async ({ page }) => {
  // Monitor all requests
  page.on('request', request => {
    console.log(`>> ${request.method()} ${request.url()}`);
  });
  
  // Monitor all responses
  page.on('response', response => {
    console.log(`<< ${response.status()} ${response.url()}`);
  });
  
  // Monitor specific API calls
  page.on('response', async response => {
    if (response.url().includes('/api/users')) {
      console.log(await response.json());
    }
  });
  
  await page.goto('https://example.com');
});

// Mock API responses
test('mock API', async ({ page }) => {
  // Intercept and mock
  await page.route('**/api/users', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' }
      ])
    });
  });
  
  await page.goto('https://example.com');
  // Page will receive mocked data instead of real API
});

// Modify requests
test('modify request', async ({ page }) => {
  await page.route('**/api/users', route => {
    const headers = {
      ...route.request().headers(),
      'Authorization': 'Bearer fake-token'
    };
    
    route.continue({ headers });
  });
  
  await page.goto('https://example.com');
});

// Abort requests (block resources)
test('block resources', async ({ page }) => {
  // Block images
  await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
  
  // Block analytics
  await page.route('**/analytics/**', route => route.abort());
  
  await page.goto('https://example.com');
  // Page loads faster without blocked resources
});

// Wait for specific API call
test('wait for API', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Wait for API response
  const response = await page.waitForResponse('**/api/users');
  const data = await response.json();
  console.log(data);
  
  // Wait for request
  await page.waitForRequest('**/api/login');
});

Q13: How do you perform API testing with Playwright?

Playwright has built-in APIRequestContext for API testing without launching a browser.

TypeScript - API Testing
import { test, expect } from '@playwright/test';

test('API testing', async ({ request }) => {
  // GET request
  const response = await request.get('https://api.example.com/users');
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);
  
  const users = await response.json();
  expect(users).toHaveLength(10);
  
  // POST request
  const createResponse = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com'
    }
  });
  expect(createResponse.status()).toBe(201);
  
  const newUser = await createResponse.json();
  expect(newUser.name).toBe('John Doe');
  
  // PUT request
  await request.put(`https://api.example.com/users/${newUser.id}`, {
    data: {
      name: 'John Updated'
    }
  });
  
  // DELETE request
  const deleteResponse = await request.delete(
    `https://api.example.com/users/${newUser.id}`
  );
  expect(deleteResponse.status()).toBe(204);
});

// API testing with authentication
test('authenticated API', async ({ request }) => {
  // Create context with auth
  const context = await request.newContext({
    baseURL: 'https://api.example.com',
    extraHTTPHeaders: {
      'Authorization': 'Bearer token123',
      'Content-Type': 'application/json'
    }
  });
  
  const response = await context.get('/protected/data');
  expect(response.ok()).toBeTruthy();
});

// Combine UI and API testing
test('hybrid test', async ({ page, request }) => {
  // Setup: Create data via API
  const user = await request.post('/api/users', {
    data: { name: 'Test User', email: 'test@example.com' }
  });
  const userId = (await user.json()).id;
  
  // Test: Verify in UI
  await page.goto(`/users/${userId}`);
  await expect(page.locator('h1')).toHaveText('Test User');
  
  // Cleanup: Delete via API
  await request.delete(`/api/users/${userId}`);
});

6. Authentication & State Management

Q14: How do you handle authentication in Playwright?

Playwright allows you to save and reuse authentication state across tests, eliminating the need to login before every test.

TypeScript - Save Auth State
// setup/auth.setup.ts
import { test as setup } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  // Perform login
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  // Wait for login to complete
  await page.waitForURL('**/dashboard');
  
  // Save signed-in state
  await page.context().storageState({ path: authFile });
});
TypeScript - playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    // Setup project
    {
      name: 'setup',
      testMatch: /.*.setup.ts/
    },
    // Test projects that use authentication
    {
      name: 'chromium',
      use: {
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup']
    }
  ]
});
TypeScript - Use Auth in Tests
import { test } from '@playwright/test';

// This test is already logged in!
test('user dashboard', async ({ page }) => {
  await page.goto('/dashboard');
  // Already authenticated, no login needed
  await expect(page.locator('.welcome-message')).toBeVisible();
});

test('profile settings', async ({ page }) => {
  await page.goto('/settings');
  // Still authenticated
  await page.fill('input[name="name"]', 'Updated Name');
  await page.click('button[type="submit"]');
});
šŸ’” Benefits:
  • Speed: Login once, reuse in all tests
  • Reliability: No repeated login operations
  • Isolation: Each test still gets fresh context
  • Multiple users: Save different auth states

Q15: How do you test with multiple user roles?

TypeScript - Multiple Auth States
// setup/auth.setup.ts
import { test as setup } from '@playwright/test';

const adminAuthFile = 'playwright/.auth/admin.json';
const userAuthFile = 'playwright/.auth/user.json';

setup('authenticate as admin', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', 'admin@example.com');
  await page.fill('input[name="password"]', 'admin123');
  await page.click('button[type="submit"]');
  await page.waitForURL('**/admin');
  await page.context().storageState({ path: adminAuthFile });
});

setup('authenticate as user', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'user123');
  await page.click('button[type="submit"]');
  await page.waitForURL('**/dashboard');
  await page.context().storageState({ path: userAuthFile });
});
TypeScript - playwright.config.ts
export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*.setup.ts/ },
    
    // Admin tests
    {
      name: 'admin',
      use: { storageState: 'playwright/.auth/admin.json' },
      dependencies: ['setup'],
      testMatch: /.*admin.*.spec.ts/
    },
    
    // User tests
    {
      name: 'user',
      use: { storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
      testMatch: /.*user.*.spec.ts/
    }
  ]
});
TypeScript - Role-Specific Tests
// tests/admin.spec.ts
test('admin can delete users', async ({ page }) => {
  await page.goto('/admin/users');
  await page.click('button.delete-user');
  // Admin-specific functionality
});

// tests/user.spec.ts
test('user cannot access admin panel', async ({ page }) => {
  await page.goto('/admin');
  await expect(page.locator('.error')).toContainText('Access Denied');
  // User-level verification
});

7. Fixtures & Test Organization

Q16: What are fixtures in Playwright?

Fixtures provide setup/teardown functionality and dependency injection for tests. They're Playwright's replacement for BeforeEach/AfterEach hooks.

TypeScript - Built-in Fixtures
import { test } from '@playwright/test';

// Built-in fixtures
test('using fixtures', async ({ page, context, browser }) => {
  // page - Isolated Page instance
  // context - BrowserContext (isolated session)
  // browser - Browser instance
  
  await page.goto('https://example.com');
  
  // Each test gets fresh fixtures
  // Auto-cleanup after test
});

// Available built-in fixtures:
test('all fixtures', async ({
  page,           // Page instance
  context,        // BrowserContext
  browser,        // Browser
  browserName,    // 'chromium' | 'firefox' | 'webkit'
  request,        // APIRequestContext for API testing
}) => {
  // Use fixtures
});
šŸ’” Custom Fixtures:
TypeScript - Custom Fixtures
// fixtures/test.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type MyFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: Page;
};

export const test = base.extend<MyFixtures>({
  // Page Object fixture
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },
  
  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },
  
  // Pre-authenticated page fixture
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('input[name="email"]', 'user@example.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
    
    await use(page);
    
    // Cleanup (if needed)
    await page.goto('/logout');
  }
});

export { expect } from '@playwright/test';
TypeScript - Using Custom Fixtures
import { test, expect } from './fixtures/test';

test('test with page objects', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password123');
  
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

test('test with authenticated page', async ({ authenticatedPage }) => {
  // Already logged in!
  await authenticatedPage.goto('/settings');
  await expect(authenticatedPage.locator('h1')).toHaveText('Settings');
});

Q17: How do you organize tests in Playwright?

Project Structure
playwright-tests/
ā”œā”€ā”€ tests/
│   ā”œā”€ā”€ auth/
│   │   ā”œā”€ā”€ login.spec.ts
│   │   └── signup.spec.ts
│   ā”œā”€ā”€ dashboard/
│   │   ā”œā”€ā”€ widgets.spec.ts
│   │   └── settings.spec.ts
│   └── checkout/
│       └── payment.spec.ts
│
ā”œā”€ā”€ pages/
│   ā”œā”€ā”€ LoginPage.ts
│   ā”œā”€ā”€ DashboardPage.ts
│   └── BasePage.ts
│
ā”œā”€ā”€ fixtures/
│   └── test.ts
│
ā”œā”€ā”€ utils/
│   ā”œā”€ā”€ helpers.ts
│   └── testData.ts
│
ā”œā”€ā”€ setup/
│   └── auth.setup.ts
│
ā”œā”€ā”€ playwright.config.ts
└── package.json
TypeScript - Test Organization
import { test, expect } from '@playwright/test';

// Use test.describe for grouping
test.describe('Login Feature', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });
  
  test('successful login', async ({ page }) => {
    // Test code
  });
  
  test('failed login', async ({ page }) => {
    // Test code
  });
});

// Nested describe blocks
test.describe('Dashboard', () => {
  test.describe('Widgets', () => {
    test('add widget', async ({ page }) => {
      // Test code
    });
  });
  
  test.describe('Settings', () => {
    test('update profile', async ({ page }) => {
      // Test code
    });
  });
});

// Tags for filtering
test('critical path @smoke', async ({ page }) => {
  // Run with: npx playwright test --grep @smoke
});

test('edge case @regression', async ({ page }) => {
  // Run with: npx playwright test --grep @regression
});

// Skip tests conditionally
test('webkit only', async ({ page, browserName }) => {
  test.skip(browserName !== 'webkit', 'This test is for Safari only');
  // Test code
});

// Fixme - known failing tests
test.fixme('known bug', async ({ page }) => {
  // Test is skipped until fixed
});

// Slow tests (3x timeout)
test('slow test', async ({ page }) => {
  test.slow();
  // Gets 3x timeout
});

8. Advanced Features

Q18: What is Playwright Trace Viewer and how do you use it?

Trace Viewer is Playwright's powerful debugging tool that records every action, screenshot, network request, and console log during test execution.

TypeScript - Enable Tracing
// playwright.config.ts
export default defineConfig({
  use: {
    // Capture trace on first retry
    trace: 'on-first-retry',
    // Other options: 'on', 'off', 'retain-on-failure'
  }
});

// Programmatic tracing
test('with manual tracing', async ({ page, context }) => {
  // Start tracing
  await context.tracing.start({
    screenshots: true,
    snapshots: true,
    sources: true
  });
  
  await page.goto('https://example.com');
  await page.click('button#submit');
  
  // Stop tracing and save
  await context.tracing.stop({
    path: 'trace.zip'
  });
});
šŸ’” View Trace:
Command Line
# View trace file
npx playwright show-trace trace.zip

# Trace viewer shows:
# - Timeline of all actions
# - Screenshots at each step
# - Network requests/responses
# - Console logs
# - Source code
# - DOM snapshots
šŸŽÆ Trace Features:
  • Time Travel: Click any action to see exact state
  • DOM Snapshot: Inspect HTML at any point
  • Network: See all requests/responses
  • Console: View all console.log output
  • Source: See which line of code executed

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

TypeScript - Screenshots
import { test } from '@playwright/test';

test('screenshots', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Full page screenshot
  await page.screenshot({ path: 'screenshot.png' });
  
  // Full page (including scrollable area)
  await page.screenshot({
    path: 'full-page.png',
    fullPage: true
  });
  
  // Element screenshot
  const element = page.locator('.header');
  await element.screenshot({ path: 'header.png' });
  
  // Screenshot with custom options
  await page.screenshot({
    path: 'custom.png',
    clip: { x: 0, y: 0, width: 800, height: 600 },
    quality: 80, // For JPEG
    type: 'jpeg'
  });
  
  // Screenshot to buffer
  const buffer = await page.screenshot();
  // Use buffer for custom processing
});

// Auto-screenshots on failure
// playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    // Options: 'off', 'on', 'only-on-failure'
  }
});
TypeScript - Videos
// playwright.config.ts
export default defineConfig({
  use: {
    video: 'retain-on-failure',
    // Options: 'off', 'on', 'retain-on-failure', 'on-first-retry'
    
    videoSize: { width: 1280, height: 720 }
  }
});

// Access video path in test
test('get video path', async ({ page }, testInfo) => {
  await page.goto('https://example.com');
  // Perform actions
  
  // Video saved automatically after test
});

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== 'passed') {
    const videoPath = await page.video()?.path();
    console.log(`Video saved: ${videoPath}`);
  }
});

Q20: How do you handle visual regression testing?

TypeScript - Visual Comparison
import { test, expect } from '@playwright/test';

test('visual regression', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Compare full page
  await expect(page).toHaveScreenshot('homepage.png');
  
  // Compare specific element
  const header = page.locator('.header');
  await expect(header).toHaveScreenshot('header.png');
  
  // With threshold (allow small differences)
  await expect(page).toHaveScreenshot('page.png', {
    maxDiffPixels: 100,
    threshold: 0.2
  });
  
  // Ignore regions that change
  await expect(page).toHaveScreenshot('page.png', {
    mask: [page.locator('.dynamic-ad')],
  });
  
  // First run creates baseline
  // Subsequent runs compare against baseline
  // Differences shown in HTML report
});

// Update baselines
// npx playwright test --update-snapshots
šŸ’” Visual Testing Workflow:
  1. First run: Creates baseline screenshots
  2. Subsequent runs: Compare against baseline
  3. If different: Test fails with diff image
  4. Review diff in HTML report
  5. Accept changes: Run with --update-snapshots

Q21: How do you use Playwright Codegen?

Codegen is Playwright's test generator that records your actions and generates test code.

Command Line
# Start codegen
npx playwright codegen https://example.com

# Codegen with specific browser
npx playwright codegen --browser=firefox https://example.com

# With device emulation
npx playwright codegen --device="iPhone 13" https://example.com

# With viewport size
npx playwright codegen --viewport-size=1280,720 https://example.com

# Save to file
npx playwright codegen --target=typescript -o tests/recorded.spec.ts https://example.com

# With authenticated state
npx playwright codegen --load-storage=auth.json https://example.com
šŸ’” Codegen Features:
  • Records clicks, typing, navigation
  • Generates locators automatically
  • Shows element picker (hover to see locators)
  • Copy locator or assertion from UI
  • Supports TypeScript, JavaScript, Python, C#, Java
āš ļø Best Practice:

Use Codegen to learn locators and get started quickly, but always refactor generated code into maintainable Page Objects and proper test structure.

Q22: How do you handle mobile emulation in Playwright?

TypeScript - Mobile Testing
import { test, devices } from '@playwright/test';

// Use predefined device
test.use(devices['iPhone 13']);

test('mobile test', async ({ page }) => {
  await page.goto('https://example.com');
  // Page behaves like iPhone 13
});

// Multiple devices in config
// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] }
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 13'] }
    },
    {
      name: 'Desktop Chrome',
      use: { ...devices['Desktop Chrome'] }
    }
  ]
});

// Custom device configuration
test('custom device', async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 375, height: 667 },
    userAgent: 'Custom Mobile User Agent',
    deviceScaleFactor: 2,
    isMobile: true,
    hasTouch: true,
    locale: 'en-US',
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
    permissions: ['geolocation']
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
});

// Test mobile-specific features
test('mobile gestures', async ({ page }) => {
  test.use(devices['iPhone 13']);
  
  await page.goto('https://example.com');
  
  // Tap (mobile click)
  await page.locator('button').tap();
  
  // Swipe
  await page.locator('.carousel').swipe('left');
  
  // Pinch zoom (not directly supported, use touch events)
});
šŸ’” Available Device Presets:
  • iPhone 13, iPhone 12, iPhone SE
  • iPad, iPad Mini, iPad Pro
  • Pixel 5, Galaxy S9+, Galaxy Tab S4
  • Desktop Chrome, Firefox, Safari

9. Configuration & CI/CD Integration

Q23: What are the key configuration options in playwright.config.ts?

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

export default defineConfig({
  // Test directory
  testDir: './tests',
  
  // Test match pattern
  testMatch: '**/*.spec.ts',
  
  // Test ignore patterns
  testIgnore: '**/build/**',
  
  // Timeout settings
  timeout: 30000, // Per test
  expect: {
    timeout: 5000 // Per assertion
  },
  
  // Global setup/teardown
  globalSetup: './global-setup.ts',
  globalTeardown: './global-teardown.ts',
  
  // Parallel execution
  fullyParallel: true,
  workers: process.env.CI ? 1 : undefined, // Auto in local, 1 in CI
  
  // Retry settings
  retries: process.env.CI ? 2 : 0,
  
  // Reporter
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'results.xml' }],
    ['list'] // Console output
  ],
  
  // Use settings (applied to all projects)
  use: {
    // Base URL
    baseURL: 'https://example.com',
    
    // Browser settings
    headless: true,
    viewport: { width: 1280, height: 720 },
    
    // Screenshots and videos
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
    
    // Navigation timeout
    navigationTimeout: 30000,
    actionTimeout: 10000,
    
    // Locale and timezone
    locale: 'en-US',
    timezoneId: 'America/New_York',
    
    // User agent
    userAgent: 'Custom User Agent',
    
    // Ignore HTTPS errors
    ignoreHTTPSErrors: true,
  },
  
  // Projects for different browsers
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    },
    {
      name: 'mobile',
      use: { ...devices['iPhone 13'] }
    }
  ],
  
  // Web server (start dev server before tests)
  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
    timeout: 120000
  }
});

Q24: How do you integrate Playwright with CI/CD?

GitHub Actions - .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]
  schedule:
    - cron: '0 0 * * *' # Daily at midnight

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - uses: actions/setup-node@v3
      with:
        node-version: 18
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    
    - name: Run Playwright tests
      run: npx playwright test
    
    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v3
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30
    
    - name: Upload test traces
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: traces
        path: test-results/
        retention-days: 7
GitLab CI - .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.40.0-focal

stages:
  - test

playwright-tests:
  stage: test
  script:
    - npm ci
    - npx playwright install
    - npx playwright test
  artifacts:
    when: always
    paths:
      - playwright-report/
      - test-results/
    expire_in: 1 week
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
Docker - Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]

Q25: How do you run tests in parallel in Playwright?

TypeScript - Parallel Execution
// playwright.config.ts
export default defineConfig({
  // Run all tests in parallel
  fullyParallel: true,
  
  // Number of workers (defaults to CPU cores / 2)
  workers: 4,
  
  // Or based on environment
  workers: process.env.CI ? 2 : undefined,
});

// Disable parallel for specific test file
test.describe.configure({ mode: 'serial' });

test('runs first', async ({ page }) => {
  // This runs first
});

test('runs second', async ({ page }) => {
  // This runs after first test
});

// Run specific number of workers
// npx playwright test --workers=4

// Run in single worker (no parallel)
// npx playwright test --workers=1

// Shard tests across machines
// npx playwright test --shard=1/3  // Machine 1
// npx playwright test --shard=2/3  // Machine 2
// npx playwright test --shard=3/3  // Machine 3
šŸ’” Parallel Execution Benefits:
  • Speed: Tests run 4-8x faster with parallelization
  • Isolation: Each test gets fresh browser context
  • Efficiency: Utilize all CPU cores
  • Sharding: Distribute across multiple machines

10. Best Practices & Common Patterns

Q26: What are Playwright testing best practices?

Categoryāœ… Best PracticeāŒ Anti-pattern
LocatorsUse role-based, test IDs, user-visible textUse CSS/XPath based on structure
WaitsUse auto-waiting locatorsUse page.waitForTimeout()
AssertionsUse web-first assertions (auto-retry)Use generic expect() on locators
Page ObjectsUse fixtures for dependency injectionManually instantiate page objects
AuthenticationSave and reuse auth stateLogin in every test
Test DataUse API to setup test dataUse UI to create test data
IsolationEach test independentTests depend on each other

Q27: Common Playwright patterns and examples

TypeScript - Best Practices Examples
import { test, expect } from '@playwright/test';

// āœ… GOOD: Independent test with setup
test('user can checkout', async ({ page, request }) => {
  // Setup via API (fast)
  const product = await request.post('/api/products', {
    data: { name: 'Test Product', price: 99 }
  });
  
  // Test via UI
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Add to Cart' }).click();
  await expect(page.getByText('Item added')).toBeVisible();
  
  // Cleanup via API (fast)
  await request.delete(`/api/products/${product.id}`);
});

// āœ… GOOD: Web-first assertions (auto-retry)
test('dynamic content', async ({ page }) => {
  await page.goto('/dashboard');
  
  // Waits automatically until text appears
  await expect(page.getByRole('heading')).toHaveText('Dashboard');
  
  // Waits until count is exactly 5
  await expect(page.locator('.item')).toHaveCount(5);
});

// āŒ BAD: Manual waits
test('bad waits', async ({ page }) => {
  await page.goto('/dashboard');
  
  await page.waitForTimeout(5000); // āŒ Never do this
  
  const text = await page.locator('h1').textContent();
  expect(text).toBe('Dashboard'); // āŒ No auto-retry
});

// āœ… GOOD: Role-based locators
test('accessible locators', async ({ page }) => {
  await page.goto('/login');
  
  await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
  await page.getByRole('textbox', { name: 'Password' }).fill('password');
  await page.getByRole('button', { name: 'Sign In' }).click();
});

// āœ… GOOD: Test IDs for testing-specific hooks
test('test IDs', async ({ page }) => {
  await page.goto('/dashboard');
  
  await page.getByTestId('user-profile').click();
  await page.getByTestId('settings-menu').click();
});

// āœ… GOOD: Page Object with fixtures
test('with page object', async ({ loginPage, dashboardPage }) => {
  await loginPage.login('user@example.com', 'password');
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

Key Interview Takeaways

šŸŽÆ Core Concepts to Master:
  • Auto-waiting: Understand how Playwright eliminates manual waits
  • Browser contexts: Know why they're faster than launching browsers
  • Locator strategies: Prioritize role-based and test IDs
  • Web-first assertions: Use built-in auto-retry assertions
  • Authentication: Save and reuse auth state
  • Fixtures: Understand dependency injection pattern
  • API testing: Combine UI and API testing effectively
šŸ’” Playwright vs Selenium - Key Differences:
  • Playwright has auto-waiting, Selenium needs explicit waits
  • Playwright uses browser contexts, Selenium uses browser instances
  • Playwright has built-in API testing, Selenium needs RestAssured
  • Playwright has modern async/await, Selenium is synchronous
  • Playwright has better debugging with Trace Viewer

šŸŽ‰ You're Ready for Playwright Interviews!

This guide covered 27 essential interview questions across fundamentals to advanced topics. Practice building real projects to solidify your understanding.

šŸ“š Continue Learning:
  • Explore Playwright Keywords & Concepts guide
  • Build a complete framework with TypeScript
  • Practice API testing with Playwright
  • Master Trace Viewer and debugging tools

Continue Learning