Topic 1 of 120%
🎭 Complete Tutorial

Playwright
Automation Testing Tutorial

Master end-to-end automation testing with Playwright — from installation to writing real tests, locators, assertions, reports, and professional best practices.

⏱️ ~3 hrs 🎯 12 Topics 💻 Code examples 🧪 Quiz each section
01
Introduction
What is Playwright?
Playwright is an open-source, end-to-end automation testing framework developed by Microsoft and first released in January 2020. It allows you to write automated tests that control a real browser — clicking buttons, filling forms, navigating pages — exactly as a human user would, but automatically. It supports Chromium, Firefox, and WebKit (Safari's engine) with a single unified API.
🧠 Simple analogy: Imagine you need to test a login page 100 times with different credentials. Doing it manually would take hours. Playwright is like hiring a robot that can open the browser, go to the login page, type the email and password, click the Login button, and verify the dashboard appears — all in 2 seconds, perfectly, every single time.

Key facts about Playwright:

Created by
Microsoft — built by ex-Google engineers who originally created Puppeteer
Released
January 2020 — relatively new but extremely fast-growing
Languages
JavaScript / TypeScript, Python, Java, .NET (C#)
Browsers
Chromium (Chrome, Edge), Firefox, WebKit (Safari) — all three with one API
License
Open source — Apache 2.0. Free to use for everyone.
GitHub Stars
74,000+ stars — more than Selenium's 32,000. Most popular automation tool in 2025.
Type of testing
End-to-End (E2E) testing — tests the full user journey from browser to server

What makes Playwright special compared to older tools:

  • Auto-waiting built in Playwright automatically waits for elements to appear, be visible, and be ready before interacting. No more manual sleep() or flaky tests from timing issues.
  • All browsers, one API Write one test and run it on Chrome, Firefox, and Safari without changing any code. Playwright handles the browser differences internally.
  • Modern web support Playwright handles SPAs (React, Angular, Vue), Shadow DOM, iframes, multiple tabs, file uploads, network mocking, and more — out of the box.
  • Codegen tool Record your browser actions and Playwright automatically generates the test code for you. No coding required to create initial test scripts.
  • Trace Viewer When a test fails, the Trace Viewer shows you a step-by-step video replay, screenshots, and DOM snapshots — so you know exactly what went wrong.
💡
Industry adoption: As of 2025, Playwright has overtaken Selenium in usage among automation testers in multiple industry surveys. It is rapidly becoming the default choice for teams starting new automation projects on modern web apps.
🧪 Quiz: Which company developed and maintains Playwright?
02
Comparison
Playwright vs Selenium — Key Differences
Both Playwright and Selenium are browser automation tools, but they differ significantly in architecture, features, and developer experience. Understanding these differences helps you choose the right tool and also prepares you for interview questions.
🎭 Playwright
Created:2020 by Microsoft
Architecture:Direct browser communication (no WebDriver middleman)
Auto-wait:Built-in — no manual waits needed
Browsers:Chromium, Firefox, WebKit (bundled)
Speed:Faster — direct protocol, parallel by default
Best for:Modern web apps (React, Angular, Vue, SPAs)
🔵 Selenium
Created:2004 by Jason Huggins
Architecture:WebDriver protocol — extra layer between test and browser
Auto-wait:Not built-in — must write explicit waits manually
Browsers:Chrome, Firefox, Safari, Edge, IE (requires separate drivers)
Speed:Slower — driver overhead, Grid needed for parallel
Best for:Legacy apps, enterprise setups, broader language support
Feature🎭 Playwright🔵 Selenium
Released2020 — Microsoft2004 — Jason Huggins
ArchitectureDirect browser control (CDP/WebSocket)WebDriver protocol — extra middleman layer
Auto-waiting✅ Built-in — no manual waits needed❌ Must write explicit waits manually
Browser setupBrowsers bundled — one command installs allManual driver files needed (chromedriver etc.)
SpeedFaster — direct protocol + native parallelSlower — driver overhead, Grid for parallel
Language supportJS/TS, Python, Java, .NETJava, Python, C#, Ruby, JS, PHP, Perl
Multi-tab testing✅ Native support⚠️ Complex — requires window handle management
Network mocking✅ Built-in API interception❌ No built-in — needs extra tools
Trace / Debug✅ Trace Viewer with video + DOM snapshotsScreenshots only
Legacy browser❌ No IE support✅ Supports Internet Explorer
Best use caseModern web apps, SPAs, CI/CD pipelinesEnterprise legacy systems, wider language needs
ℹ️
The core architectural difference: Selenium adds an extra layer — your test code → WebDriver protocol → browser driver → browser. Playwright removes the middleman and communicates directly with the browser using its native debugging protocol (like Chrome DevTools Protocol). This is why Playwright is faster and more reliable.
⚠️
When to still use Selenium: If your team needs to test on Internet Explorer (legacy enterprise apps), or if your existing automation is already in Selenium with Ruby or PHP, Selenium remains the right choice. For new projects on modern apps — Playwright is the recommended starting point in 2025.
🧪 Quiz: What is the main reason Playwright tests are faster and less flaky compared to Selenium tests?
03
Getting Started
Installation & Project Setup
Playwright is installed as an npm package. You need Node.js version 18 or higher installed on your system first. The recommended way to set up a new Playwright project is using the official initializer command which scaffolds everything automatically.

Prerequisites — make sure these are installed first:

  • 1
    Node.js (v18+) Download from nodejs.org. After installing, verify with: node --version
  • 2
    VS Code (recommended) Best IDE for Playwright. Install the official Playwright Test for VS Code extension by Microsoft for an excellent testing experience.

Step 1 — Create a new project folder and open terminal:

mkdir my-playwright-tests && cd my-playwright-tests

Step 2 — Run the Playwright initializer:

npm init playwright@latest
📋 Initializer will ask you 4 questions:
1. Where to put tests? Press Enter to accept default: tests
2. Add GitHub Actions workflow? Press Y (yes) — useful for CI/CD later
3. Install Playwright browsers? Press Y (yes) — downloads Chromium, Firefox, WebKit
4. TypeScript or JavaScript? Choose based on your preference. TypeScript recommended for professional projects.

Step 3 — What gets created after initialization:

my-playwright-tests/ ├── playwright.config.ts # Main config: browsers, base URL, timeouts ├── package.json # Project dependencies ├── tests/ │ └── example.spec.ts # Example test file — study and then delete ├── tests-examples/ │ └── demo-todo-app.spec.ts # More example tests for reference └── playwright-report/ # HTML reports generated here after test run

Step 4 — Verify installation by running example tests:

npx playwright test
💡
The playwright.config.ts file is where you set your base URL (the root URL of the app you're testing), timeout values, which browsers to test on, and whether to run in headless or headed mode. Always configure baseURL so you can write page.goto('/') instead of the full URL in every test.
ℹ️
Headless vs Headed mode:
Headless = Browser runs in the background with no visible window. Faster. Used in CI/CD pipelines.
Headed = Browser window opens and you can watch tests execute visually. Used during development and debugging.
Default is headless. Run headed with: npx playwright test --headed
🧪 Quiz: What is the correct command to initialize a new Playwright project with all browsers and configuration files?
04
Project Setup
Project Structure & playwright.config.ts
Understanding the project structure is essential before writing tests. The playwright.config.ts file is the heart of your Playwright project — it controls which browsers to test on, timeouts, base URL, screenshot/video capture settings, and more.

The playwright.config.ts file — key settings explained:

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',           // Folder where test files live
  timeout: 30000,              // Max time for each test (30 seconds)
  retries: 1,                  // Retry failed tests once before marking fail
  reporter: 'html',           // Generate HTML report after test run

  use: {
    baseURL: 'http://localhost:3000', // Root URL of your app
    headless: true,                  // Run without browser window
    screenshot: 'only-on-failure',   // Capture screenshot when test fails
    video: 'retain-on-failure',     // Record video on failure
    trace: 'on-first-retry',        // Capture trace on first retry
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

Recommended folder structure for a real project:

my-playwright-tests/ ├── playwright.config.ts ├── tests/ │ ├── login.spec.ts # Tests for Login feature │ ├── registration.spec.ts # Tests for Registration │ └── checkout.spec.ts # Tests for Checkout flow ├── pages/ # Page Object Model files │ ├── LoginPage.ts │ └── HomePage.ts └── utils/ # Reusable helper functions └── helpers.ts
💡
Naming convention: All Playwright test files must end with .spec.ts (or .spec.js for JavaScript). Playwright only picks up files matching this pattern. Naming by feature makes it easy to run tests for a specific feature: npx playwright test login runs only login tests.
🧪 Quiz: Which configuration property in playwright.config.ts sets the root URL of the application being tested?
05
Writing Tests
Your First Playwright Test
A Playwright test file has a predictable structure: import the test and expect functions, use test() to declare each test, and use async/await because all browser interactions are asynchronous. Each test receives a page object — this represents the browser tab and is your main tool for all interactions.

Anatomy of a Playwright test:

TypeScript / JavaScript
// 1. Import test and expect from Playwright
import { test, expect } from '@playwright/test';

// 2. Declare a test with a descriptive name
test('user can login with valid credentials', async ({ page }) => {

  // 3. Navigate to the login page
  await page.goto('/login');

  // 4. Find the email field and type into it
  await page.getByLabel('Email').fill('priya@example.com');

  // 5. Find the password field and type into it
  await page.getByLabel('Password').fill('Test@123');

  // 6. Click the Login button
  await page.getByRole('button', { name: 'Login' }).click();

  // 7. Verify the dashboard heading is visible after login
  await expect(page.getByRole('heading', { name: 'Dashboard' }))
    .toBeVisible();
});

Grouping related tests with test.describe():

login.spec.ts — multiple tests grouped
import { test, expect } from '@playwright/test';

test.describe('Login Page', () => {

  test.beforeEach(async ({ page }) => {
    await page.goto('/login'); // runs before EVERY test in this group
  });

  test('valid login redirects to dashboard', async ({ page }) => {
    await page.getByLabel('Email').fill('priya@example.com');
    await page.getByLabel('Password').fill('Test@123');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL('/dashboard');
  });

  test('wrong password shows error message', async ({ page }) => {
    await page.getByLabel('Email').fill('priya@example.com');
    await page.getByLabel('Password').fill('wrongpass');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.getByText('Invalid credentials')).toBeVisible();
  });

});
test()
Declares a single test case. Takes a name string and an async function with the page object.
test.describe()
Groups related tests together. Like a folder for tests that test the same feature.
test.beforeEach()
Runs a setup action before every test in the group. Used to navigate to a page, or log in before each test.
test.afterEach()
Runs cleanup after every test. Used to log out, clear data, or reset state.
async / await
Required because all browser interactions (goto, click, fill) are asynchronous operations. Always await them.
🧪 Quiz: What is the purpose of test.beforeEach() in a Playwright test file?
06
Core Concept
Locators — How to Find Page Elements
A Locator is Playwright's way of finding an element on the page so you can interact with it (click, type, read text, etc.). Choosing the right locator is the most important skill in writing stable, maintainable tests. Playwright provides several locator strategies — from the most preferred to fallback options.

Locator priority — use in this order:

⭐ Best: getByRole ⭐ Best: getByLabel ⭐ Best: getByPlaceholder ⭐ Best: getByText ✅ Good: getByTestId ⚠️ Fallback: CSS selector 🔴 Last resort: XPath
⭐ getByRole — Most Recommended Locator
Finds elements by their ARIA role — the semantic meaning of the element (button, heading, link, checkbox, etc.). This is the most stable and recommended locator because roles don't change when developers restyle or refactor UI. It also aligns with accessibility standards.
getByRole examples
// Click a button with visible text "Submit"
await page.getByRole('button', { name: 'Submit' }).click();

// Click a button — case insensitive match using regex
await page.getByRole('button', { name: /submit/i }).click();

// Assert a heading with text "Welcome" is visible
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();

// Check a checkbox labeled "Accept Terms"
await page.getByRole('checkbox', { name: 'Accept Terms' }).check();

// Click a navigation link
await page.getByRole('link', { name: 'Home' }).click();
⭐ getByLabel — Best for Form Fields
Finds an input field by its associated <label> text. Perfect for forms — email, password, name, phone fields. If your input has a proper label in HTML, always use this.
getByLabel examples
// Fill the Email input field
await page.getByLabel('Email').fill('priya@example.com');

// Fill the Password input field
await page.getByLabel('Password').fill('Test@123');
⭐ getByPlaceholder — For Inputs Without Labels
Finds an input by its placeholder attribute text. Useful when the input field shows hint text like "Enter your email" but has no visible label.
getByPlaceholder examples
// Fill input that shows placeholder "Search products..."
await page.getByPlaceholder('Search products...').fill('laptop');

// Fill input with placeholder "Enter your email"
await page.getByPlaceholder('Enter your email').fill('test@test.com');
⭐ getByText — Find by Visible Text Content
Finds elements by their visible text content. Use for paragraphs, spans, divs, error messages, and any element identified by what the user can read.
getByText examples
// Assert error message is shown
await expect(page.getByText('Invalid credentials')).toBeVisible();

// Click a menu item by its text
await page.getByText('My Profile').click();

// Exact match — only matches "Log out", not "Log out now"
await page.getByText('Log out', { exact: true }).click();
✅ getByTestId — For Elements With data-testid Attribute
Finds elements by a custom data-testid attribute that developers add specifically for testing purposes. Very stable — doesn't break with UI changes — but requires collaboration with developers to add the attribute.
getByTestId example
// HTML: <button data-testid="submit-btn">Submit</button>
await page.getByTestId('submit-btn').click();
⚠️
Avoid CSS / XPath as first choice. Locators like page.locator('.btn-primary') or page.locator('//div[@class="container"]') break easily when developers change class names or restructure HTML. Use them only when user-facing locators above are not possible.
🧪 Quiz: A login form has a properly labeled "Email" input field. Which Playwright locator should you use to find it?
07
Interactions
Actions — Interacting With Page Elements
Actions are the methods you call on a locator to interact with the page — clicking, typing, selecting, hovering, checking checkboxes, uploading files, and more. Every action in Playwright is awaited because browser interactions are asynchronous.

Most commonly used actions with examples:

Common Playwright Actions
// ── CLICK ──────────────────────────────────────
await page.getByRole('button', { name: 'Login' }).click();
await page.getByText('Forgot Password?').click();

// ── FILL (type into input) ──────────────────────
await page.getByLabel('Username').fill('priya123');
// fill() clears existing text first, then types

// ── TYPE (types character by character) ─────────
await page.getByLabel('Search').pressSequentially('laptop');
// Triggers keystroke events — needed for autocomplete

// ── CLEAR ───────────────────────────────────────
await page.getByLabel('Email').clear();

// ── SELECT DROPDOWN ─────────────────────────────
await page.getByRole('combobox').selectOption('India');
await page.getByLabel('Country').selectOption({ value: 'IN' });

// ── CHECKBOX ────────────────────────────────────
await page.getByRole('checkbox', { name: 'Accept Terms' }).check();
await page.getByRole('checkbox', { name: 'Subscribe' }).uncheck();

// ── HOVER ───────────────────────────────────────
await page.getByText('Account').hover();
// Use to open dropdown menus that appear on hover

// ── NAVIGATE ────────────────────────────────────
await page.goto('/login');          // relative URL (uses baseURL)
await page.goto('https://example.com'); // absolute URL
await page.goBack();                  // browser back button
await page.reload();                  // refresh the page

// ── KEYBOARD KEYS ───────────────────────────────
await page.getByLabel('Search').press('Enter');
await page.keyboard.press('Escape');   // close modal

// ── SCREENSHOT ──────────────────────────────────
await page.screenshot({ path: 'screenshot.png' });
💡
fill() vs pressSequentially():
fill() directly sets the input value — fast, no keystroke events. Use for most form fields.
pressSequentially() simulates actual keystrokes one by one — use for search boxes, autocomplete fields, or any input that listens for keyboard events to trigger suggestions.
🧪 Quiz: Which Playwright action clears existing text in an input field and then types new text into it?
08
Verification
Assertions — Verifying Expected Results
Assertions are the checks in your test that verify the page behaves as expected. In Playwright, assertions use the expect() function. Playwright's assertions are "web-first" — they automatically retry until the condition is met or the timeout is reached, making tests resilient to minor timing variations.

Most important assertions you will use every day:

Essential Playwright Assertions
// ── VISIBILITY ──────────────────────────────────
await expect(page.getByText('Welcome, Priya!')).toBeVisible();
await expect(page.getByText('Error')).not.toBeVisible();

// ── URL ─────────────────────────────────────────
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/dashboard/); // regex match

// ── PAGE TITLE ──────────────────────────────────
await expect(page).toHaveTitle('My App - Dashboard');

// ── ELEMENT TEXT ────────────────────────────────
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByTestId('cart-count')).toContainText('3');

// ── INPUT VALUE ─────────────────────────────────
await expect(page.getByLabel('Email')).toHaveValue('priya@example.com');

// ── CHECKBOX STATE ──────────────────────────────
await expect(page.getByRole('checkbox', { name: 'Terms' })).toBeChecked();

// ── BUTTON/INPUT STATE ──────────────────────────
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();

// ── COUNT (number of items in a list) ───────────
await expect(page.getByRole('listitem')).toHaveCount(5);

Complete test example — Login with assertions:

Real login test with full assertions
test('valid login shows dashboard', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('priya@example.com');
  await page.getByLabel('Password').fill('Test@123');
  await page.getByRole('button', { name: 'Login' }).click();

  // Assert URL changed to dashboard
  await expect(page).toHaveURL('/dashboard');

  // Assert heading is visible
  await expect(page.getByRole('heading', { name: 'Dashboard' }))
    .toBeVisible();

  // Assert user name appears in the header
  await expect(page.getByText('Welcome, Priya')).toBeVisible();
});
ℹ️
toHaveText() vs toContainText():
toHaveText('Cart (3)') — full exact text match (entire element text must equal this)
toContainText('3') — partial match (element text just needs to contain '3')
Use toContainText when you only want to verify part of the text, e.g. verifying a cart count without knowing the full surrounding label.
🧪 Quiz: After clicking Login, you want to verify the browser navigated to /dashboard. Which assertion do you use?
09
Core Feature
Auto-Wait & Timeouts — No More Flaky Tests
Auto-waiting is Playwright's most powerful feature. Before performing any action (click, fill, etc.) on an element, Playwright automatically waits until the element is: attached to the DOM, visible, stable (not animating), enabled (not disabled), and receives events. This eliminates the most common cause of test failures — timing issues.
🧠 Old way (Selenium): You had to manually add WebDriverWait(driver, 10).until(EC.element_to_be_clickable(...)) before every interaction. Forget to add it, and your test fails randomly when the page loads slowly.

