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:
| Piece | What 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 everywhere | Every 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:
| Fixture | Gives you |
|---|---|
page | A fresh tab in a fresh context |
context | The browser context (for cookies, storage state, new tabs) |
browser | The shared browser instance |
request | An 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:
| JMeter | Playwright | Notes |
|---|---|---|
| Test Plan | playwright.config.ts | Global settings |
| Thread Group | Project + workers | Parallelism is per worker process, not threads |
| HTTP Sampler | Action on a locator | Playwright drives the browser, the browser sends the requests |
| Transaction Controller | test.describe or test.step | Grouping in reports |
| Assertion | expect() | Web-first assertions retry |
| Extractor / correlation | Not needed | The browser handles tokens and cookies itself |
| View Results Tree | Trace Viewer / UI Mode | Section 7 |
| CSV Data Set Config | tests/data/*.json + fixtures | Section 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 missingawaitfirst. -
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 forpageand let the runner manage isolation.