This is the post where you stop reading and start doing. If you followed Part 1 (mindset shift) and Part 2 (planning strategy), you understand why to automate and what to automate first. Now we learn how.

I’m assuming you’ve never written code. I’m assuming you’ve never used a command line. I’m assuming the most technical thing you’ve done is write a VLOOKUP in Excel. That’s perfectly fine. By the end of this post, you’ll have a real Playwright test running on your machine.

Step 1: Install the Prerequisites

You need three things. All free, all safe, all standard.

Install Node.js

Node.js is the runtime that executes your test code. Think of it as the engine that makes Playwright work.

  1. Go to nodejs.org
  2. Download the LTS version (the one on the left, marked “Recommended for Most Users”)
  3. Run the installer — click Next through everything, accept the defaults
  4. Verify it worked: open a terminal and type:
node --version

You should see something like v20.11.0. The exact number doesn’t matter.

What’s a terminal?

  • On Windows: Press Win + R, type cmd, press Enter. Or search for “Terminal” in the Start menu.
  • On Mac: Press Cmd + Space, type “Terminal”, press Enter.
  • Inside VS Code: Press Ctrl + ` (backtick key, usually above Tab).

Install VS Code

VS Code is the code editor where you’ll write your tests. It’s like Notepad, but with superpowers.

  1. Go to code.visualstudio.com
  2. Download and install
  3. Open it — you’ll see a welcome screen

Install the Playwright Extension for VS Code

This extension gives you a green “Play” button to run tests, highlights test results, and provides debugging tools.

  1. In VS Code, click the Extensions icon in the left sidebar (it looks like four squares)
  2. Search for “Playwright Test for VS Code”
  3. Click Install on the one by Microsoft

Step 2: Create Your Test Project

Open VS Code’s terminal (Ctrl + `) and run these commands. I’ll explain each one:

mkdir my-first-tests
cd my-first-tests
npm init playwright@latest

What each command does:

  • mkdir my-first-tests — Creates a new folder called “my-first-tests”
  • cd my-first-tests — Enters that folder
  • npm init playwright@latest — Installs Playwright and sets up the project

The installer will ask you a few questions. Choose these:

? Do you want to use TypeScript or JavaScript? → TypeScript
? Where to put your end-to-end tests? → tests
? Add a GitHub Actions workflow? → true
? Install Playwright browsers? → true

Wait while it downloads browsers (Chrome, Firefox, Safari). This takes 1-3 minutes depending on your internet speed.

When it’s done, you’ll see this folder structure:

my-first-tests/
├── tests/
│   └── example.spec.ts     ← Your first test file
├── tests-examples/
│   └── demo-todo-app.spec.ts
├── playwright.config.ts     ← Configuration
├── package.json
└── package-lock.json

Step 3: Run the Example Test

Playwright comes with an example test. Let’s run it to make sure everything works:

npx playwright test

You should see output like:

Running 6 tests using 3 workers
  6 passed (4.2s)

All six passed. You just ran automated browser tests. That’s it. You’re an automation engineer now.

To see the detailed HTML report:

npx playwright show-report

This opens a beautiful report in your browser showing every test, how long it took, and whether it passed.

Step 4: Understand the Example Test

Open tests/example.spec.ts in VS Code. Here’s what’s inside:

import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page).toHaveURL(/.*intro/);
});

Let me translate this to plain English:

Test 1: “has title”

  1. Open the Playwright website
  2. Check that the page title contains the word “Playwright”

Test 2: “get started link”

  1. Open the Playwright website
  2. Find the link that says “Get started” and click it
  3. Check that the URL now contains “intro”

That’s what every automated test looks like:

  1. Navigate somewhere
  2. Interact with the page (click, type, select)
  3. Verify something happened (text appeared, URL changed, element is visible)

Breaking Down the Syntax

Let’s decode each piece:

import { test, expect } from '@playwright/test';

This imports Playwright’s tools. test defines a test case. expect creates assertions (checks).