🎭 Playwright's way: Just write await page.getByRole('button', {name: 'Submit'}).click(). Playwright automatically waits — no extra code needed.
Auto-wait in action — no explicit waits needed
// ✅ PLAYWRIGHT WAY — clean, no manual waits
test('add item to cart', async ({ page }) => {
  await page.goto('/products');

  // Playwright waits for button to appear and be clickable
  await page.getByRole('button', { name: 'Add to Cart' }).click();

  // Playwright waits for toast/confirmation to become visible
  await expect(page.getByText('Added to cart!')).toBeVisible();
});

// ❌ OLD WAY (Selenium-style) — messy, error-prone
// await page.waitForSelector('button');  // Don't need this
// await page.waitForTimeout(2000);       // Never use this

Timeouts — what they are and how to configure:

Test timeout
Max time for a complete test to finish. Default: 30 seconds. Set in playwright.config.ts with timeout: 30000.
Assertion timeout
Max time for an assertion to pass. Default: 5 seconds. Playwright retries the check every 100ms within this window.
Action timeout
Max time for an action (click, fill) to complete. Default: no timeout (uses test timeout). Set with actionTimeout in config.
Navigation timeout
Max time for a page navigation (goto) to complete. Default: 30 seconds.
Override timeout for a specific test or assertion
// Override timeout for one specific test
test('slow report generation', async ({ page }) => {
  test.setTimeout(60000); // give this test 60 seconds
  await page.getByRole('button', { name: 'Generate Report' }).click();
  await expect(page.getByText('Report Ready'))
    .toBeVisible({ timeout: 50000 }); // wait up to 50s for this element
});
🚫
Never use page.waitForTimeout(2000) — This is a hardcoded sleep that makes tests slow and brittle. If the element appears in 500ms, you still waste 1.5 seconds. If it takes 2.1s, the test fails. Always use Playwright's smart assertions and auto-wait instead.
🧪 Quiz: Playwright's default assertion timeout is 5 seconds. What does Playwright actually do during those 5 seconds?
10
Execution
Running Tests & HTML Reports
Playwright provides a powerful CLI (command-line interface) with many options to control how tests are run — which browsers, which test files, headed or headless, debug mode, and more. After every test run, an HTML report is generated showing pass/fail results, error details, screenshots, and videos.

