
Playwright Interview Questions: From Basics to Advanced Automation Concepts
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.
Table Of Contents-
- Playwright Fundamentals
- Architecture and Browser Communication
- Installation and Setup
- Locators and Selectors
- Auto-Waiting and Synchronization
- Assertions and Validations
- API Testing and Network Interception
- Visual Testing and Screenshots
- Fixtures and Test Organization
- Parallel Execution and Performance
- CI/CD Integration
- Advanced Scenarios
- Debugging and Troubleshooting
- Playwright vs Other Frameworks
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:
| Feature | Description |
|---|---|
| Multi-browser support | Chromium, Firefox, WebKit (Safari) with single API |
| Multi-language support | JavaScript/TypeScript, Python, Java, .NET, C# |
| Auto-waiting | Built-in waits for elements to be actionable |
| Browser contexts | Isolated test environments without full browser restarts |
| Network control | Request/response interception and mocking |
| Modern protocol | Uses 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 Engine | Browsers Covered | Notes |
|---|---|---|
| Chromium | Chrome, Edge, Opera | Latest Chromium builds |
| Firefox | Firefox | Latest Firefox builds |
| WebKit | Safari | Cross-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:
- Test script calls Playwright API methods
- Playwright library translates to DevTools Protocol commands
- Protocol communicates directly with browser internals
- Browser executes actions and returns results
- 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 contextQ: 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 playwrightWhat 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 installConfiguration 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=firefoxBenefits:
- 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):
| Locator | Syntax | Use Case |
|---|---|---|
| Role | page.getByRole('button', { name: 'Submit' }) | Accessibility-focused, most resilient |
| Label | page.getByLabel('Email address') | Form inputs with labels |
| Placeholder | page.getByPlaceholder('Enter email') | Inputs with placeholders |
| Text | page.getByText('Welcome back') | Text content |
| Test ID | page.getByTestId('submit-button') | Stable test identifiers |
| Alt text | page.getByAltText('Profile picture') | Images with alt attributes |
| Title | page.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:
| Method | Returns | Auto-waits | Strict Mode |
|---|---|---|---|
locator() | Locator | Yes | Yes (throws if multiple) |
$() | ElementHandle | No | No (returns first) |
$$() | ElementHandle[] | No | No |
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 indexBest practices:
- Prefer
getByRole()for resilience and accessibility - Use
getByTestId()for dynamic content that lacks stable attributes - Avoid
$()and$$()in favor oflocator()for auto-waiting - 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 with6. 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:
- Attached: Element is attached to the DOM
- Visible: Element is visible (not
display: noneorvisibility: hidden) - Stable: Element is not animating or moving
- Receives events: Element can receive pointer events (not covered by another element)
- 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 clicksComparison 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 waits4. 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) overwaitForTimeout - 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: 5When 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-snapshotsCross-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 @smokeParallel 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 3Limit failures to save resources:
export default defineConfig({
// Stop after N failures
maxFailures: process.env.CI ? 10 : undefined,
});npx playwright test --max-failures=5Q: 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: 30Parallel 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_requestsJenkins:
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:
geolocationnotificationscameramicrophoneclipboard-readclipboard-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=25Features:
- 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.zipFeatures:
- 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 test5. Headed mode:
# Run tests with visible browser
npx playwright test --headed
# Slow down execution
npx playwright test --headed --slow-mo=10006. 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, stable2. 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.
| Feature | Playwright | Selenium | Cypress |
|---|---|---|---|
| Protocol | DevTools Protocol | WebDriver Protocol | Custom (runs in browser) |
| Auto-waiting | Built-in | Manual waits needed | Built-in |
| Browser support | Chromium, Firefox, WebKit | All browsers including IE | Chromium, Firefox, Edge |
| Language support | JS/TS, Python, Java, .NET | Java, Python, C#, Ruby, JS | JavaScript/TypeScript only |
| Architecture | Out-of-process | Out-of-process | In-browser |
| API testing | Built-in | Requires RestAssured/other | Requires cy.request |
| Visual testing | Built-in | Requires plugins | Requires plugins |
| Network interception | Built-in | Limited | Built-in |
| Multi-tab support | Native | Supported | Limited |
| Parallel execution | Built-in | Requires Grid | Paid feature |
| Mobile testing | Emulation | Real devices (Appium) | Emulation only |
| CI/CD integration | Excellent | Good | Excellent |
| Learning curve | Moderate | Steep | Easy |
| Community size | Growing | Largest | Large |
| Release year | 2020 | 2004 | 2017 |
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?