/* Best Practice Hierarchy */
getByRole('button', { name: 'Submit' })
getByLabel('Email')
getByText('Confirm Order')
/* Fallback */
locator('[data-testid="submit-btn"]')
/* Avoid when possible */
locator('.btn.primary')
locator('//div[3]/button[2]')
/* Add 3 days */
const future = new Date();
future.setDate(future.getDate() + 3);
/* Round to nearest 15 minutes */
let minutes = future.getMinutes();
let rounded = Math.round(minutes / 15) * 15;
if (rounded === 60) {
rounded = 0;
future.setHours(future.getHours() + 1);
}
/* Handle midnight rollover */
if (future.getHours() === 0 && rounded === 0) {
future.setDate(future.getDate() + 1);
}
/* Format (MM/DD/YYYY) */
const date = future.toLocaleDateString('en-US');
/* Format time */
const time = `${future.getHours()}:${String(rounded).padStart(2,'0')}`;
/* Wait for element */
await expect(locator).toBeVisible();
/* Wait for loading spinner to disappear */
await expect(loadBox).not.toBeVisible();
/* Wait for navigation */
await page.waitForURL('**/orders');
/* Wait for API response */
await page.waitForResponse(resp =>
resp.url().includes('/api/orders') && resp.status() === 200
);
/* Wait for specific state (not just visibility) */
await expect(locator).toHaveText('Completed');
/* Wait for count */
await expect(rows).toHaveCount(10);
/* Avoid this */
await page.waitForTimeout(2000); // ❌ flaky
expect(...).toPass() to retry a code block until it succeeds or times out.
This is useful for slow-loading images, delayed rendering, and unstable UI readiness.
/* Retry Until Stable */
const chart = page.locator('#revenue-chart');
await expect(async () => {
await expect(chart).toBeVisible();
await expect(chart).toHaveAttribute('data-rendered', 'true');
}).toPass({
intervals: [1000, 2000, 5000],
timeout: 30000
});
/* Wait for Full Page Stability */
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');
/* Wait for Spinner / Overlay to Disappear */
await page.locator('.loading-spinner').waitFor({ state: 'hidden', timeout: 10000 });
/* Avoid This */ await page.waitForTimeout(3000); // ❌ flaky
await page.goto('/dashboard');
await FlakyFixer.waitForStability(page);
await FlakyFixer.retryUntil(async () => {
const chart = page.locator('#revenue-chart');
await expect(chart).toBeVisible();
await expect(chart).toHaveAttribute('data-rendered', 'true');
});
await page.getByRole('button', { name: 'Generate Report' }).click();
const spinner = page.locator('.loading-spinner');
await FlakyFixer.waitForVanished(spinner, 60_000);
/* Save signed-in state once */
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.USER_EMAIL);
await page.getByLabel('Password').fill(process.env.USER_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard|home/);
await page.context().storageState({ path: 'playwright/.auth/user.json' });
use: {
storageState: 'playwright/.auth/user.json'
}
/* Admin project */
{
name: 'admin',
use: { storageState: 'playwright/.auth/admin.json' }
}
/* Standard user project */
{
name: 'user',
use: { storageState: 'playwright/.auth/user.json' }
}
/* Faster than UI login when supported */
const requestContext = await request.newContext();
const response = await requestContext.post('/api/login', {
data: {
email: process.env.USER_EMAIL,
password: process.env.USER_PASSWORD
}
});
await expect(response).toBeOK();
/* Avoid logging in through the UI in every test */
test('every test logs in from scratch', async ({ page }) => {
await page.goto('/login');
...
}); // ❌ slow and flaky
/* global.setup.js */
import { chromium, expect } from '@playwright/test';
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.USER_EMAIL);
await page.getByLabel('Password').fill(process.env.USER_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard|home/);
await page.context().storageState({ path: 'playwright/.auth/user.json' });
await browser.close();
}
/* Mock a successful API response */
await page.route('**/api/v1/menu**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [
{ id: 1, name: 'Turkey Sandwich' },
{ id: 2, name: 'Chicken Wrap' }
]
})
});
});
/* Force empty results */
await page.route('**/api/v1/menu**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [] })
});
});
/* Simulate server failure */
await page.route('**/api/v1/menu**', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal Server Error' })
});
});
/* Wait for a real API response */
const response = await page.waitForResponse(resp =>
resp.url().includes('/api/v1/menu') && resp.status() === 200
);
/* Avoid depending on unstable backend data for UI state tests */ // ❌ test passes or fails depending on real API data
await page.route('**/api/v1/menu**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [{ id: 1, name: 'Mocked Sandwich' }]
})
});
});
await page.goto('/menu');
await expect(page.getByText('Mocked Sandwich')).toBeVisible();
/* Pause test execution and inspect live */
await page.pause();
/* Log current URL for context */
console.log(await page.url());
/* Capture visible page state */
await page.screenshot({ path: 'debug-screenshot.png', fullPage: true });
/* playwright.config.js */
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
}
/* Open trace after run */
npx playwright show-trace trace.zip
/* Run in headed debug mode */ npx playwright test --debug /* Or use UI mode */ npx playwright test --ui
/* Capture locator text */
const bannerText = await page.locator('#editTrip p').textContent();
console.log('Trip banner:', bannerText);
/* Capture element count */
const rows = page.locator('table tbody tr');
console.log('Row count:', await rows.count());
/* Browser console logging */
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
/* Failed request logging */
page.on('requestfailed', request => {
console.log('REQUEST FAILED:', request.url());
});
/* Avoid guessing why a test failed */ // ❌ adding random waitForTimeout calls // ❌ changing locators before checking trace / UI / console evidence
test('debug failing checkout', async ({ page }) => {
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
page.on('requestfailed', request => {
console.log('REQUEST FAILED:', request.url());
});
await page.goto('/checkout');
await page.pause();
await page.screenshot({ path: 'checkout-debug.png', fullPage: true });
const errorBanner = page.locator('.error-banner');
console.log('Error visible:', await errorBanner.isVisible());
});
# GitHub Actions basic Playwright run
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
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
# Upload report even if tests fail
- name: Upload Playwright Report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
/* playwright.config.js */
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
}
# GitHub Actions env example
- name: Run tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.BASE_URL }}
USER_EMAIL: ${{ secrets.USER_EMAIL }}
USER_PASSWORD: ${{ secrets.USER_PASSWORD }}
# Run one folder or tag in CI - name: Run regression suite run: npx playwright test Regression # Or run tagged tests - name: Run smoke tests run: npx playwright test --grep "@smoke"
# Avoid this - using npm install instead of npm ci in CI - skipping playwright install --with-deps - not uploading artifacts after failures - hardcoding credentials in workflow files
name: Playwright Regression
on:
workflow_dispatch:
jobs:
regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install browsers
run: npx playwright install --with-deps
- name: Run regression
run: npx playwright test Regression --reporter=line
env:
CI: 'true'
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
npm init playwright@latest — what it creates and whytest(), page.goto(), expect()npx playwright test, --ui flag, --headednpx playwright show-report--ui and --headed?getByRole(), getByText(), getByLabel(),
getByPlaceholder(), getByTestId()
page.locator('.card').getByRole('button')getByTestId over getByRole?click(), fill(), type(), press(),
selectOption()
check() / uncheck() for checkboxes and radioshover(), focus(), blur()dragAndDrop() and mouse eventspage.keyboard and page.mouse for low-level controlsetInputFiles()fill() and type()?toBeVisible(), toHaveText(), toHaveValue(),
toBeEnabled(), toBeChecked()
toHaveURL(), toHaveTitle()toHaveCount()toHaveAttribute(), toHaveClass()expect.soft().notexpect.soft() instead of a normal assertion?toHaveText() and toContainText()?test.describe() for grouping related teststest.beforeEach(), test.afterEach(), test.beforeAll(),
test.afterAll()
page, context, browser, request
test.extend()test.use() for per-suite configuration overridesbeforeAll vs beforeEach?loggedInPage fixture from memory.Page instancepage.route() to intercept and fulfill requestsroute.fulfill()route.continue()route.abort()page.waitForResponse()request fixture (no browser needed)route.fulfill() and route.continue()?page.context().storageState() for saving sessionstorageState in playwright.config.jswaitForTimeout(). Learn to diagnose and fix flaky tests systematically.
page.waitForURL(), page.waitForLoadState()locator.waitFor() for explicit element waitingpage.waitForFunction() for custom conditionswaitForTimeout() causes flakes — and alternativestest.retries — when appropriate--trace on to debug flakesawait page.waitForTimeout(2000)?PWDEBUG=1 — step through tests livescreenshot: 'only-on-failure'page.pause() for breakpoint-style debugging--ui): live reload and trace viewer built inexpect(page).toHaveScreenshot() — full page snapshotsexpect(locator).toHaveScreenshot() — component-level--update-snapshotsmask optionrequest fixture for API-only testsrequest.get/post/put/delete()playwright.request.newContext() for standalone API contextsfullyParallel, workers config options--shard=1/4 to split across machines--grep, test.tagframeLocator() — the modern approachpage.waitForEvent('popup')page.on('dialog', ...)page.waitForEvent('download')locator.shadowRoot() vs CSS piercingnpx playwright install --with-deps.env locally vs CI secrets--with-deps in CI but not locally?test.extend<{}>()Partial<>, Pick<> for test data buildersUserData interface with 5 fields for a test data builder.process.env.X directly?
@axe-core/playwrightcheckA11y() — configuring rules, disabling false positivespage.evaluate(() => performance.timing)checkA11y actually check — and what does it NOT check?test.info().attach() — adding custom artifacts to reports@playwright/experimental-ct-reactmount() fixture