Essential CLI commands:

Run all tests
npx playwright test — runs all .spec.ts files on all configured browsers
Run specific file
npx playwright test login.spec.ts — run only the login tests
Run specific test
npx playwright test -g "valid login" — run only tests whose name contains "valid login"
Run on one browser
npx playwright test --project=chromium — only Chrome
Headed mode
npx playwright test --headed — see the browser window while tests run
Debug mode
npx playwright test --debug — opens Playwright Inspector, step through tests
View report
npx playwright show-report — opens the HTML report in your browser
View trace
npx playwright show-trace trace.zip — opens Trace Viewer for a failed test
📊 What the HTML Report shows:
After running npx playwright test, Playwright generates a detailed HTML report. Open it with npx playwright show-report.

Passed tests — shown in green with execution time
Failed tests — shown in red with full error message, expected vs actual values
📸 Screenshots — automatically captured on failure
🎥 Video recordings — full recording of the test run on failure
🔍 Trace Viewer — step-by-step DOM snapshots + network logs for every action
💡
Trace Viewer is Playwright's superpower for debugging. When a test fails in CI and you can't reproduce locally, attach the trace file. The Trace Viewer shows you: every action taken, a screenshot at each step, the DOM state before and after each action, and all network requests made. It's like having a DVR recording of your test failure.
🧪 Quiz: Which command runs ONLY the tests inside the file named checkout.spec.ts?
11
Productivity Tool
Codegen — Record Tests Automatically
Codegen is Playwright's built-in test recorder. You launch it, perform actions in the browser window (click, type, navigate), and Playwright automatically generates the test code for you in real time. It's the fastest way to create the skeleton of a new test — especially useful when you're new to Playwright.

