Before We Start โ What Playwright Fixes
| Pain we used to live with | How Playwright removes it |
|---|---|
| Thread.sleep and hand-written waits | Every action waits until the element is ready to receive it |
| Long, brittle XPath chains | Locators that read like the screen: role, label, text, test id |
| Testing one browser at a time | Chromium, Firefox and WebKit through one API, plus device emulation |
| Assertions that check only once | Assertions that keep retrying until true or timed out |
| Five extra libraries per project | Runner, reports, traces, API client and mocking ship together |
Step A ยท Get a Project Running
- Confirm Node.js 18+ with
node -v(install the LTS build if it is older). - Scaffold:
npm init playwright@latestโ pick TypeScript, name the test folderspecs, say yes to the GitHub Actions file if you use GitHub. - Pull the browsers:
npx playwright install(on Linux CI add--with-deps). - In VS Code, add the official Playwright Test extension to run, debug and pick locators from the editor.
- Smoke-check the setup:
npx playwright testโnpx playwright show-report.
AutomateWithUs suite layout
awu-pw-suite/
โโโ playwright.config.ts
โโโ package.json
โโโ specs/
โ โโโ web/
โ โ โโโ sign-in.spec.ts
โ โ โโโ enrolment.spec.ts
โ โโโ service/
โ โโโ courses.api.spec.ts
โโโ screens/ # screen objects (POM)
โ โโโ SignInScreen.ts
โ โโโ CatalogScreen.ts
โโโ kit/
โ โโโ awu-test.ts # extended test with custom fixtures
โโโ fixtures/
โ โโโ learners.json # test data
โโโ support/
โโโ formatters.tsStep B ยท Anatomy of a Test โ Arrange, Act, Assert
import { test, expect } from '@playwright/test';
test('learner can add a course to cart', async ({ page }) => {
// Arrange
await page.goto('/courses');
// Act
await page.getByRole('link', { name: 'API Automation' }).click();
await page.getByRole('button', { name: 'Add to cart' }).click();
// Assert
await expect(page.getByTestId('cart-count')).toHaveText('1');
});Every Playwright call that touches the browser returns a Promise. Missing `await` is the #1 reason beginner tests pass locally and fail randomly.
Step C ยท Finding Elements โ the AWU Locator Ladder
| Rung | Locator | AWU example | Fits best |
|---|---|---|---|
| 1 | getByRole() | page.getByRole('button', { name: 'Pay now' }) | Buttons, links, headings, checkboxes |
| 2 | getByLabel() | page.getByLabel('Mobile number') | Form fields with labels |
| 3 | getByPlaceholder() | page.getByPlaceholder('Search courses') | Inputs without labels |
| 4 | getByText() | page.getByText('Order confirmed') | Non-interactive visible text |
| 5 | getByAltText() / getByTitle() | page.getByAltText('AWU logo') | Images, tooltips |
| 6 | getByTestId() | page.getByTestId('cart-count') | Stable dev-added hooks |
| 7 | locator() CSS / XPath | page.locator('#coupon-code') | Last resort |
Narrowing and chaining
// Filter rows by text, then act inside the matching row
await page.getByRole('row')
.filter({ hasText: 'Playwright Batch' })
.getByRole('button', { name: 'Enroll' })
.click();
// Position (use only when order is guaranteed)
await page.locator('.course-card').nth(2).click();
await page.locator('.course-card').last().click();
// Scope inside a section
const sidebar = page.getByRole('navigation', { name: 'Learner menu' });
await sidebar.getByRole('link', { name: 'My certificates' }).click();Configure a custom test id attribute once in config โ use: { testIdAttribute: 'data-qa' } โ and getByTestId() will look for data-qa.
Step D ยท Interacting โ Grouped by Intent
| Intent | What to call |
|---|---|
| Navigate | await page.goto('/batches') ยท page.goBack() ยท page.reload() |
| Mouse | locator.click() ยท dblclick() ยท click({ button: 'right' }) ยท hover() |
| Keyboard | locator.press('Tab') ยท pressSequentially('400708', { delay: 60 }) |
| Text fields | locator.fill('priya.k@awu.test') (clears, then sets the value) |
| Choices | locator.check() ยท uncheck() ยท selectOption({ label: 'Navi Mumbai' }) |
| Files | locator.setInputFiles('fixtures/id-proof.pdf') |
| Move things | sourceCard.dragTo(doneColumn) |
| Evidence | page.screenshot({ path: 'evidence/learner-home.png', fullPage: true }) |
Step E ยท Proving It โ Assertions That Retry
An expect on a locator or page keeps re-checking until it passes or expect.timeout (5 s by default) runs out โ no manual waits needed.
| What you want to prove | Assertion |
|---|---|
| Right page | expect(page).toHaveURL(/learner-home/) ยท toHaveTitle(/AutomateWithUs/) |
| Shown or gone | toBeVisible() ยท toBeHidden() |
| Wording | toHaveText('Enrolled') ยท toContainText('Batch') |
| Field content | toHaveValue('400708') |
| State | toBeEnabled() ยท toBeChecked() |
| How many | toHaveCount(3) |
| Attributes / style | toHaveAttribute('aria-expanded', 'true') ยท toHaveCSS('background-color', 'rgb(124, 181, 24)') |
| Keep going after a miss | expect.soft(locator).toHaveText('โฆ') |
expect(await locator.textContent()).toBe('Enrolled') does not retry โ it reads once. Prefer await expect(locator).toHaveText('Enrolled').
Step F ยท Screen Objects (POM) in TypeScript
// screens/SignInScreen.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class SignInScreen {
readonly learnerEmail: Locator;
readonly secretKey: Locator;
readonly continueBtn: Locator;
readonly alertBanner: Locator;
constructor(private readonly page: Page) {
this.learnerEmail = page.getByLabel('Learner email');
this.secretKey = page.getByLabel('Account password');
this.continueBtn = page.getByRole('button', { name: 'Continue to portal' });
this.alertBanner = page.getByRole('alert');
}
async visit() {
await this.page.goto('/sign-in');
}
async enterPortal(email: string, secret: string) {
await this.learnerEmail.fill(email);
await this.secretKey.fill(secret);
await this.continueBtn.click();
}
async expectAlert(text: string) {
await expect(this.alertBanner).toContainText(text);
}
}import { test } from '@playwright/test';
import { SignInScreen } from '../../screens/SignInScreen';
test('wrong secret shows the alert banner', async ({ page }) => {
const signIn = new SignInScreen(page);
await signIn.visit();
await signIn.enterPortal('priya.k@awu.test', 'not-my-secret');
await signIn.expectAlert('Email or password is incorrect');
});Step G ยท Hooks & Keeping the Session
| Hook | Fires |
|---|---|
test.describe() | Not a hook โ groups specs so hooks can be scoped |
test.beforeEach() | Before each test in the group (most used) |
test.afterEach() | After each test โ attach evidence, clean data |
test.beforeAll() | Once per worker, before the group starts |
test.afterAll() | Once per worker, after the group ends |
// Save the signed-in session
await page.context().storageState({ path: '.awu-session/learner-state.json' });
// Reuse it from config
use: { storageState: '.awu-session/learner-state.json' }Step H ยท Owning the Network
Call an API inside a test
test('API: course catalogue is not empty', async ({ request }) => {
const res = await request.get('/api/courses');
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.length).toBeGreaterThan(0);
});Fake a response to test UI edge cases
test('shows empty-state when no courses', async ({ page }) => {
await page.route('**/api/courses', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }),
);
await page.goto('/courses');
await expect(page.getByText('No courses yet')).toBeVisible();
});
test('shows error banner on 500', async ({ page }) => {
await page.route('**/api/courses', (route) => route.fulfill({ status: 500 }));
await page.goto('/courses');
await expect(page.getByRole('alert')).toContainText('Something went wrong');
});Mocking lets you test states the real backend rarely produces โ empty lists, slow responses, server errors โ without touching test data.
Other built-in powers
- New tabs:
const receipt = await page.waitForEvent('popup')around the click that opens it. - Downloads:
const file = await page.waitForEvent('download')thenfile.saveAs(โฆ). - iFrames:
page.frameLocator('#upi-widget').getByLabel('UPI ID'). - Browser dialogs:
page.on('dialog', d => d.accept()). - Geolocation, permissions, mobile devices, video recording, trace viewer.
Step I ยท Playwright MCP โ AI Meets the Browser
@playwright/mcp is Microsoft's MCP server that lets AI assistants (Claude, Copilot, Cursor and others) drive a real browser through the page's accessibility tree rather than screenshots. Useful for exploring an app, generating first-draft tests and quick checks.
{
"mcpServers": {
"playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] }
}
}AI-generated tests are a starting point. Review locators, add real assertions and remove hard-coded data before committing.
Step J ยท Everyday Commands
| Command | Purpose |
|---|---|
npx playwright test | Run every spec |
npx playwright test enrolment | Run files matching "enrolment" |
npx playwright test --headed | Watch the browser |
npx playwright test --project=desktop-chrome | One project |
npx playwright test --grep "@sanity" | Filter by tag/title |
npx playwright test --ui | Interactive UI Mode |
npx playwright test --debug | Step through with Inspector |
npx playwright codegen https://โฆ | Record a test |
npx playwright show-report | Open HTML report |
npx playwright show-trace trace.zip | Open a trace |
Interview Questions & Answers
Q1. What is auto-waiting?
Before an action Playwright waits until the element is attached, visible, stable, enabled and able to receive events.
Q2. Why prefer getByRole?
It matches how users and assistive tech see the page, so it survives CSS/DOM refactors.
Q3. fill() vs pressSequentially()?
fill() sets the value at once; pressSequentially() types key by key โ needed when the app reacts to each keystroke.
Q4. What is a web-first assertion?
An expect on a locator/page that retries until the condition holds or times out.
Q5. locator vs elementHandle?
Locators are lazy and re-query the DOM each time (no stale elements); handles point to one fixed node โ avoid them.
Q6. How do you handle a new tab?
const p = await page.waitForEvent('popup') (or context.waitForEvent('page')) around the click that opens it.
Q7. How do you test a 500 error screen?
Mock the API with page.route() and route.fulfill({ status: 500 }).
Q8. What is storageState?
A JSON snapshot of cookies and localStorage used to start tests already signed in.
Q9. Context vs page?
A context is an isolated browser profile; a page is a tab inside it.
Q10. What is a trace?
A recording of actions, DOM snapshots, network and console you open in Trace Viewer to debug failures.
Want to learn this live?
Small batches, real projects, mock interviews included. Sit in on a free demo class first.
See all coursesBook a mock interview