test('has title', async ({ page }) => {

This creates a test named “has title”. page is the browser page — your window into the website.

await page.goto('https://playwright.dev/');

Navigate to this URL. await means “wait for this to finish before continuing.”

await expect(page).toHaveTitle(/Playwright/);

Verify the page title contains “Playwright”. The /Playwright/ is a pattern match (regular expression) — it checks if the word appears anywhere in the title.

await page.getByRole('link', { name: 'Get started' }).click();

Find a link whose text includes “Get started” and click it.

await expect(page).toHaveURL(/.*intro/);

Verify the URL now contains “intro”.

Step 5: Write Your First Real Test

Let’s write a test for a real website. We’ll test Google’s search page:

// tests/my-first-test.spec.ts
import { test, expect } from '@playwright/test';

test('Google search works', async ({ page }) => {
  // Step 1: Navigate to Google
  await page.goto('https://www.google.com');

  // Step 2: Type a search query
  await page.getByRole('combobox', { name: 'Search' }).fill('Playwright testing');

  // Step 3: Press Enter to search
  await page.keyboard.press('Enter');

  // Step 4: Verify results appear
  await expect(page.getByRole('heading', { name: /Playwright/i })).toBeVisible();
});

Create this file: in VS Code, right-click the tests folder → New File → name it my-first-test.spec.ts → paste the code above.

Run it:

npx playwright test my-first-test.spec.ts

Step 6: Understanding Locators

Locators are how you tell Playwright which element to interact with. Think of them as directions: “Click the button that says Submit” or “Type in the search box.”

Playwright has several locator strategies, ranked from best to worst:

The Best: Role-Based Locators

These find elements by their purpose, not their appearance. They’re the most resilient to UI changes:

// Find a button with the text "Submit"
page.getByRole('button', { name: 'Submit' })

// Find a text input with the label "Email"
page.getByRole('textbox', { name: 'Email' })

// Find a link with the text "Learn more"
page.getByRole('link', { name: 'Learn more' })

// Find a heading with specific text
page.getByRole('heading', { name: 'Welcome' })

// Find a checkbox
page.getByRole('checkbox', { name: 'Remember me' })

Why role-based locators are best: Even if the developer changes the button’s CSS class, its color, or its position on the page, the role and text stay the same. Your test survives UI redesigns.

Text-Based Locators

Find elements by their visible text:

// Find any element containing this exact text
page.getByText('Welcome to our app')

// Find elements containing this text (case-insensitive)
page.getByText('welcome', { exact: false })

Placeholder and Label Locators

For form fields:

// Find input by its placeholder text
page.getByPlaceholder('Enter your email')

// Find input by its associated label
page.getByLabel('Password')

Test ID Locators

For elements that are hard to locate by role or text, developers can add test IDs:

<div data-testid="user-profile">...</div>
page.getByTestId('user-profile')

CSS Selectors (Last Resort)

If nothing else works, use CSS selectors. But these are brittle — they break when CSS classes change:

// ❌ Brittle — breaks if class name changes
page.locator('.btn-primary-submit')

// ❌ Very brittle — breaks if page structure changes
page.locator('#app > div > form > button:nth-child(3)')

Locator Cheat Sheet

What You WantLocatorExample
A buttongetByRole('button', { name: 'X' })Save, Submit, Cancel
A text inputgetByRole('textbox', { name: 'X' })Email field
A linkgetByRole('link', { name: 'X' })Navigation links
A headinggetByRole('heading', { name: 'X' })Page titles
A checkboxgetByRole('checkbox', { name: 'X' })Settings toggles
A dropdowngetByRole('combobox', { name: 'X' })Select menus
Any textgetByText('X')Labels, messages
Form fieldgetByPlaceholder('X')Search boxes
Test IDgetByTestId('X')Complex elements

Step 7: Common Actions

Here are the actions you’ll use most often:

// Go to a URL
await page.goto('https://example.com');

// Go back (browser back button)
await page.goBack();

// Reload the page
await page.reload();

Clicking

// Click a button
await page.getByRole('button', { name: 'Submit' }).click();

// Double-click
await page.getByText('Edit').dblclick();

// Right-click
await page.getByText('Options').click({ button: 'right' });

Typing

// Type into a field (replaces existing content)
await page.getByPlaceholder('Search...').fill('test query');

// Type one character at a time (simulates real typing)
await page.getByPlaceholder('Search...').pressSequentially('test query');

// Clear a field
await page.getByPlaceholder('Search...').clear();

// Press a key
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.press('Escape');

Selecting Options

// Select from a dropdown by visible text
await page.getByRole('combobox', { name: 'Country' }).selectOption('Vietnam');

// Select by value
await page.getByRole('combobox', { name: 'Country' }).selectOption({ value: 'VN' });

Checkboxes and Radio Buttons

// Check a checkbox
await page.getByRole('checkbox', { name: 'I agree' }).check();

// Uncheck
await page.getByRole('checkbox', { name: 'Subscribe' }).uncheck();

// Verify checked state
await expect(page.getByRole('checkbox', { name: 'I agree' })).toBeChecked();

Step 8: Assertions (Verification)

Assertions are the checks that determine if your test passes or fails. They’re the “verify” step in your manual test case.

Visibility Assertions

// Element is visible on the page
await expect(page.getByText('Welcome')).toBeVisible();

// Element is NOT visible
await expect(page.getByText('Error')).not.toBeVisible();

// Element is hidden (exists in DOM but not visible)
await expect(page.getByTestId('modal')).toBeHidden();

Text Assertions

// Element contains specific text
await expect(page.locator('h1')).toContainText('Dashboard');

// Element has exact text
await expect(page.locator('.status')).toHaveText('Active');

URL and Title Assertions

// URL matches a pattern
await expect(page).toHaveURL(/\/dashboard/);

// URL is exact
await expect(page).toHaveURL('https://example.com/dashboard');

// Page title
await expect(page).toHaveTitle('Dashboard - My App');

Count Assertions

// Number of elements
await expect(page.locator('.product-card')).toHaveCount(10);

// At least one element
const count = await page.locator('.search-result').count();
expect(count).toBeGreaterThan(0);

Important: Auto-Waiting

Playwright automatically waits for elements to appear before checking them. You don’t need to add manual waits:

// ❌ Bad — unnecessary manual wait
await page.waitForTimeout(3000);
await expect(page.getByText('Loaded')).toBeVisible();

// ✅ Good — Playwright auto-waits up to 5 seconds
await expect(page.getByText('Loaded')).toBeVisible();

// ✅ If you need a longer wait, increase the timeout
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 10000 });