How to launch Codegen:

npx playwright codegen https://your-app-url.com
🎬 What happens when you run Codegen:
1. Two windows open side by side:
   • Left: A browser window — interact with your app here
   • Right: Playwright Inspector — shows the generated code live

2. As you click, type, and navigate in the browser, code appears on the right automatically.

3. When you're done, copy the generated code into your .spec.ts test file.

4. Important: Codegen gives you a starting point — you still need to add meaningful expect() assertions manually. It records actions but not verifications.
Example of Codegen output — auto-generated code
// Auto-generated by Playwright Codegen
import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('https://your-app.com/login');
  await page.getByLabel('Email').click();
  await page.getByLabel('Email').fill('priya@example.com');
  await page.getByLabel('Password').fill('Test@123');
  await page.getByRole('button', { name: 'Login' }).click();

  // ← Add your assertions here manually after recording:
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Other useful Codegen options:

Specific browser
npx playwright codegen --browser=firefox https://app.com
Mobile emulation
npx playwright codegen --device="iPhone 15" https://app.com
Save to file
npx playwright codegen -o tests/login.spec.ts https://app.com
⚠️
Codegen is a starting point, not a finished test. The generated code captures your actions accurately but has a generic test name ('test') and no assertions. Always: rename the test with a clear description, add meaningful assertions for what you want to verify, and review the generated locators — sometimes Codegen picks CSS selectors when a better getByRole or getByLabel is available.
🧪 Quiz: What is the biggest limitation of Playwright's Codegen tool that you must address after recording?
12
Professional Skills
Playwright Best Practices

