Complete Interview Course
Master Playwright for
SDET & QA Interviews
A structured, hands-on course covering everything you need to crack Playwright interviews. Real code examples, interview Q&A, and quizzes — built for working professionals.
13
Lessons
40+
Code Examples
30+
Interview Q&A
JS/TS
Both Languages
📋 What you'll learn
- Playwright architecture, browsers, and how it differs from Selenium & Cypress
- Setting up Playwright with JavaScript & TypeScript projects
- Locators, selectors, and best practices for finding elements
- Click, type, navigate, upload, drag-drop and all interaction APIs
- Assertions with
expect()— soft, hard, custom - Auto-waiting mechanics and when to use
waitFor - Page Object Model design pattern in TypeScript
- Fixtures, hooks, and test configuration
- API testing with Playwright's
requestcontext - Screenshots, videos, traces, and debugging
- 30+ most-asked interview questions with model answers
How to use this course
Each lesson ends with interview tips. Look for the ⭐ Interview Note boxes — those are the things interviewers ask about most. Complete the quiz at the end to test your readiness.
Beginner
What is Playwright?
Understand Playwright's architecture, how it works under the hood, and why it's become the top choice for modern web automation.
🎭 Core Definition
Playwright is an open-source automation framework by Microsoft (2020) that enables reliable end-to-end testing for modern web apps. It controls browsers using the Chrome DevTools Protocol (CDP) for Chromium/Chrome and WebKit, and its own protocol for Firefox.
Interview Note — Very commonly asked!
"Playwright uses browser-native APIs (CDP / WebKit protocol) to control browsers directly, unlike Selenium WebDriver which uses HTTP. This gives Playwright faster and more stable automation."
🆚 Playwright vs Selenium vs Cypress
| Feature | Playwright | Selenium | Cypress |
|---|---|---|---|
| Multi-browser | ✓ Chrome, FF, Safari | ✓ All | ~ Chrome, FF, Edge |
| Multi-tab / iframe | ✓ Native | ~ Limited | ✗ No multi-tab |
| Auto-waiting | ✓ Built-in | ✗ Manual | ✓ Built-in |
| API Testing | ✓ Built-in | ✗ No | ~ Via plugin |
| Language support | JS, TS, Python, Java, C# | Many languages | JS/TS only |
| Parallel execution | ✓ Native workers | ~ Grid needed | ~ Paid feature |
| Mobile emulation | ✓ Built-in | ~ Appium needed | ~ Limited |
| Trace viewer | ✓ Built-in | ✗ No | ~ Time-travel |
🏗️ Playwright Architecture
-
1Test Runner (Node.js process)Your test code runs in a Node.js process.
@playwright/testmanages scheduling, parallelism, and reporting. -
2Browser Server (separate process)Playwright launches the actual browser (Chromium, Firefox, or WebKit) as a separate process. This isolation prevents crashes from killing your test runner.
-
3CDP / WebSocket CommunicationTest runner ↔ Browser communicate over a WebSocket using CDP (Chrome) or browser-specific protocols. This is how Playwright achieves network-level interception.
-
4Browser Contexts (Isolation)Each test gets its own
BrowserContext— like an incognito session. Cookies, storage, and auth state are fully isolated between tests.
Quick Check
1 question
Which protocol does Playwright use to control Chromium browsers?
Correct! Playwright uses CDP (Chrome DevTools Protocol) for Chromium. This gives it direct, low-level access to the browser — enabling features like network interception, performance monitoring, and more reliable automation than the WebDriver HTTP protocol used by Selenium.
Beginner
Setup & Installation
Get a Playwright project running from scratch. Understand the config file, project structure, and how to run your first test.
⚡ Installation
BASH
# Create a new project and install Playwright npm init playwright@latest # OR add to existing project npm install -D @playwright/test # Install browsers (Chromium, Firefox, WebKit) npx playwright install # Install only specific browser npx playwright install chromium
Interview Tip
Interviewers often ask: "How do you install Playwright and what browsers does it support?" Answer:
npm init playwright@latest scaffolds the project. Playwright bundles its own browser binaries — Chromium, Firefox, and WebKit (Safari engine) — via npx playwright install.
📁 Project Structure
STRUCTURE
my-project/ ├── tests/ │ ├── example.spec.ts # Test files │ └── pages/ # Page Object classes │ └── LoginPage.ts ├── playwright.config.ts # Main config file ├── package.json └── test-results/ # Auto-generated reports
⚙️ playwright.config.ts — Key Settings
TYPESCRIPT
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, // run all tests in parallel retries: 2, // retry failed tests workers: 4, // parallel workers reporter: 'html', // HTML report use: { baseURL: 'https://example.com', headless: true, screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'on-first-retry', }, 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'] } }, ], });
▶️ Running Tests
BASH
# Run all tests npx playwright test # Run specific file npx playwright test login.spec.ts # Run with UI mode (interactive) npx playwright test --ui # Run headed (see browser) npx playwright test --headed # Run specific project (browser) npx playwright test --project=chromium # Debug mode (pause on each step) npx playwright test --debug # Generate HTML report and open npx playwright show-report
Core
Locators & Selectors
Locators are the heart of Playwright. Learn all selector strategies, their priority order, and how to write resilient, maintainable locators.
Most asked interview topic on Playwright!
"What is a Locator in Playwright and how is it different from ElementHandle?" Locators are lazy — they re-query the DOM on each action and auto-wait. ElementHandles are eager snapshots and can go stale. Always prefer Locators.
🎯 Locator Priority (Best → Worst)
-
1Role-based (ARIA) — Most Recommended
page.getByRole('button', {name: 'Submit'})— Mirrors how assistive tech sees the page. Most resilient. -
2Label / Placeholder text
page.getByLabel('Email')/page.getByPlaceholder('Enter email')— Great for forms. -
3Text content
page.getByText('Welcome back')— Good for headings and static text. Use{exact: true}for strict matching. -
4Test ID
page.getByTestId('login-btn')— Requiresdata-testidattribute on the element. Best for dynamic pages. -
5CSS / XPath — Use as last resort
page.locator('.btn-primary')/page.locator('//button[@type="submit"]')— Fragile if UI changes.
💻 Locator Code Examples
TYPESCRIPT
import { test, expect } from '@playwright/test'; test('locator examples', async ({ page }) => { await page.goto('/'); // ✅ Best: ARIA role await page.getByRole('button', { name: 'Sign in' }).click(); // ✅ Form label await page.getByLabel('Username').fill('john@example.com'); // ✅ Placeholder await page.getByPlaceholder('Search...').fill('Playwright'); // ✅ Test ID await page.getByTestId('submit-btn').click(); // Chaining locators (filter within scope) const row = page.getByRole('row', { name: 'John Doe' }); await row.getByRole('button', { name: 'Delete' }).click(); // Filter locator const active = page.locator('.card').filter({ hasText: 'Active' }); // nth element await page.getByRole('listitem').nth(2).click(); // Inside iframe const frame = page.frameLocator('#payment-frame'); await frame.getByLabel('Card number').fill('4242424242424242'); });
Quick Check
1 question
What is the key difference between a
Locator and an ElementHandle in Playwright?
Correct! Locators are lazy — they don't hold a reference to a DOM node. Every time you call
.click() or .fill(), Playwright re-queries the DOM. This means they are immune to "stale element" errors. ElementHandles capture the element at a point in time and can become invalid if the DOM updates.
Core
Actions & Interactions
Explore every interaction API — clicks, keyboard, file uploads, drag & drop, iframes, new tabs, and more.
🖱️ Common Actions
TYPESCRIPT
// Click variants await page.getByRole('button').click(); await page.getByText('Menu').dblclick(); await element.click({ button: 'right' }); // right click await element.click({ modifiers: ['Control'] }); // Ctrl+click // Typing await page.getByLabel('Name').fill('Rishabh'); // clears and types await page.getByLabel('OTP').pressSequentially('1234'); // keystroke by keystroke // Keyboard await page.keyboard.press('Enter'); await page.keyboard.press('Control+A'); // Select dropdown await page.getByLabel('Country').selectOption('India'); await page.getByLabel('Tags').selectOption(['a', 'b']); // multi-select // Checkbox / Radio await page.getByLabel('Accept terms').check(); await page.getByLabel('Accept terms').uncheck(); // File upload await page.getByLabel('Upload').setInputFiles('./file.pdf'); // Hover await page.getByText('Profile').hover(); // Drag and Drop await page.getByTestId('card').dragTo(page.getByTestId('dropzone')); // Scroll into view await page.getByText('Footer').scrollIntoViewIfNeeded();
🆕 Handling New Tabs & Popups
TYPESCRIPT
// Capture new page/tab opened by a click const [newPage] = await Promise.all([ context.waitForEvent('page'), page.getByText('Open in new tab').click(), ]); await newPage.waitForLoadState(); await expect(newPage).toHaveTitle('Dashboard'); // Handle browser dialogs (alert/confirm/prompt) page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.accept(); // or dialog.dismiss() }); // Mock geolocation await context.grantPermissions(['geolocation']); await context.setGeolocation({ latitude: 28.6, longitude: 77.2 });
Key Interview Pattern
The
Promise.all() pattern for new tabs is a classic interview question. You must start listening BEFORE clicking — if you click first, you might miss the event. This is a race condition and Playwright's recommended pattern avoids it.
Core
Assertions
Playwright's
expect() API includes auto-retrying assertions that wait for your expected condition to become true.
Auto-retrying Assertions
Playwright's
expect(locator).toBeVisible() will retry for up to 5 seconds (configurable via timeout) until the assertion passes or times out. This eliminates flaky tests from timing issues.
✅ Most Important Assertions
TYPESCRIPT
// ── PAGE ASSERTIONS ── await expect(page).toHaveURL('https://example.com/dashboard'); await expect(page).toHaveTitle(/Dashboard/); // regex allowed // ── ELEMENT ASSERTIONS ── await expect(locator).toBeVisible(); await expect(locator).toBeHidden(); await expect(locator).toBeEnabled(); await expect(locator).toBeDisabled(); await expect(locator).toBeChecked(); await expect(locator).toBeFocused(); // ── TEXT & VALUE ── await expect(locator).toHaveText('Welcome, Rishabh'); await expect(locator).toContainText('Welcome'); await expect(locator).toHaveValue('user@example.com'); await expect(locator).toHaveAttribute('href', '/home'); // ── COUNT ── await expect(page.getByRole('listitem')).toHaveCount(5); // ── SOFT ASSERTIONS (don't stop test on failure) ── await expect.soft(locator).toBeVisible(); await expect.soft(locator).toHaveText('Expected'); // All soft assertion failures are reported together at the end // ── CUSTOM TIMEOUT ── await expect(locator).toBeVisible({ timeout: 10000 });
🔄 Hard vs Soft Assertions
Hard Assertion (default)
Throws immediately on failure. Test stops. All subsequent steps are skipped. Use for critical path checks.
Soft Assertion
expect.soft() — Records the failure but continues execution. Good for validating multiple fields in one go.Quick Check
1 question
You want to validate 5 form fields but you do NOT want the test to stop if one field fails — you want all failures reported together. What should you use?
Correct!
expect.soft() marks the assertion as "non-fatal". All soft assertion failures are collected and reported together at the end of the test, but the test continues executing. This is perfect for form validation scenarios.
Core
Waits & Auto-waiting
One of Playwright's biggest strengths — auto-waiting. Understand what it waits for and when you need explicit waits.
⏳ Auto-waiting Checklist
Before performing any action, Playwright automatically checks ALL of the following:
- Element is attached to the DOM
- Element is visible (not hidden, not opacity:0)
- Element is stable (not moving / animating)
- Element receives events (not obscured by overlay)
- Element is enabled (not disabled)
🔧 Explicit Waits — When to Use
TYPESCRIPT
// Wait for navigation / page load await page.waitForLoadState('networkidle'); // no network req for 500ms await page.waitForLoadState('domcontentloaded'); // Wait for URL to change await page.waitForURL('**/dashboard'); // Wait for specific network request await page.waitForResponse('**/api/users'); // Wait for element state await page.getByRole('button').waitFor({ state: 'visible' }); await page.getByText('Loading').waitFor({ state: 'hidden' }); // ❌ AVOID: page.waitForTimeout() is flaky and slow // await page.waitForTimeout(3000); // don't do this
Interview Red Flag
Never use
page.waitForTimeout(ms) in production tests. Interviewers watch for this. It makes tests slow and brittle. Use event-based waits or assertion retries instead.
Advanced
Page Object Model (POM)
The most important design pattern for scalable test automation. POM separates page structure from test logic.
Top 3 Interview Question
"Implement a Page Object Model for a login page." Be ready to write this from scratch in TypeScript. Know the benefits: reusability, readability, single point of change when UI changes.
📄 LoginPage.ts — Page Class
TYPESCRIPT
import { Page, Locator } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.loginButton = page.getByRole('button', { name: 'Sign In' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } async loginAndExpectSuccess(email: string, password: string) { await this.login(email, password); await this.page.waitForURL('**/dashboard'); } }
🧪 login.spec.ts — Using the POM
TYPESCRIPT
import { test, expect } from '@playwright/test'; import { LoginPage } from './pages/LoginPage'; test('valid login redirects to dashboard', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.loginAndExpectSuccess('user@test.com', 'secret'); await expect(page).toHaveURL(/dashboard/); }); test('invalid login shows error', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('wrong@test.com', 'wrongpass'); await expect(loginPage.errorMessage).toBeVisible(); await expect(loginPage.errorMessage).toContainText('Invalid credentials'); });
Advanced
Fixtures & Hooks
Fixtures are Playwright's powerful dependency injection system. Understand built-in fixtures and how to create custom ones.
🔧 Built-in Fixtures
page
Isolated browser page per test
context
Browser context (session)
browser
Browser instance
request
API request context
browserName
e.g. 'chromium'
testInfo
Test metadata & status
🛠️ Custom Fixture — Authenticated Page
TYPESCRIPT
// fixtures.ts import { test as base, expect } from '@playwright/test'; import { LoginPage } from './pages/LoginPage'; type Fixtures = { loggedInPage: Page; loginPage: LoginPage; }; export const test = base.extend<Fixtures>({ loggedInPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@test.com', 'pass'); await page.waitForURL('**/dashboard'); await use(page); // provide fixture to test // teardown code here (runs after test) }, }); export { expect } from '@playwright/test'; // ── Usage in test ── // import { test, expect } from './fixtures'; // test('dashboard shows username', async ({ loggedInPage }) => { // await expect(loggedInPage.getByText('Hello User')).toBeVisible(); // });
🎣 Hooks
TYPESCRIPT
test.beforeAll(async ({ browser }) => { // Runs once before all tests in this file // Good for: creating shared auth state, DB seeding }); test.afterAll(async () => { // Cleanup after all tests complete }); test.beforeEach(async ({ page }) => { await page.goto('/'); // navigate before each test }); test.afterEach(async ({ page }, testInfo) => { if (testInfo.status !== testInfo.expectedStatus) { await page.screenshot({ path: `failed-${testInfo.title}.png` }); } });
Advanced
API Testing
Playwright can test REST APIs directly without a browser. Also learn to mock and intercept network requests.
🌐 Direct API Testing
TYPESCRIPT
import { test, expect, request } from '@playwright/test'; test('GET /users returns 200', async ({ request }) => { const response = await request.get('https://api.example.com/users'); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveLength(10); expect(body[0]).toHaveProperty('id'); }); test('POST /users creates a user', async ({ request }) => { const response = await request.post('/users', { data: { name: 'Rishabh', email: 'r@example.com' }, headers: { 'Authorization': 'Bearer token123' } }); expect(response.status()).toBe(201); });
🔁 Network Interception & Mocking
TYPESCRIPT
// Mock an API response await page.route('**/api/products', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ id: 1, name: 'Mock Product' }]), }); }); // Intercept and modify request await page.route('**/api/users', async route => { const req = route.request(); console.log(req.url(), req.method()); await route.continue(); // let request go through }); // Block specific requests (e.g. analytics) await page.route('**/{analytics,tracking}/**', route => route.abort()); // Wait for a specific response const [response] = await Promise.all([ page.waitForResponse('**/api/login'), page.getByRole('button', {name: 'Login'}).click(), ]); expect(await response.json()).toHaveProperty('token');
Advanced
Screenshots, Videos & Traces
Playwright's debugging toolkit — capture evidence of test runs, watch replays, and use the trace viewer.
📸 Screenshots
TYPESCRIPT
// Full page screenshot await page.screenshot({ path: 'screenshot.png', fullPage: true }); // Element screenshot await page.getByRole('dialog').screenshot({ path: 'dialog.png' }); // Visual comparison (pixel-level diff) await expect(page).toHaveScreenshot('baseline.png'); // Config: auto-capture on failure // In playwright.config.ts: screenshot: 'only-on-failure'
🎥 Video & Trace Viewer
BASH
# Record trace on first retry, then view it npx playwright test --trace on npx playwright show-trace trace.zip # In config: trace: 'on-first-retry' captures # screenshots, network, console, DOM snapshots # for every step — time-travel debugging!
Interview Q: How do you debug a failing Playwright test?
Answer: "I use Trace Viewer —
npx playwright show-trace — which lets me step through each action, see DOM state, network requests, and console logs at every point in time. I also use --debug flag which opens Playwright Inspector for step-by-step execution."
Interview Prep
Top Interview Q&A
The 30 most frequently asked Playwright interview questions with model answers. Click each question to reveal the answer.
Final Quiz
Interview Readiness Quiz
10 questions. Covers all topics. Timed like a real interview. Good luck!
© 2025 Rishabh Ranjan. All rights reserved.