Step 9: Organize Your Tests

As you write more tests, organize them with test.describe() and test.beforeEach():

import { test, expect } from '@playwright/test';

test.describe('Login Page', () => {
  // This runs before EACH test in this group
  test.beforeEach(async ({ page }) => {
    await page.goto('https://your-app.com/login');
  });

  test('shows login form', async ({ page }) => {
    await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
    await expect(page.getByLabel('Email')).toBeVisible();
    await expect(page.getByLabel('Password')).toBeVisible();
    await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
  });

  test('shows error for invalid credentials', async ({ page }) => {
    await page.getByLabel('Email').fill('wrong@email.com');
    await page.getByLabel('Password').fill('wrongpassword');
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page.getByText('Invalid email or password')).toBeVisible();
  });

  test('redirects to dashboard after successful login', async ({ page }) => {
    await page.getByLabel('Email').fill('valid@email.com');
    await page.getByLabel('Password').fill('correctpassword');
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page).toHaveURL(/\/dashboard/);
  });

  test('shows error for empty form submission', async ({ page }) => {
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page.getByText('Email is required')).toBeVisible();
  });
});

Notice how each test reads like a manual test case:

  • “shows login form” → Open login page, verify all elements are visible
  • “shows error for invalid credentials” → Enter wrong credentials, click sign in, verify error message
  • “redirects to dashboard” → Enter valid credentials, click sign in, verify redirect

