Skip to main content

Playwright Concepts

Before recording or writing tests, learn the handful of building blocks every Playwright test is made of. This is the JMeter Elements Overview equivalent for UI automation.

The Shape of a Test​

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

test('valid user lands on the products page', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();

await expect(page).toHaveURL(/inventory\.html/);
});

Read it top to bottom:

PieceWhat it is
test(name, fn)Declares one test. The name shows up in reports
{ page }A fixture. Playwright hands you a fresh browser tab for this test
page.goto()Navigation
page.getByRole(...)A locator: how you find an element
.fill(), .click()Actions on the located element
expect(...)An assertion: what must be true
await everywhereEvery Playwright call is asynchronous. Forgetting await is the number one bug

The Building Blocks​

Browser, Context, Page​

Browser (Chromium process)
└── BrowserContext (an incognito-like session: its own cookies and storage)
└── Page (one tab)

Every test gets its own context and page. That is why tests are isolated: nothing leaks between them, and they can run in parallel. You rarely create these yourself. The page fixture does it for you.

Locators​

A locator is a recipe for finding an element, not the element itself. It is resolved fresh every time you act on it, and Playwright waits for the element to be ready before acting.

const loginButton = page.getByRole('button', { name: 'Login' });
await loginButton.click(); // waits for the button to be visible, enabled and stable

This built-in waiting ("auto-wait") is why Playwright tests need almost no sleep calls. Section 5 covers how to pick good locators.

Actions​

What you do to a located element: click(), fill(), check(), selectOption(), press(), hover(), setInputFiles(). Each one performs actionability checks first (visible, enabled, not covered, stable) and retries until they pass or the timeout hits.

Assertions​

expect() with a locator gives you web-first assertions that retry until they pass:

await expect(page.getByText('Products')).toBeVisible();
await expect(page.getByTestId('cart-badge')).toHaveText('1');
await expect(page).toHaveURL(/inventory/);

Compare with a plain check that runs once and fails if the page is a few milliseconds behind:

// Avoid: no retry, flaky by design
expect(await page.getByText('Products').isVisible()).toBe(true);

Fixtures​

Fixtures are things a test asks for by name in its argument list. Built-in ones:

FixtureGives you
pageA fresh tab in a fresh context
contextThe browser context (for cookies, storage state, new tabs)
browserThe shared browser instance
requestAn API client for calling endpoints directly
browserName'chromium', 'firefox' or 'webkit'

You can define your own, for example a loginPage fixture that returns a ready page object. Section 6 shows how.

Hooks and Groups​

test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test('valid user', async ({ page }) => { /* ... */ });
test('locked out user', async ({ page }) => { /* ... */ });
});

describe groups tests, beforeEach runs before every test in the group. Use hooks for navigation and setup, not for assertions.

Configuration and Projects​

playwright.config.ts holds everything that is not test logic: where tests live, timeouts, retries, reporters, the base URL, and projects. A project is a named combination of browser and settings. The same tests can run under a chromium project and a Mobile Safari project without changing the code. Section 8 goes through the file line by line.


Project Layout​

The layout the sample project uses, which is also what I recommend for a new suite:

playwright/sample/
├── playwright.config.ts ← runner config
├── package.json
├── tests/
│ ├── auth.setup.ts ← logs in once, saves browser state
│ ├── fixtures.ts ← custom fixtures (page objects)
│ ├── login.spec.ts ← one spec file per flow or screen
│ ├── cart.spec.ts
│ ├── todo.spec.ts
│ ├── pages/ ← page object classes
│ │ ├── LoginPage.ts
│ │ └── InventoryPage.ts
│ └── data/
│ └── users.json ← test accounts
├── playwright/.auth/ ← saved login state (gitignored)
├── playwright-report/ ← HTML report (gitignored)
└── test-results/ ← traces, screenshots (gitignored)

Test files end in .spec.ts. Setup files end in .setup.ts. Everything else is plain TypeScript.


How It Maps to JMeter​

If you come from the JMeter track, this table is the quickest way in:

JMeterPlaywrightNotes
Test Planplaywright.config.tsGlobal settings
Thread GroupProject + workersParallelism is per worker process, not threads
HTTP SamplerAction on a locatorPlaywright drives the browser, the browser sends the requests
Transaction Controllertest.describe or test.stepGrouping in reports
Assertionexpect()Web-first assertions retry
Extractor / correlationNot neededThe browser handles tokens and cookies itself
View Results TreeTrace Viewer / UI ModeSection 7
CSV Data Set Configtests/data/*.json + fixturesSection 6

The big difference: Playwright is for functional correctness with one or a few users. It is not a load tool. For load, stay with JMeter or k6.


Tips​

  • Everything is await. If a line looks like it did nothing, check for a missing await first.

  • Locators are lazy. Creating one does not touch the page. Only actions and assertions do.

  • Do not fight the fixtures. Reaching for browser.newPage() in a normal test is almost always wrong. Ask for page and let the runner manage isolation.