GUIDE / automation
Playwright Tutorial: End-to-End Testing from Scratch
Playwright tutorial for beginners: install, write your first test, TypeScript setup, codegen, fixtures, API testing, debugging, and CI tips in one guide.
If you need a practical Playwright tutorial that takes you from zero to useful end-to-end tests, start with setup, one solid first test, locators, assertions, debugging, and then the features that make Playwright productive: codegen, fixtures, API setup, and CI. Playwright is a modern framework for reliable web automation across Chromium, Firefox, and WebKit, with strong first-party tooling for traces, reports, and parallel runs.
This guide is written for beginners and manual testers moving into automation. It uses TypeScript oriented examples, explains commands you will actually run, and shows patterns that keep suites maintainable. For tool choice context, see Selenium vs Playwright vs Cypress.
Playwright Tutorial Roadmap
This Playwright tutorial is ordered so each step unlocks the next. Do not skip installation and your first green test. Debugging, fixtures, and CI only make sense after you can run one intentional check locally.
What You Will Build
By the end of this tutorial path, you will know how to:
- Scaffold a Playwright project.
- Run tests on multiple browsers.
- Write a first UI test with stable locators.
- Use codegen without shipping messy scripts.
- Add assertions that fail for the right reasons.
- Use fixtures for authenticated state.
- Mix API setup with UI verification.
- Debug with UI mode, trace viewer, and screenshots.
- Organize a small smoke suite for CI.
If you are still choosing your first automation stack, compare the learning path in Playwright vs Selenium for beginners before you commit.
Prerequisites
- Node.js LTS installed (18+ or current LTS recommended).
- Basic command line comfort.
- A target app: local dev app, staging, or a public demo site.
- Optional: TypeScript basics help, but you can learn as you go.
If you still think in manual cases first, keep that strength. Automation should encode clear intent from good test cases, not random clicks.
Step 1: Install Playwright
Create a folder and scaffold:
mkdir playwright-demo
cd playwright-demo
npm init playwright@latest
The init wizard asks questions such as:
- TypeScript or JavaScript
- Tests folder name
- GitHub Actions workflow
- Install browsers
For most beginners, choose:
- TypeScript: yes
- Tests folder:
tests - GitHub Actions: yes if you want CI early
- Install browsers: yes
Manual equivalent pieces after scaffold:
npm i -D @playwright/test
npx playwright install
npx playwright install downloads browser binaries Playwright controls.
Verify Installation
npx playwright test
You should see example tests run and an HTML report option at the end.
Open the report:
npx playwright show-report
Step 2: Project Shape You Should Understand
A typical scaffold includes:
playwright.config.ts
package.json
tests/example.spec.ts
tests-examples/ (sometimes)
Key Config Ideas in playwright.config.ts
You will commonly set:
testDir: where tests livetimeout: per test timeoutretries: CI retries policyuse.baseURL: app originuse.trace: when to collect tracesprojects: browser matrix
Example config sketch:
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [["list"], ["html", { open: "never" }]],
use: {
baseURL: "http://127.0.0.1:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
});
Start with one browser locally if you want faster feedback, then enable the matrix in CI.
Step 3: Write Your First Playwright Test
Create tests/login.spec.ts:
import { test, expect } from "@playwright/test";
test("user can open login page and see form", async ({ page }) => {
await page.goto("/login");
await expect(page.getByRole("heading", { name: /sign in/i })).toBeVisible();
await expect(page.getByLabel(/email/i)).toBeVisible();
await expect(page.getByLabel(/password/i)).toBeVisible();
await expect(page.getByRole("button", { name: /sign in|log in/i })).toBeEnabled();
});
Run one file:
npx playwright test tests/login.spec.ts
Run headed for visibility:
npx playwright test tests/login.spec.ts --headed
Run one browser project:
npx playwright test tests/login.spec.ts --project=chromium
First Test Design Rules
- Assert something meaningful, not only that a page loaded.
- Prefer role and label locators over brittle CSS when possible.
- Keep one main behavior per test.
- Name tests as behavior statements.
A weak test name:
test("test1", async ({ page }) => {});
A strong test name:
test("shows validation when email is empty", async ({ page }) => {});
Step 4: Locators That Stay Stable
Playwright locators are lazy and auto-waiting. You usually do not need manual sleeps.
Preferred Locator Order
getByRolegetByLabelgetByPlaceholderwhen accessible names are weakgetByTextfor unique visible textgetByTestIdwhen the app provides test ids- CSS/XPath as last resort
Examples:
page.getByRole("button", { name: "Create project" });
page.getByLabel("Email");
page.getByTestId("checkout-pay");
page.locator("#legacy-id"); // avoid if you can
Waiting the Playwright Way
Prefer:
await expect(page.getByRole("alert")).toContainText("Saved");
await page.getByRole("button", { name: "Save" }).click();
Avoid:
await page.waitForTimeout(5000);
Sleeps hide race conditions and slow the suite.
Step 5: A Realistic First Journey Test
Assume a demo app with login and dashboard.
import { test, expect } from "@playwright/test";
test("registered user can log in and reach dashboard", async ({ page }) => {
await page.goto("/login");
await page.getByLabel(/email/i).fill("qa.user@example.com");
await page.getByLabel(/password/i).fill("ValidPass#2026");
await page.getByRole("button", { name: /log in|sign in/i }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible();
});
Negative Test Example
test("login rejects invalid password with safe error", async ({ page }) => {
await page.goto("/login");
await page.getByLabel(/email/i).fill("qa.user@example.com");
await page.getByLabel(/password/i).fill("WrongPass#1");
await page.getByRole("button", { name: /log in|sign in/i }).click();
await expect(page.getByRole("alert")).toContainText(/invalid|incorrect/i);
await expect(page).toHaveURL(/login/);
});
Negative paths matter as much as happy paths. Automation should not only celebrate success screens.
Step 6: Playwright Codegen for Beginners
Codegen records actions and prints code.
npx playwright codegen http://127.0.0.1:3000/login
Use it to:
- Discover locator candidates quickly.
- Learn API surface by reading generated calls.
- Draft long forms you do not want to type by hand.
Codegen Cleanup Checklist
Generated code is a draft, not a final suite.
- Replace fragile selectors with roles/labels/test ids.
- Remove unnecessary clicks and waits.
- Add assertions after important steps.
- Extract repeated login into fixtures or helpers.
- Split giant recordings into focused tests.
- Delete absolute timeouts codegen may encourage indirectly by structure.
Codegen is a bicycle with training wheels. Ride it, then graduate.
Step 7: Assertions Worth Writing
Playwright's expect is integrated and auto-retrying for many matchers.
Common assertions:
await expect(page).toHaveURL(/projects/);
await expect(page.getByRole("heading", { name: "Projects" })).toBeVisible();
await expect(page.getByRole("row")).toHaveCount(3);
await expect(page.getByLabel("Name")).toHaveValue("Alpha");
await expect(page.getByRole("button", { name: "Save" })).toBeDisabled();
Assertion Quality
Weak:
await expect(page.locator("body")).toBeVisible();
Strong:
await expect(page.getByRole("status")).toHaveText("Project created");
await expect(page.getByRole("link", { name: "Alpha" })).toBeVisible();
Strong assertions encode product oracles. Weak assertions only prove the browser did not crash immediately.
Step 8: Playwright Fixtures (and Fixtures vs Hooks)
Fixtures are one of Playwright's best productivity features.
Built-in Fixtures
page: a single tabcontext: browser contextbrowserrequest: API clientbaseURLindirectly via configuse
Classic Hooks vs Fixtures
| Approach | Strength | Weakness |
|---|---|---|
beforeEach hooks | Familiar, simple | Can become global spaghetti |
| Fixtures | Composable, typed, reusable, scoped | Small learning curve |
Custom Fixture Example: Authenticated Page
tests/fixtures.ts:
import { test as base, expect } from "@playwright/test";
type MyFixtures = {
loggedInPage: import("@playwright/test").Page;
};
export const test = base.extend<MyFixtures>({
loggedInPage: async ({ page }, use) => {
await page.goto("/login");
await page.getByLabel(/email/i).fill(process.env.QA_EMAIL!);
await page.getByLabel(/password/i).fill(process.env.QA_PASSWORD!);
await page.getByRole("button", { name: /log in|sign in/i }).click();
await expect(page).toHaveURL(/dashboard/);
await use(page);
},
});
export { expect };
Use it:
import { test, expect } from "./fixtures";
test("dashboard shows recent projects module", async ({ loggedInPage }) => {
await expect(loggedInPage.getByRole("heading", { name: /recent projects/i })).toBeVisible();
});
Storage State Pattern (Faster Auth)
For larger suites, log in once, save storage state, reuse it.
# conceptual flow
# 1) global setup logs in
# 2) saves auth.json
# 3) config use.storageState = 'auth.json'
This reduces repetitive UI login cost. Refresh state when sessions expire.
Step 9: Does Playwright Support API Testing? Yes
Playwright can send HTTP requests with request.
import { test, expect } from "@playwright/test";
test("API health returns ok", async ({ request }) => {
const response = await request.get("/api/health");
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.status).toBe("ok");
});
Mixed API + UI Pattern
test("user sees project created via API", async ({ page, request }) => {
const create = await request.post("/api/projects", {
data: { name: `Proj ${Date.now()}` },
});
expect(create.ok()).toBeTruthy();
const project = await create.json();
await page.goto("/projects");
await expect(page.getByRole("link", { name: project.name })).toBeVisible();
});
Why this matters:
- Faster, more reliable setup than clicking through every precondition.
- UI test focuses on user visible behavior.
- API assertions catch backend contract issues earlier.
Do not replace all UI coverage with API checks. Use each layer for what it validates best.
Step 10: Page Object Style Without Overengineering
Small helper classes can reduce duplication.
import { Page, expect } from "@playwright/test";
export class LoginPage {
constructor(private readonly page: Page) {}
async open() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel(/email/i).fill(email);
await this.page.getByLabel(/password/i).fill(password);
await this.page.getByRole("button", { name: /log in|sign in/i }).click();
}
async expectError(message: RegExp) {
await expect(this.page.getByRole("alert")).toContainText(message);
}
}
Guidelines:
- Keep methods user oriented (
login,createProject). - Do not hide assertions in every method blindly.
- Avoid deep inheritance trees.
- Prefer fixtures composing page objects for setup.
Step 11: Debugging Like a Professional
UI Mode
npx playwright test --ui
UI mode helps you watch steps, locate elements, and time travel through the run.
Debug Flag
npx playwright test tests/login.spec.ts --debug
Trace Viewer
When traces are captured:
npx playwright show-trace trace.zip
Traces show DOM snapshots, network, actions, and timing. They are often better than "works on my machine" debates.
Screenshots and Video
Configured failures can keep screenshots/videos. Use them in CI artifacts.
Local Troubleshooting Checklist
- Run headed and watch.
- Confirm baseURL and environment.
- Confirm test data still exists.
- Check if the locator matches multiple nodes.
- Open trace on failure.
- Verify the app bug is not a real regression.
When you find a product defect, file it with the same rigor as manual testing using a solid bug report habit.
Step 12: Organize Suites by Purpose
Not every test should run on every pull request.
| Suite | Purpose | Size target |
|---|---|---|
| Smoke | Build confidence | Tiny, stable |
| Critical journey | Revenue or mission paths | Small |
| Full regression | Broad confidence | Larger, maybe nightly |
| API pack | Contract and setup validation | Fast |
Tagging example:
test("checkout paid path @smoke", async ({ page }) => {
// ...
});
Run smoke:
npx playwright test --grep @smoke
This maps cleanly to smoke vs regression strategy.
Step 13: Test Data and Isolation
Flaky suites often share users, projects, or mutable global state.
Better patterns:
- Create unique names with timestamps or UUIDs.
- Prefer API seeding per test or per worker.
- Clean up when cost is low, or use disposable tenants.
- Never rely on "whatever is first in the list" without unique identifiers.
- Keep secrets in env vars, not committed files.
const name = `billing-${crypto.randomUUID()}`;
Step 14: CI Basics with Playwright
If you accepted the GitHub Actions scaffold, you already have a starting workflow. Core CI needs:
- Install dependencies
- Install Playwright browsers with deps
- Run tests
- Upload report/trace artifacts on failure
Conceptual steps:
npm ci
npx playwright install --with-deps
npx playwright test
CI tips:
- Use
retries: 1carefully; investigate flakes, do not normalize them. - Fail
test.onlyin CI withforbidOnly. - Cache intelligently, but do not cache in ways that break browser installs.
- Start with chromium on PR, broader matrix nightly if needed.
- Publish HTML report artifacts.
Step 15: Common Playwright Beginner Mistakes
Mistake 1: Copying Codegen Blindly
Unedited recordings become unmaintainable.
Mistake 2: Overusing CSS Selectors Tied to Layout
Class names change with CSS refactors. Roles and test ids survive better.
Mistake 3: Giant End to End Tests That Do Everything
A 40 step test that signs up, updates profile, pays, and exports CSV fails opaquely. Split by risk and purpose.
Mistake 4: No Assertions
A test that only clicks can pass while the product is wrong.
Mistake 5: Sleeps Everywhere
If you need sleeps, prefer event based waits and fix app testability when needed.
Mistake 6: Testing Against Production Only
Use safe environments. If production checks exist, keep them read only and carefully governed.
Mistake 7: Ignoring WebKit Until Users File Safari Bugs
Run WebKit where Safari risk is real.
Mistake 8: Automating Unstable UX Still Under Heavy Design Churn
Wait for minimum stability or automate below the UI for changing rules.
Playwright TypeScript Getting Started Tips
TypeScript pays off quickly:
- Config autocompletion
- Fixture typing
- Safer refactors
If TS errors intimidate you:
- Keep
stricton if possible, but learn incrementally. - Start from official scaffold types.
- Avoid
anyfor page helpers if you can. - Read the type error bottom line first.
JavaScript remains valid. Consistency matters more than purity.
Practice Path for the Next Seven Days
Day 1: Install, run examples, change baseURL.
Day 2: Write two smoke tests for your app.
Day 3: Add one negative validation test.
Day 4: Use codegen, then rewrite locators cleanly.
Day 5: Add a custom fixture for auth.
Day 6: Seed one entity via API, assert in UI.
Day 7: Run in CI and store artifacts.
For testing judgment practice under constraints, warm up in a QABattle battle, write the manual case, then automate only the highest value path in Playwright.
Sample Mini Suite You Can Imitate
import { test, expect } from "@playwright/test";
test.describe("Projects", () => {
test("creates a project from empty state @smoke", async ({ page }) => {
await page.goto("/projects");
await page.getByRole("button", { name: /new project/i }).click();
const name = `Demo ${Date.now()}`;
await page.getByLabel(/name/i).fill(name);
await page.getByRole("button", { name: /create/i }).click();
await expect(page.getByRole("heading", { name })).toBeVisible();
});
test("shows validation when name is empty", async ({ page }) => {
await page.goto("/projects");
await page.getByRole("button", { name: /new project/i }).click();
await page.getByRole("button", { name: /create/i }).click();
await expect(page.getByText(/name is required/i)).toBeVisible();
});
});
This suite demonstrates naming, smoke tagging, uniqueness, and a negative path.
Handling Common UI Patterns in Playwright
File Upload
await page.getByLabel(/upload/i).setInputFiles("tests/fixtures/sample.pdf");
await expect(page.getByText("sample.pdf")).toBeVisible();
Download
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByRole("button", { name: /export/i }).click(),
]);
const path = await download.path();
expect(path).toBeTruthy();
Dialogs
page.once("dialog", async (dialog) => {
expect(dialog.message()).toMatch(/delete/i);
await dialog.accept();
});
await page.getByRole("button", { name: /delete/i }).click();
iFrames
const frame = page.frameLocator('iframe[title="Billing"]');
await frame.getByLabel(/card number/i).fill("4242424242424242");
Multiple Pages or Tabs
const [newPage] = await Promise.all([
page.context().waitForEvent("page"),
page.getByRole("link", { name: /open statement/i }).click(),
]);
await newPage.waitForLoadState();
await expect(newPage.getByRole("heading", { name: /statement/i })).toBeVisible();
Learn these patterns only when your product needs them. Do not pad beginner suites with every API demo.
Network and Timing Tools (Use Carefully)
You can route network calls for deterministic tests:
await page.route("**/api/projects", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: "1", name: "Stubbed" }]),
});
});
Use stubs for unit-like UI tests. Prefer real backend integration for true end-to-end confidence. Over-mocking creates green tests that miss contract breaks.
Environment Variables and Secrets
export QA_EMAIL="qa.user@example.com"
export QA_PASSWORD="ValidPass#2026"
export BASE_URL="https://staging.example.com"
In config:
use: {
baseURL: process.env.BASE_URL || "http://127.0.0.1:3000",
}
Never commit real passwords. Use CI secrets and local .env files ignored by git.
Parallelism and Workers
Playwright runs tests in parallel by default in many setups. That is great until tests share one user account.
Rules for parallel safety:
- Unique data per test
- No shared mutable admin settings
- Avoid depending on sort order of global lists
- Use worker-scoped fixtures for expensive setup when needed
If everything is entangled, start with workers: 1, fix isolation, then scale up.
How Playwright Fits a Wider Quality System
Playwright is powerful, but it is not the whole strategy.
Pair it with:
- Unit tests for pure logic
- API tests for contracts
- Accessibility checks
- Exploratory sessions for unknown risks
- Clear manual cases for judgment heavy areas
Automation should shorten feedback loops and protect known value, not replace thinking.
Final Practical Workflow
When automating a new feature:
- Write or refine the manual scenario and oracle.
- Decide if the check belongs in smoke, critical, or deep regression.
- Prefer API setup when UI setup is long and irrelevant.
- Choose stable locators with accessibility in mind.
- Write focused tests with strong assertions.
- Run locally headed once, then headless.
- Inspect failures with trace/UI mode.
- Add to CI with appropriate tags.
- Watch flake rate for a week.
- Refactor helpers only after duplication is real.
If you remember one lesson from this Playwright tutorial, remember this: reliable tests come from clear intent, stable locators, good data isolation, and honest assertions. Playwright gives you excellent tooling for those habits. The habits still have to be yours.
Install it today, write one meaningful test against a real flow, and make that test earn its place in smoke. That single green, trustworthy check is a better start than twenty fragile recordings.
Quick Command Cheat Sheet
npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test tests/login.spec.ts --headed
npx playwright test --project=chromium
npx playwright test --ui
npx playwright test --debug
npx playwright test --grep @smoke
npx playwright codegen http://127.0.0.1:3000
npx playwright show-report
npx playwright show-trace trace.zip
Keep this list nearby for your first week. Muscle memory around these commands matters as much as memorizing API method names.
FAQ
Questions testers ask
How do I get started with Playwright testing?
Install Node.js, scaffold a project with the Playwright init command, install browsers, then run the example tests. Next, replace examples with tests against your app using stable locators, assertions, and a local or staging base URL. Read failures with the HTML report and trace viewer.
How do you write your first Playwright test?
Create a test file, import test and expect from @playwright/test, open a page with page.goto, perform actions with locators, and assert outcomes with expect. Start with one user journey such as login or form submit. Run it with npx playwright test and iterate on locators until stable.
Does Playwright support API testing?
Yes. Playwright includes an APIRequestContext for HTTP requests, so you can set up data via API, assert responses, and mix API setup with UI checks in the same project. Many teams use API calls to create state quickly, then validate the UI path that users see.
What is Playwright codegen?
Codegen records your browser interactions and generates Playwright script stubs. It is excellent for learning locators and accelerating first drafts. Always clean up generated code: improve locator quality, add assertions, remove noise, and fit the test into your fixtures and folder structure.
What are Playwright fixtures?
Fixtures are Playwright's dependency injection system for tests. Built-in fixtures include page, context, and request. Custom fixtures can provide authenticated pages, test data, or API helpers. Compared with classic hooks, fixtures are more composable, scoped, and reusable across files.
Should beginners use TypeScript with Playwright?
Yes if you can. Playwright TypeScript getting started is smooth because the official scaffold supports it well, and types improve editor help for locators, assertions, and config. JavaScript is fine too. Pick one and stay consistent in the project.
RELATED GUIDES
Continue the route
Selenium vs Playwright vs Cypress in 2026
Compare Selenium vs Playwright vs Cypress in 2026 for speed, browsers, CI, flakiness, ecosystem, and which web automation framework beginners should choose.
How to Write Test Cases: Complete Guide with Examples
Learn how to write test cases with practical steps, examples, a QA template, common mistakes, and review tips for reliable software coverage.
Smoke vs Sanity vs Regression Testing
Compare smoke vs sanity vs regression testing with definitions, checklists, when to run each, release strategy tips, and common mistakes to avoid.
Bug Report Template: How to Write a Great Defect Report
Learn how to write a bug report with a clear template, steps to reproduce, severity vs priority, expected vs actual results, and examples developers trust.