Step 10: Running and Debugging Tests

Running Tests

# Run all tests
npx playwright test

# Run a specific test file
npx playwright test tests/my-first-test.spec.ts

# Run tests with a specific name
npx playwright test -g "login"

# Run in headed mode (see the browser)
npx playwright test --headed

# Run in UI mode (best for debugging!)
npx playwright test --ui

UI Mode — Your Best Friend

UI mode is the most powerful debugging tool. Run it:

npx playwright test --ui

This opens a visual interface where you can:

  • See all your tests in a list
  • Click to run individual tests
  • Watch the browser actions in real-time
  • See a timeline of every action
  • Time-travel through test steps
  • Inspect the page at any point

I recommend running --ui mode every time you write or debug a test. It’s the closest thing to manual testing but with automated verification.

When Tests Fail

When a test fails, Playwright tells you exactly what went wrong:

Error: Timed out 5000ms waiting for expect(locator).toBeVisible()

  Locator: getByText('Welcome back')
  Expected: visible
  Received: <element not found>

  Call log:
    - waiting for getByText('Welcome back')

This tells you:

  1. What was expected: The text “Welcome back” should be visible
  2. What actually happened: The text wasn’t found on the page
  3. How long it tried: 5 seconds before giving up

Common causes and fixes:

ErrorLikely CauseFix
”element not found”Text changed or element doesn’t existCheck the actual page text
”Timed out waiting”Page didn’t load fast enoughIncrease timeout or check the URL
”strict mode violation”Multiple elements matchMake the locator more specific
”Navigation timeout”Page URL is wrong or server is downCheck the URL and ensure app is running

Using page.pause() for Debugging

Insert await page.pause() in your test to freeze execution and inspect the page:

test('debug this', async ({ page }) => {
  await page.goto('https://your-app.com');
  await page.pause(); // Execution stops here — you can interact with the page
  await page.getByRole('button', { name: 'Submit' }).click();
});

This opens the Playwright Inspector where you can:

  • Click around the page
  • See locators for any element
  • Step through your test one action at a time

Exercise: Automate a Real Flow

Now it’s your turn. Pick one of these exercises based on your comfort level:

// tests/wikipedia-search.spec.ts
import { test, expect } from '@playwright/test';

test('Wikipedia search returns results', async ({ page }) => {
  await page.goto('https://en.wikipedia.org');

  // Fill the search box
  await page.getByRole('searchbox', { name: 'Search Wikipedia' }).fill('Playwright');

  // Click search button
  await page.getByRole('button', { name: 'Search' }).click();

  // Verify we're on a results page
  await expect(page.locator('h1')).toContainText('Playwright');
});

Intermediate: Test a Login Flow

Pick your application’s login page and write three tests:

  1. Valid login succeeds
  2. Invalid login shows error
  3. Empty form shows validation

Advanced: Test a Multi-Step Flow

Pick a 3-5 step user journey in your application (e.g., search → view → add to cart) and automate it.

What You’ve Learned

In this post, you’ve:

  • ✅ Installed Node.js, VS Code, and Playwright
  • ✅ Created a test project from scratch
  • ✅ Run your first automated tests
  • ✅ Understood test structure (navigate → interact → verify)
  • ✅ Learned locator strategies (role, text, placeholder, test ID)
  • ✅ Written assertions (visibility, text, URL)
  • ✅ Organized tests with describe and beforeEach
  • ✅ Learned to debug with UI mode and page.pause()

That’s a solid foundation. You can already automate basic regression tests.

Series Navigation

In Part 4, we’ll level up your tests with Page Objects, custom fixtures, and real-world patterns that make your test suite scalable and maintainable. No more copy-pasting the same selectors into every test file.

Export for reading

Comments