Writing one test is easy. Writing 50 tests that are organized, readable, and maintainable? That requires structure. Playwright gives you tools to group tests, share setup code, and clean up after yourself. Think of it like organizing your wardrobe -- without structure, you waste time searching for things.
import { test, expect } from '@playwright/test';
test.describe('Banking Portal Login', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/banking');
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
test('should show error for invalid password', async ({ page }) => {
await page.goto('/banking');
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('wrongpass');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(/invalid/i)).toBeVisible();
});
test('should show error for empty fields', async ({ page }) => {
await page.goto('/banking');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(/required/i)).toBeVisible();
});
});Notice every test navigates to /banking. That is duplicated code. Let us fix it.
import { test, expect } from '@playwright/test';
test.describe('Banking Portal Login', () => {
// Runs before EVERY test in this describe block
test.beforeEach(async ({ page }) => {
await page.goto('/banking');
});
test('should login with valid credentials', async ({ page }) => {
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
test('should show error for invalid password', async ({ page }) => {
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('wrongpass');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(/invalid/i)).toBeVisible();
});
test('should show error for empty fields', async ({ page }) => {
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText(/required/i)).toBeVisible();
});
});| Hook | When It Runs | Common Use Case |
|---|---|---|
| test.beforeAll | Once before all tests in the describe block | Start a server, seed a database, create test data |
| test.beforeEach | Before each individual test | Navigate to a page, log in, reset state |
| test.afterEach | After each individual test | Log out, take a screenshot, clear data |
| test.afterAll | Once after all tests in the describe block | Stop a server, clean up database, close connections |
import { test, expect } from '@playwright/test';
test.describe('Banking Portal Dashboard', () => {
test.beforeAll(async () => {
console.log('Starting test suite -- dashboard tests');
// Could seed test data here via API
});
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/banking');
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
test.afterEach(async ({ page }) => {
// Logout after each test to start fresh
await page.getByRole('button', { name: 'Logout' }).click();
});
test('should display account balance', async ({ page }) => {
await expect(page.getByText(/balance/i)).toBeVisible();
});
test('should show recent transactions', async ({ page }) => {
await expect(page.getByRole('table')).toBeVisible();
});
});test.beforeAll does NOT receive a page object -- it runs once, not per-test. If you need to do browser-level setup once for all tests, use a worker fixture instead. Use beforeAll for non-browser setup like API calls or database seeding.
You can nest describe blocks. Outer describe for the feature, inner describe for sub-features. Each level can have its own beforeEach. They run in order: outer beforeEach first, then inner beforeEach.
Q: What is the difference between beforeAll and beforeEach in Playwright?
A: beforeAll runs once before all tests in the describe block. Use it for expensive one-time setup like seeding a database or starting a mock server. beforeEach runs before every single test. Use it for per-test setup like navigating to a page or logging in. Important: beforeAll does not have access to the page object because there is no test-specific browser context yet. beforeEach does get a fresh page for each test.
Key Point: Use test.describe to group related tests, test.beforeEach for shared setup, and test.afterEach for cleanup. Hooks keep your tests DRY and organized.