Playwright (TypeScript)  ›  Course Overview
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 request context
  • 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 supportJS, TS, Python, Java, C#Many languagesJS/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
  • 1
    Test Runner (Node.js process)
    Your test code runs in a Node.js process. @playwright/test manages scheduling, parallelism, and reporting.
  • 2
    Browser 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.
  • 3
    CDP / WebSocket Communication
    Test runner ↔ Browser communicate over a WebSocket using CDP (Chrome) or browser-specific protocols. This is how Playwright achieves network-level interception.
  • 4
    Browser 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?
A
WebDriver Protocol (W3C)
B
Selenium RemoteWebDriver HTTP API
C
Chrome DevTools Protocol (CDP)
D
Browser Extension Messaging API
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)
  • 1
    Role-based (ARIA) — Most Recommended
    page.getByRole('button', {name: 'Submit'}) — Mirrors how assistive tech sees the page. Most resilient.
  • 2
    Label / Placeholder text
    page.getByLabel('Email') / page.getByPlaceholder('Enter email') — Great for forms.
  • 3
    Text content
    page.getByText('Welcome back') — Good for headings and static text. Use {exact: true} for strict matching.
  • 4
    Test ID
    page.getByTestId('login-btn') — Requires data-testid attribute on the element. Best for dynamic pages.
  • 5
    CSS / 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?
A
ElementHandle is faster because it caches the DOM reference
B
Locators re-query the DOM on every action (lazy) and auto-wait; ElementHandles can go stale
C
Locators only work with CSS selectors; ElementHandles work with all selector types
D
They are identical — ElementHandle is just the older API name
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?
A
Wrap each assertion in a try/catch block
B
Use expect.soft() for each assertion
C
Use expect().toBeVisible() with timeout: 0
D
Use test.skip() after each assertion
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.