Writing page objects is half the job. The other half is using them correctly in tests. There are patterns that make tests clean and patterns that make tests messy. Let me show you both so you know which to follow and which to avoid.
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
test.describe('Login Tests', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test('valid credentials redirect to dashboard', async () => {
await loginPage.login('john@test.com', 'password123');
await dashboardPage.expectLoaded();
});
test('invalid email shows error', async () => {
await loginPage.login('wrong@test.com', 'password123');
const error = await loginPage.getErrorText();
expect(error).toBe('Invalid email or password');
});
test('empty fields show validation', async () => {
await loginPage.login('', '');
await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
});
});For simpler tests that touch only one page, you can create the page object inside the test. No beforeEach needed.
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test('forgot password link navigates correctly', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.clickForgotPassword();
await expect(page).toHaveURL(/forgot-password/);
});import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';
import { TransferPage } from '../pages/transfer.page';
test('complete transfer flow', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
const transferPage = new TransferPage(page);
// Step 1: Login
await loginPage.goto();
await loginPage.login('john@test.com', 'password123');
// Step 2: Verify dashboard loaded
await dashboardPage.expectLoaded();
const balanceBefore = await dashboardPage.getBalance();
// Step 3: Navigate to transfer
await dashboardPage.clickTransfer();
// Step 4: Make transfer
await transferPage.transferMoney('9876543210', '500', 'Monthly rent');
await transferPage.expectSuccess();
// Step 5: Verify balance updated
await dashboardPage.goto();
const balanceAfter = await dashboardPage.getBalance();
expect(balanceAfter).not.toBe(balanceBefore);
});Never create page objects at the describe level like this: const loginPage = new LoginPage(page). The page fixture does not exist at describe scope. Always create page objects inside test() or beforeEach() where the page parameter is available.
Q: How do you use page objects in your tests?
A: I create page objects in beforeEach for tests that share the same setup. For standalone tests, I create them inline. I never put locators in test files -- only page object method calls. My tests read like user stories: loginPage.goto(), loginPage.login(), dashboardPage.expectLoaded(). For multi-page flows, I create multiple page objects and call their methods in sequence. Each test is independent and does not rely on state from other tests.
Key Point: Create page objects in beforeEach or inside tests -- never at describe scope. Tests should read like user stories with zero locators visible.