Playwright Tutorial for Beginners -- Your First Test in 10 Minutes
A step-by-step Playwright tutorial for complete beginners. Learn to install Playwright, write your first E2E test, use locators, debug with UI mode, and set up CI/CD with GitHub Actions.
Playwright is the most popular end-to-end testing framework in 2026. If you have never written an automated test before, this tutorial will take you from zero to a working test suite in 10 minutes.
What You Will Build
By the end of this tutorial, you will have:
- A Playwright project installed and configured
- Your first passing E2E test
- Knowledge of locator strategies (getByRole, getByText, getByLabel)
- Experience with Playwright's debugging tools
- A GitHub Actions CI pipeline running your tests automatically
Prerequisites
- Node.js 18+ installed (check with
node --version) - A code editor (VS Code recommended)
- Basic knowledge of JavaScript or TypeScript
- A terminal/command line
No prior testing experience needed.
Step 1: Install Playwright
Open your terminal and run:
npm init playwright@latest
The installer will ask you a few questions:
- Language: Choose TypeScript (recommended) or JavaScript
- Test directory: Press Enter to accept
tests/ - GitHub Actions: Choose Yes to add a CI workflow
- Install browsers: Choose Yes
This creates your project structure:
my-project/
tests/
example.spec.ts # Sample test
playwright.config.ts # Configuration
package.json
.github/
workflows/
playwright.yml # CI pipeline
Step 2: Write Your First Test
Delete the sample test and create tests/homepage.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage has correct title', async ({ page }) => {
// Navigate to a website
await page.goto('https://playwright.dev');
// Verify the page title
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link navigates to intro page', async ({ page }) => {
await page.goto('https://playwright.dev');
// Click the "Get started" link
await page.getByRole('link', { name: 'Get started' }).click();
// Verify we landed on the right page
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
What Is Happening Here?
test()defines a test case with a name and an async function{ page }is a Playwright fixture -- a fresh browser page created for each testpage.goto()navigates to a URLpage.getByRole()finds elements by their accessibility roleexpect()makes assertions -- Playwright auto-waits until they pass or timeout
Step 3: Run Your Tests
npx playwright test
You should see output like:
Running 2 tests using 2 workers
2 passed (3.2s)
To see the tests run in a visible browser:
npx playwright test --headed
To run a specific test file:
npx playwright test tests/homepage.spec.ts
Step 4: Understand Locators
Locators are how you find elements on the page. Playwright provides several locator strategies, ranked by preference:
1. getByRole (Best)
Finds elements by their accessibility role. This is the most resilient strategy:
// Find a button
page.getByRole('button', { name: 'Submit' });
// Find a link
page.getByRole('link', { name: 'Sign up' });
// Find a heading
page.getByRole('heading', { level: 1 });
// Find a textbox
page.getByRole('textbox', { name: 'Email' });
2. getByLabel
Finds form elements by their associated label:
page.getByLabel('Email address');
page.getByLabel('Password');
page.getByLabel('Remember me');
3. getByText
Finds elements by their visible text:
page.getByText('Welcome back');
page.getByText('Sign up', { exact: true });
4. getByPlaceholder
Finds inputs by placeholder text:
page.getByPlaceholder('Search...');
page.getByPlaceholder('Enter your email');
5. getByTestId (Last Resort)
Finds elements by data-testid attribute. Use when no semantic locator works:
page.getByTestId('submit-button');
page.getByTestId('user-avatar');
Rule of thumb: Always try getByRole first. Only fall back to getByTestId when the element has no accessible role, label, or text.
Step 5: Write a Real-World Test
Let us write a test for a login form:
import { test, expect } from '@playwright/test';
test.describe('Login page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://myapp.com/login');
});
test('successful login redirects to dashboard', async ({ page }) => {
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('securepassword');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByText('Welcome back')).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.getByLabel('Email').fill('wrong@example.com');
await page.getByLabel('Password').fill('badpassword');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid email or password')).toBeVisible();
});
test('shows validation error for empty email', async ({ page }) => {
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Email is required')).toBeVisible();
});
});
Key Patterns
test.describe()groups related teststest.beforeEach()runs before every test in the group.fill()types text into an input (clears existing content first).click()clicks an elementexpect(...).toBeVisible()checks that an element is visible on screenexpect(page).toHaveURL()checks the current URL
Step 6: Assertions
Playwright's assertions auto-wait. They keep retrying until the assertion passes or the timeout expires.
| Assertion | What It Checks |
|---|---|
toBeVisible() | Element is visible |
toBeHidden() | Element is hidden |
toHaveText('x') | Element has exact text |
toContainText('x') | Element contains text |
toHaveURL(/pattern/) | Page URL matches |
toHaveTitle('x') | Page title matches |
toHaveValue('x') | Input has value |
toBeEnabled() | Element is enabled |
toBeDisabled() | Element is disabled |
toBeChecked() | Checkbox is checked |
toHaveCount(n) | Locator matches n elements |
Step 7: Debug with UI Mode
When a test fails, Playwright's UI Mode is your best friend:
npx playwright test --ui
This opens an interactive window where you can:
- Watch tests run in real-time with a visible browser
- Step through actions one at a time
- Inspect the DOM at each step
- See network requests and console logs
- Pick locators by pointing at elements
Trace Viewer
For debugging CI failures, enable trace recording:
// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry',
},
});
After a failure, view the trace:
npx playwright show-trace test-results/trace.zip
Code Generator
Not sure how to locate an element? Let Playwright show you:
npx playwright codegen https://myapp.com
This opens a browser. Click around, and Playwright generates the test code automatically.
Step 8: Page Object Model
As your test suite grows, organize with the Page Object Model pattern:
// pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly email: Locator;
readonly password: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.email = page.getByLabel('Email');
this.password = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submitButton.click();
}
}
Use it in tests:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL(/dashboard/);
});
Step 9: Configuration
Here is a production-ready playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
This config runs tests across Chrome, Firefox, Safari, and mobile, starts your dev server automatically, and enables traces on failure.
Step 10: CI/CD with GitHub Actions
The Playwright installer created .github/workflows/playwright.yml. Here is an optimized version:
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Every push and pull request runs your tests automatically. Failed test reports are uploaded as downloadable artifacts.
Common Beginner Mistakes
| Mistake | Fix |
|---|---|
Using page.locator('#id') for everything | Use getByRole, getByLabel first |
Adding await page.waitForTimeout(3000) | Remove it. Playwright auto-waits. |
Not using test.beforeEach | Group shared setup to reduce duplication |
| Putting selectors directly in tests | Use Page Object Model for maintainability |
| Running all browsers locally | Run Chromium only in dev, all browsers in CI |
| Ignoring the trace viewer | Always check traces for CI failures |
Supercharge with QA Skills
Want your AI coding agent to write Playwright tests using all these best practices automatically? Install the Playwright E2E skill:
npx @qaskills/cli add playwright-e2e
This teaches Claude Code, Cursor, Copilot, and other AI agents to follow Page Object Model patterns, use proper locator strategies, and write resilient tests from the start.
For more advanced patterns, read our complete Playwright guide or our guide to fixing flaky tests. Browse all QA skills at qaskills.sh/skills or get started now.
What Is Next?
Now that you have your first tests running:
- Add more tests for your application's critical user flows
- Set up the Page Object Model for maintainability
- Enable visual comparison with
toHaveScreenshot() - Add API testing with Playwright's
requestfixture - Install QA skills to let your AI agent help write tests
npx @qaskills/cli add playwright-e2e
Written by Pramod Dutta, founder of The Testing Academy and QASkills.sh.