These practices are followed by professional QA automation engineers. Applying them consistently is what separates a clean, maintainable test suite from a fragile, hard-to-maintain one.

  1. 1
    Prefer user-facing locators over CSS/XPath Always try getByRole, getByLabel, getByPlaceholder, and getByText first. These locators reflect how real users identify elements and remain stable when developers refactor the UI without changing functionality.
  2. 2
    Never use page.waitForTimeout() — use smart assertions instead Hardcoded sleeps like waitForTimeout(3000) make tests slow and brittle. Use await expect(locator).toBeVisible() which retries automatically and succeeds as soon as the element appears.
  3. 3
    One test = one user scenario Each test should verify one clear behaviour end-to-end. Tests with 30+ steps that test login, profile update, checkout, and logout all in one test are hard to debug and maintain. Split into focused, independent tests.
  4. 4
    Use test.describe() to group related tests and beforeEach() for shared setup If 5 tests all start on the same page, use beforeEach(() => page.goto('/login')) rather than repeating it in every test. Reduces duplication and is easier to update.
  5. 5
    Use meaningful, descriptive test names A test named 'test1' tells you nothing when it fails. Name tests like 'user cannot login with expired password' — the name should describe the scenario and expected outcome so any team member understands what broke.
  6. 6
    Enable screenshots and video on failure in config Set screenshot: 'only-on-failure' and video: 'retain-on-failure' in playwright.config.ts. This provides instant visual evidence when tests fail in CI — no guessing about what went wrong.
  7. 7
    Run tests across all three browsers — Chromium, Firefox, WebKit A test that passes on Chrome may fail on Safari due to browser-specific rendering differences. Cross-browser testing is Playwright's biggest strength — use it. Configure all three projects in playwright.config.ts.
  8. 8
    Always use await — never skip it Forgetting await before a Playwright action is the most common beginner mistake. The action runs, the test moves on before it completes, and you get confusing failures. Every single interaction and assertion must be awaited.
  9. 9
    Test both positive and negative paths For every feature, write tests for the happy path (valid input → success) AND the error paths (invalid input → correct error message shown). Error handling bugs are extremely common and only caught by negative tests.
  10. 10
    Keep tests independent — each test must work alone Tests should never depend on other tests running first. If Test B requires Test A's data, use beforeEach to set up that data fresh. Tests that work only when run in a specific order are a maintenance nightmare.
🎯
Interview-ready answer — "How do you write a Playwright test for a login feature?"

"I start by running Codegen to record the login flow to get a quick skeleton. I then rename the test descriptively, split it into multiple test cases — valid login, wrong password, empty fields. I use getByLabel for form inputs and getByRole for the button. After clicking login, I use toHaveURL to verify the redirect and toBeVisible to confirm the dashboard heading. I add a negative test asserting the error message appears with wrong credentials. I configure beforeEach to navigate to the login page before each test. Finally, I run the suite on Chromium, Firefox, and WebKit to ensure cross-browser compatibility."
🧪 Final Quiz: Your test suite has 10 tests for checkout. Tests 5–10 only work if test 1 (login) runs first and creates session data. What is the correct fix?

Ready to Master Playwright in Real Projects?

STAD Solution's QA Automation course covers Playwright end-to-end with hands-on practice on real applications, CI/CD integration, Page Object Model, and 100% placement support.

Explore Courses at STAD Solution →