๐ญ Playwright 8-Tier Complete Mastery Tutorial
From zero to hero: Fundamentals โ Core API โ Test Structure โ Advanced Interactions โ Debugging โ CI/CD โ Visual Testing โ Enterprise Mastery
What is Playwright?
Playwright is a Node.js library developed by Microsoft that automates Chromium, Firefox, and WebKit browsers. It provides a single, powerful API for cross-browser automation, testing, scraping, and interaction.
Why Playwright?
- Multi-browser: Chromium, Firefox, WebKit with one API
- Fast: Direct browser automation (not remote protocol) โ 2-3x faster than Selenium
- Auto-wait: Built-in waiting for elements โ no manual sleep() calls
- Network control: Mock APIs, intercept requests, record HAR files
- Visual testing: Screenshot comparison, baseline management, masking
- Debugging: Trace Viewer, UI Mode, Inspector, Codegen
- CI-native: Parallel execution, sharding, Docker support
- Multi-language: TypeScript, JavaScript, Python, C#, Java
Playwright vs Selenium vs Cypress
| Feature | Playwright | Selenium | Cypress |
|---|---|---|---|
| Browsers | Chromium, Firefox, WebKit | All | Chrome, Firefox, Edge |
| Languages | TS, JS, Python, C#, Java | Most | JS/TS only |
| Speed | โกโกโก Fastest | โก Moderate | โกโก Fast |
| Auto-wait | โ Built-in | โ Manual | โ Built-in |
| Network Mock | โ Full | โ ๏ธ Limited | โ cy.intercept() |
| Visual Testing | โ Native | โ No | โ No |
| Setup | npm init | Maven/Gradle | npm install |
Your First Playwright Test
import { test, expect } from '@playwright/test'; test('homepage loads', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle(/Example/); });
Interview Questions
What is Playwright?
A modern web automation library by Microsoft that automates Chromium, Firefox, and WebKit browsers with a single API. Used for testing, scraping, and browser automation.
Why is Playwright faster than Selenium?
Playwright uses direct browser automation (Chrome DevTools Protocol), not remote protocols. No network round-trips for each action = 2-3x faster.
Async/Await (Critical)
Playwright is asynchronous. Browser operations (navigate, click, wait) happen off the main thread. Use async/await to wait for them to complete.
Arrow Functions
Playwright uses arrow functions extensively for test callbacks and fixtures.
TypeScript (Recommended)
TypeScript provides IDE autocomplete, type checking, and catches errors before running tests. Highly recommended for Playwright projects.
// โ Correct: async/await test('login', async ({ page }) => { await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'user@test.com'); await page.click('.submit-btn'); }); // โ Wrong: missing await test('login', async ({ page }) => { page.goto('...'); // Doesn't wait! });
What is the DOM?
The DOM (Document Object Model) is the in-memory tree representation of an HTML page. Playwright interacts with the DOM to find elements, read text, and trigger events.
CSS Selectors
| Selector | Example | Matches |
|---|---|---|
.class | .btn-primary | Elements with class="btn-primary" |
#id | #submit-btn | Element with id="submit-btn" |
element | button | All <button> elements |
[attr="value"] | [type="submit"] | Elements with type="submit" |
parent > child | form > button | Direct button children of form |
<form> <label>Email</label> <input name="email" type="email" /> <button type="submit" class="btn-primary">Submit</button> </form>
Prerequisites
- Node.js 18+ (from nodejs.org)
- npm 8+ (comes with Node.js)
- Code editor (VSCode recommended)
Quick Start
Create Project
npm init playwright@latest scaffolds everything
Install Browsers
Automatic with setup script
Run Tests
npx playwright test
View Report
npx playwright show-report
# Quick start npm init playwright@latest # Run all tests npx playwright test # Run with browser visible npx playwright test --headed # Interactive UI Mode npx playwright test --ui # Debug mode npx playwright test --debug # View HTML report npx playwright show-report
Opening DevTools
- Chrome/Edge: F12 or Cmd+Opt+I (Mac)
- Firefox: F12 or Cmd+Opt+I (Mac)
- Safari: Cmd+Opt+I (enable in Preferences)
Key Tabs
document.querySelector() to verify selectors.// Test CSS selector document.querySelector('.submit-btn') // Returns element or null // Find all matches document.querySelectorAll('button') // Returns NodeList // Test by role (semantic) document.querySelector('[role="button"]')
Test Anatomy
test() function
Define test with name and async callback
Fixtures
Receive page, context, browser automatically
Actions
Navigate, click, fill, submit
Assertions
Verify expected outcomes
import { test, expect } from '@playwright/test'; test('login with valid credentials', async ({ page }) => { // Navigate await page.goto('https://example.com/login'); // Fill form await page.fill('input[name="email"]', 'user@test.com'); await page.fill('input[name="password"]', 'password123'); // Submit await page.click('.btn-login'); // Assert await expect(page).toHaveURL(/dashboard/); });
Run Your Test
# Run all tests npx playwright test # Run with browser visible npx playwright test --headed # Run specific test file npx playwright test tests/login.spec.ts
Recommended Locators (Tier 1)
| Locator | Use Case | Resilience |
|---|---|---|
getByRole() | Buttons, links, headings โ semantic HTML | โญโญโญ Best |
getByText() | Exact visible text match | โญโญ Good |
getByLabel() | Form labels associated with inputs | โญโญโญ Best |
getByPlaceholder() | Input placeholders | โญโญ Good |
getByAltText() | Images with alt text | โญโญโญ Best |
getByTestId() | data-testid attributes (testing API) | โญโญโญ Best |
// By semantic role (best โ tests accessibility) page.getByRole('button', { name: 'Submit' }); page.getByRole('link', { name: 'Home' }); page.getByRole('heading', { level: 1 }); // By form label page.getByLabel('Email Address'); page.getByLabel('Password'); // By visible text page.getByText('Welcome'); page.getByText(/Log.* in/i); // regex // By test ID (recommended for complex apps) page.getByTestId('submit-button');
CSS Selectors
CSS selectors are fast and readable. Use them when semantic locators don't work.
XPath
XPath is powerful for complex relationships but harder to read. Use for ancestor, preceding-sibling, complex conditions.
locator() Method
Universal locator that accepts CSS or XPath strings.
// CSS selectors page.locator('button.btn-primary'); page.locator('input[type="email"]'); page.locator('.form > button:first-child'); // XPath (for complex scenarios) page.locator('//button[@class="btn-primary"]'); page.locator('//button[text()="Submit"]');
Locator Methods
| Method | Purpose | Example |
|---|---|---|
.filter() | Narrow by condition | .filter({ hasText: 'Delete' }) |
.and() | Both conditions must match | .and(locator('[disabled]')) |
.or() | Either condition matches | .or(locator('.alt-selector')) |
.first() | Get first match | .first() |
.last() | Get last match | .last() |
.nth(n) | Get nth match (0-indexed) | .nth(2) |
.count() | Count matches | await .count() |
// Filter by text page.locator('button').filter({ hasText: 'Delete' }); // Combine conditions page.getByRole('button') .and(page.locator('[disabled]')); // Get specific match page.locator('button').nth(2); // Child element page.locator('.modal').locator('button').first();
Common Actions
| Action | Auto-waits for | Example |
|---|---|---|
.click() | Visibility & clickability | await btn.click() |
.fill() | Visibility & enabled | await input.fill('text') |
.type() | Visibility & enabled | await input.type('text') |
.check() | Visibility & clickable | await checkbox.check() |
.selectOption() | Visibility & enabled | await select.selectOption('USA') |
.hover() | Visibility | await element.hover() |
.dragTo() | Visibility & stable | await source.dragTo(target) |
.setInputFiles() | Visibility & enabled | await input.setInputFiles('file.pdf') |
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByLabel('Email').fill('user@example.com'); await page.locator('select').selectOption('USA'); await page.getByLabel('I Agree').check(); await page.getByLabel('Upload').setInputFiles('./document.pdf');
Web-First Assertions
| Assertion | Auto-waits for (5s) | Use when |
|---|---|---|
toBeVisible() | Element visibility | Checking element appears on screen |
toHaveText() | Text appears | Verifying text content |
toHaveValue() | Input value | Form field values change |
toHaveURL() | URL changes | Navigation completes |
toHaveTitle() | Page title loads | Page fully loads |
toBeChecked() | Checkbox state | Checkbox is checked/unchecked |
toBeEnabled() | Button enabled | Button is no longer disabled |
toHaveCount() | Element count | List size reaches expected value |
// Visibility (auto-waits up to 5s) await expect(page.locator('.success-msg')).toBeVisible(); // Text content await expect(page.getByRole('heading')).toHaveText('Welcome'); // URL change (navigation) await expect(page).toHaveURL(/dashboard/); // Soft assertion (doesn't fail test) await expect.soft(page.locator('.optional')).toBeVisible(); // Custom timeout await expect(element).toBeVisible({ timeout: 10000 });
The Problem with Manual Waits
Thread.sleep(2000)
Slow, brittle, wastes time. Always too short or too long.
WebDriverWait (Selenium)
Verbose boilerplate. Doesn't cover all edge cases.
Playwright Auto-wait
Built-in, instant, covers element + event readiness
How Auto-wait Works
When you call click(), Playwright:
- Waits for element visibility (not hidden by CSS, not off-screen)
- Waits for element to be enabled (not disabled)
- Waits for element to be stable (not covered by other elements)
- Only then performs the click
// โ Correct: Playwright auto-waits await page.getByRole('button').click(); // Internally waits for: visibility, enabled, stability // โ Wrong: Manual sleep (bad) await page.waitForTimeout(2000); // Fixed delay await page.click('button'); // Flaky, slow, unreliable // โ Assertions also auto-wait await expect(page).toHaveURL(/dashboard/); // Retries for 5 seconds if URL hasn't changed yet
Hook Types
| Hook | When | Use Case |
|---|---|---|
beforeEach() | Before each test | Navigate to page, login, setup data |
afterEach() | After each test (always) | Cleanup, logout, reset state |
beforeAll() | Once before all tests | Global setup, expensive operations |
afterAll() | Once after all tests | Global cleanup |
test.beforeEach(async ({ page }) => { // Setup: runs before each test await page.goto('https://example.com'); await page.fill('input[name="email"]', 'user@test.com'); }); test('dashboard loads', async ({ page }) => { // Test runs here (page already navigated) await expect(page.locator('.dashboard')).toBeVisible(); }); test.afterEach(async ({ page }) => { // Cleanup: runs after each test console.log('Test ended at: ' + page.url()); });
Why Steps?
- Organize complex tests into logical phases
- HTML report shows which step failed
- Trace Viewer highlights steps for debugging
- Improves readability of multi-phase flows
test('complete e-commerce flow', async ({ page }) => { await test.step('Open homepage', async () => { await page.goto('https://example.com'); }); await test.step('Search for product', async () => { await page.fill('.search', 'laptop'); await page.click('.search-btn'); }); await test.step('Add to cart', async () => { await page.click('.add-btn'); }); });
Built-in Fixtures
| Fixture | What it provides | Scope |
|---|---|---|
page | Browser tab/page for the test | Per test |
context | Isolated browser context (cookies, storage) | Per test |
browser | Browser instance | Per worker |
browserName | chromium, firefox, or webkit | Per test |
Custom Fixtures
Create fixtures for reusable setup (logged-in page, pre-populated form, API client, etc.)
import { test as base } from '@playwright/test'; const test = base.extend({ authenticatedPage: async ({ page }, use) => { // Setup: login before test await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'user@test.com'); await page.fill('input[name="password"]', 'pass'); await page.click('.login-btn'); // Use fixture in test await use(page); // Teardown: logout await page.click('.logout'); }, }); test('user dashboard', async ({ authenticatedPage }) => { await expect(authenticatedPage).toHaveURL(/dashboard/); });
POM Principles
One class per page
LoginPage, DashboardPage, CheckoutPage
Store locators as fields
Reuse across multiple tests
Action methods
login(), fillForm(), submit()
Return objects
Enable chaining and composition
import { Page } from '@playwright/test'; export class LoginPage { private page: Page; constructor(page: Page) { this.page = page; } async goto() { await this.page.goto('https://example.com/login'); } async login(email: string, password: string) { await this.page.fill('input[name="email"]', email); await this.page.fill('input[name="password"]', password); await this.page.click('.submit-btn'); } } // Usage in tests test('login', async ({ page }) => { const login = new LoginPage(page); await login.goto(); await login.login('user@test.com', 'pass'); });
Why Factories?
- Generate test users, products, orders consistently
- Avoid hardcoded test data scattered across tests
- Easy to create variations (premium user, guest user, admin)
- Centralized, maintainable
import { faker } from '@faker-js/faker'; interface User { email: string; password: string; name: string; } export const userFactory = { create(): User { return { email: faker.internet.email(), password: 'Password123!', name: faker.person.fullName(), }; }, createAdmin(): User { return { ...userFactory.create(), email: 'admin@example.com' }; }, }; // Usage in tests test('register user', async ({ page }) => { const user = userFactory.create(); // Now use user.email, user.password, user.name });
Tags for Organization
Tag tests to run subsets: smoke tests, regression, slow tests, etc.
Test Isolation
Each test runs in isolation โ no state leakage. But can configure serial tests when order matters (checkout flow).
test('login', { tag: '@smoke' }, async ({ page }) => { // This test tagged as smoke }); test.describe.serial('checkout flow', () => { test('add to cart', async ({ page }) => { // Runs first }); test('place order', async ({ page }) => { // Runs second (after previous test) }); }); // Run only smoke tests // npx playwright test --grep @smoke
page.route() for Network Control
page.route() intercepts HTTP requests before they leave the browser. Mock responses, abort requests, or modify headers.
Use Cases
// Mock API response await page.route('**/api/users/123', route => { route.fulfill({ status: 200, body: JSON.stringify({ id: 123, name: 'John' }), }); }); // Mock 500 error await page.route('**/api/checkout', route => { route.fulfill({ status: 500 }); }); // Abort request (network failure) await page.route('**/analytics', route => { route.abort(); }); // Wait for response const responsePromise = page.waitForResponse('**/api/login'); await page.click('.submit'); const response = await responsePromise;
APIRequestContext Fixture
Make HTTP requests (GET, POST, PUT, DELETE) directly from Playwright tests. No separate API test framework needed.
Common Patterns
- API-first testing: Call API to get auth token, use in UI tests
- Setup data: Create test users/orders via API before UI tests
- Verify backend: Check API returns correct data
- Hybrid flows: API setup + UI interaction + API verification
test('API + UI flow', async ({ request, page }) => { // Call login API const res = await request.post('https://api.example.com/login', { data: { email: 'user@test.com', password: 'pass' } }); const { token } = await res.json(); // Use token in UI test await page.goto('https://example.com', { headers: { Authorization: `Bearer ${token}` } }); await expect(page).toHaveURL(/dashboard/); });
storageState for Auth Reuse
Save browser state (cookies, localStorage) after login. Reuse across tests to skip login flow.
Setup Projects
Playwright config supports setup projects โ run once before all tests to establish state.
// First test: save auth state test('login and save state', async ({ page, context }) => { await page.goto('https://example.com/login'); await page.fill('input[name="email"]', 'user@test.com'); await page.fill('input[name="password"]', 'pass'); await page.click('.submit'); // Save storage state (cookies, localStorage) await context.storageState({ path: 'auth.json' }); }); // Other tests: reuse auth test.use({ storageState: 'auth.json' }); test('dashboard (already logged in)', async ({ page }) => { // Auth cookie already set, no login needed await page.goto('https://example.com/dashboard'); await expect(page).toHaveURL(/dashboard/); });
Working with iFrames
iFrames are isolated documents. Get the frame object, then interact with elements inside it.
Shadow DOM
Shadow DOM is encapsulated DOM tree. Playwright can penetrate it to find elements.
// Access iframe const frameHandle = page.frameLocator('[name="payment-iframe"]'); await frameHandle.locator('input[name="card"]').fill('4111111111111111'); // Shadow DOM penetration (automatic) page.locator('input'); // Finds elements in shadow DOM too
Browser Contexts
Isolated browser sessions with separate cookies, storage, etc. Test logged-in user + guest user side-by-side.
Multiple Pages/Tabs
Open multiple pages in same context. Simulate tab switching, window focus changes.
// Multiple contexts (isolated sessions) test('user + guest comparison', async ({ browser }) => { const userContext = await browser.newContext(); const guestContext = await browser.newContext(); const userPage = await userContext.newPage(); const guestPage = await guestContext.newPage(); // Test both simultaneously await userPage.goto('https://example.com'); await guestPage.goto('https://example.com'); await userContext.close(); await guestContext.close(); }); // Multiple pages in same context const page1 = await context.newPage(); const page2 = await context.newPage(); await page1.goto('https://example.com');
Common Browser Features
- Downloads: Capture downloaded files without saving to disk
- Dialogs: Handle alert/confirm/prompt dialogs
- Geolocation: Emulate location for location-aware features
- Device emulation: Test on iPhone, Android, tablet screens
- Locale/timezone: Test localized features
- Permissions: Grant/deny camera, microphone permissions
// Download file const downloadPromise = page.waitForEvent('download'); await page.click('.download-btn'); const download = await downloadPromise; // Handle dialog page.on('dialog', dialog => { dialog.accept(); // or dialog.dismiss() }); // Emulate geolocation await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); await context.grantPermissions(['geolocation']);
What is Trace Viewer?
Trace Viewer records everything: DOM snapshots at each step, network requests, console logs, browser events, timestamps. Open in browser to replay test execution frame-by-frame.
What You Get
use: {
// Record trace on first retry (test failure)
trace: 'on-first-retry', // or 'on', 'off'
},
// View trace
// npx playwright show-trace trace.zip
UI Mode Features
- Run tests with browser and UI side-by-side
- Step through test line-by-line
- Inspect DOM and network at each step
- Live editing: change test code and re-run
- Perfect for test development
# Launch UI Mode npx playwright test --ui # UI Mode for specific file npx playwright test tests/login.spec.ts --ui
Codegen Workflow
Run npx playwright codegen to open browser + recorder. Interact with page, and Playwright generates test code automatically.
Start Codegen
npx playwright codegen https://example.com
Interact
Click, type, navigate โ recorder captures everything
Copy Code
Copy generated code into your test file
Refine
Add assertions, organize with POM
--debug Mode
npx playwright test --debug pauses on first line, opens Inspector. Step through test line-by-line, see DOM/network at each step.
page.pause()
Add await page.pause() in test code to pause execution at that point. Opens Inspector for inspection.
Screenshots
await page.screenshot() saves PNG to disk. Useful for documenting test state in reports.
Video Recording
Set recordVideo in config to record all test execution as MP4 video. Great for debugging, sharing test evidence.
recordVideo: 'retain-on-failure'.HAR Recording
HAR (HTTP Archive) files record all network traffic. Replay them offline to run tests without backend.
Trace Files
Trace files contain full test execution timeline: DOM snapshots, network events, console logs. Open in Trace Viewer.
Timeline Analysis
Trace Viewer shows timeline of all events: navigations, API calls, DOM updates, with exact timestamps. Identify slow operations.
Using LLMs for Test Debugging
Copy test error + trace data into ChatGPT or Claude. AI can often spot issues (timing, selector specificity, logical errors) instantly.
Code Generation
Use LLMs to generate test code from requirements. "Write a test for user login flow" โ instant test skeleton.
Parallel by Default
Playwright runs tests in parallel automatically. Each test worker runs independently with isolated browser context. No state leakage.
Configuration
- fullyParallel: true โ Enable parallelization (default)
- workers: N โ Number of parallel workers (default = CPU cores)
- CI override โ Set workers to 1 in CI for stability
Sharding Concept
Divide test suite across multiple CI machines. Machine 1 runs tests 1-25, Machine 2 runs 26-50, etc. Each machine reports results, merged at end.
Usage
npx playwright test --shard=1/4 means "run shard 1 of 4 total shards"
Merging Sharded Reports
After all shards complete, use npx playwright merge-reports to combine individual reports into one unified HTML report.
Minimal Workflow
- Checkout code
- Setup Node.js
- Install dependencies
- Install Playwright browsers
- Run tests
- Upload report as artifact
npx playwright install --with-deps to install browser dependencies (chrome, firefox libs).Official Playwright Docker Image
mcr.microsoft.com/playwright โ pre-installed browsers, dependencies, perfect for CI.
Built-in Retries
Configure retries for flaky tests. Playwright automatically retries failed tests (default: 0 retries, recommended: 2-3 for CI).
Common Causes of Flakiness
- Race conditions (element appears asynchronously)
- Network timeouts (API slow)
- Timing-dependent tests (too strict timing)
- Hard-coded waits (sleep()) instead of auto-wait
Available Reporters
- HTML: Beautiful interactive report with screenshots
- JUnit: XML format for CI system integration
- JSON: Machine-readable format for processing
- GitHub: Annotate PR with failures
- Custom: Build your own reporter
Global Setup Pattern
Define separate setup/teardown scripts that run once before/after entire test suite. Good for expensive operations (start test server, initialize database).
Why Visual Testing?
- Functional tests pass but UI looks broken
- CSS changes break layout on mobile
- Designer updates button color accidentally
- Cross-browser rendering differences
Workflow
First run: Create baseline screenshots. Future runs: Compare pixel-by-pixel against baseline. Any difference fails test.
Basic Usage
await expect(page).toHaveScreenshot() takes screenshot and compares to baseline.
Options
- maxDiffPixels: Allow N pixels to differ (anti-aliasing)
- threshold: Allow X% difference (0-1)
- mask: Ignore regions (dynamic content)
- fullPage: Screenshot entire page
Baseline Workflow
- Create:
npx playwright teston first run creates baselines - Compare: Future runs compare against baselines
- Review: Failed tests show diff report (expected vs. actual)
- Approve:
npx playwright test --update-snapshotsaccepts new baseline
Versioning
Store baselines in git alongside tests. Review baseline changes in code review โ visual changes require approval.
Why Masking?
Some content changes every test run (timestamps, random IDs, counters). Mask these regions so they don't fail visual tests.
Common Cases
- Timestamps ("Updated 5 minutes ago")
- Random avatars/images
- Counters that increment
- User-specific data
Component Testing vs E2E
Component tests: Test single component in isolation (Button, Modal, Form). E2E tests: Test full user flows.
Playwright for Components
Mount components in dev server, test with Playwright. Bridges gap between unit tests and E2E.
Accessibility Testing with Playwright
Use getByRole() locators (encourages semantic HTML). Combine with axe-playwright for automated accessibility scanning.
Common A11y Issues
- Missing alt text on images
- Buttons without role/label
- Low contrast colors
- Missing form labels
- Keyboard navigation broken
Custom Reporters
Implement custom reporter to send results to Jira, Slack, custom database, or build internal dashboards.
Performance Metrics
Use page.metrics() to collect: navigation time, paint time, layout time. Assert times are under budget.
Core Web Vitals
- LCP: Largest Contentful Paint (loading speed)
- FID: First Input Delay (interactivity)
- CLS: Cumulative Layout Shift (visual stability)
Enterprise Considerations
- Test isolation: Parallel execution with no cross-test pollution
- Reporting: Centralized dashboards tracking all test suites
- Maintenance: Shared libraries, POM patterns, reusable fixtures
- Security: Secret management, credential rotation, compliance
- Scalability: Sharding, parallel execution, distributed infrastructure
Common Patterns in Production
- Authentication: Setup projects + storageState for multi-user testing
- Flakiness: Use soft assertions, retries, and comprehensive tracing
- Scale: Sharding across 10+ machines for 10,000-test suites
- Reporting: Custom reporters sending results to Jira, Slack, internal dashboards
- Maintenance: Shared POM library, test data factories, centralized config
Top Interview Questions (Playwright Mastery)
What makes Playwright faster than Selenium?
Direct browser automation (Chrome DevTools Protocol) vs. remote protocol. No network latency for each action = 2-3x faster execution.
How do you handle flaky tests?
Use Playwright's auto-wait (don't use sleep()), enable retries in CI, use Trace Viewer to debug, run tests multiple times to catch intermittencies.
Explain the difference between context and page.
Page = single tab. Context = isolated session with own cookies/storage. One context can have multiple pages.
How do you test OAuth/SSO flows?
Use storageState to save auth after login, reuse across tests. Or use setup projects to login once, share state with all tests.
What's the best way to handle dynamic content in visual tests?
Use masking to ignore regions that change every run (timestamps, counters). Or use maxDiffPixels for minor anti-aliasing differences.
How do you scale tests to 1000+ suite?
Sharding across multiple CI machines (--shard flag), parallelization on each machine (workers), merge-reports to combine results.
Common Errors & Solutions
Timeout: waiting for locator
Fix: Element not found or not visible. Use --debug to inspect, or increase timeout for slow operations.
Strict mode: locator matched multiple elements
Fix: Make locator more specific using .first(), .last(), .nth(), or getByRole() with name/level.
Test passes locally, fails in CI
Fix: Open Trace Viewer (trace: 'on-first-retry' in config), check for timing issues, run in docker locally.
Browser crashes in Docker
Fix: Use official mcr.microsoft.com/playwright image. If custom, add --no-sandbox and --disable-dev-shm-usage.