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.
- 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
- 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
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.
Playwright Test Runner
ā
Test Fixtures (page, context, browser)
ā
Playwright Library
ā
Browser Server (CDP/WebSocket)
ā
Actual Browser (Chromium/Firefox/WebKit)| Component | Description |
|---|---|
| Browser | Represents a browser instance (Chromium, Firefox, WebKit) |
| BrowserContext | Isolated incognito-like session with own cookies/cache |
| Page | Single tab/page within a context |
| Frame | iframe within a page |
| Locator | Represents element query (auto-waits, auto-retries) |
| Fixtures | Built-in test setup/teardown (page, context, browser) |
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.
- 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
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.
| Action | Automatic 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 |
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
});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.
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?
- Role-based:
role=button[name="Submit"]- Accessibility-friendly - Test ID:
[data-testid="submit-btn"]- Explicit test hooks - User-visible text:
text=Sign In- User-centric - CSS Selector:
#id,.class- Fast and readable - XPath: Last resort for complex scenarios
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?
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?
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?
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.
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 });
});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
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.
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.
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.
// 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 });
});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']
}
]
});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"]');
});- 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?
// 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 });
});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/
}
]
});// 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.
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
});// 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';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?
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
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.
// 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 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
- 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?
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'
}
});// 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?
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- First run: Creates baseline screenshots
- Subsequent runs: Compare against baseline
- If different: Test fails with diff image
- Review diff in HTML report
- 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.
# 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
- 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
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?
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)
});- 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?
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?
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: 7image: 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_failureFROM 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?
// 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- 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 |
|---|---|---|
| Locators | Use role-based, test IDs, user-visible text | Use CSS/XPath based on structure |
| Waits | Use auto-waiting locators | Use page.waitForTimeout() |
| Assertions | Use web-first assertions (auto-retry) | Use generic expect() on locators |
| Page Objects | Use fixtures for dependency injection | Manually instantiate page objects |
| Authentication | Save and reuse auth state | Login in every test |
| Test Data | Use API to setup test data | Use UI to create test data |
| Isolation | Each test independent | Tests depend on each other |
Q27: Common Playwright patterns and 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
- 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 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.
- Explore Playwright Keywords & Concepts guide
- Build a complete framework with TypeScript
- Practice API testing with Playwright
- Master Trace Viewer and debugging tools