Theory is done. Let us build real page objects for the Banking Portal. We will create DashboardPage and TransferPage with full working code. These are the kinds of page objects you will build in your actual job.
import { type Locator, type Page, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class DashboardPage extends BasePage {
readonly welcomeMessage: Locator;
readonly balanceDisplay: Locator;
readonly accountTypeLabel: Locator;
readonly transferButton: Locator;
readonly transactionTable: Locator;
readonly transactionRows: Locator;
readonly logoutButton: Locator;
readonly notificationBell: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.getByTestId('welcome-message');
this.balanceDisplay = page.getByTestId('balance');
this.accountTypeLabel = page.getByTestId('account-type');
this.transferButton = page.getByRole('button', { name: 'Transfer' });
this.transactionTable = page.getByRole('table');
this.transactionRows = page.getByRole('row');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
this.notificationBell = page.getByTestId('notification-bell');
}
async goto() {
await super.goto('/banking/dashboard');
}
async expectLoaded() {
await expect(this.welcomeMessage).toBeVisible();
await expect(this.balanceDisplay).toBeVisible();
}
async getBalance(): Promise<string> {
return (await this.balanceDisplay.textContent()) ?? '';
}
async getWelcomeText(): Promise<string> {
return (await this.welcomeMessage.textContent()) ?? '';
}
async clickTransfer() {
await this.transferButton.click();
}
async getTransactionCount(): Promise<number> {
// Subtract 1 for the header row
return (await this.transactionRows.count()) - 1;
}
async getTransactionByPayee(payee: string) {
return this.transactionRows.filter({ hasText: payee });
}
async logout() {
await this.logoutButton.click();
}
}import { type Locator, type Page, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class TransferPage extends BasePage {
readonly fromAccountDropdown: Locator;
readonly toAccountInput: Locator;
readonly amountInput: Locator;
readonly noteInput: Locator;
readonly transferButton: Locator;
readonly cancelButton: Locator;
readonly successMessage: Locator;
readonly errorMessage: Locator;
readonly balanceAfterTransfer: Locator;
constructor(page: Page) {
super(page);
this.fromAccountDropdown = page.getByLabel('From Account');
this.toAccountInput = page.getByLabel('To Account');
this.amountInput = page.getByLabel('Amount');
this.noteInput = page.getByLabel('Note');
this.transferButton = page.getByRole('button', { name: 'Transfer' });
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
this.successMessage = page.getByTestId('success-message');
this.errorMessage = page.getByTestId('error-message');
this.balanceAfterTransfer = page.getByTestId('balance-after');
}
async goto() {
await super.goto('/banking/transfer');
}
async transferMoney(toAccount: string, amount: string, note?: string) {
await this.toAccountInput.fill(toAccount);
await this.amountInput.fill(amount);
if (note) {
await this.noteInput.fill(note);
}
await this.transferButton.click();
}
async selectFromAccount(accountName: string) {
await this.fromAccountDropdown.selectOption(accountName);
}
async expectSuccess() {
await expect(this.successMessage).toBeVisible();
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
async cancel() {
await this.cancelButton.click();
}
}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.describe('Banking Transfer', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
let transferPage: TransferPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
transferPage = new TransferPage(page);
// Login before each test
await loginPage.goto();
await loginPage.login('john@test.com', 'password123');
await dashboardPage.expectLoaded();
});
test('transfer money to another account', async ({ page }) => {
await dashboardPage.clickTransfer();
await transferPage.transferMoney('9876543210', '500', 'Rent payment');
await transferPage.expectSuccess();
});
test('transfer with insufficient balance shows error', async ({ page }) => {
await dashboardPage.clickTransfer();
await transferPage.transferMoney('9876543210', '999999');
await transferPage.expectError('Insufficient balance');
});
test('cancel transfer returns to dashboard', async ({ page }) => {
await dashboardPage.clickTransfer();
await transferPage.cancel();
await dashboardPage.expectLoaded();
});
});Key Point: Look at those tests. No locators. No getByRole. No getByLabel. Just clean method calls: loginPage.login(), dashboardPage.clickTransfer(), transferPage.transferMoney(). This is what POM gives you -- tests that read like user stories.
Notice how each page object method name describes a user action, not a technical step. We say transferMoney(), not fillAmountAndClickButton(). Method names should describe WHAT the user does, not HOW the code does it.
Q: Show me a real-world page object you have built.
A: In my banking application project, I built a TransferPage with locators for the from-account dropdown, to-account input, amount input, and transfer button. The key method was transferMoney(toAccount, amount, note) which filled all fields and clicked transfer. I also had expectSuccess() and expectError(message) for verifying outcomes. The class extended BasePage for common navigation. Tests called transferPage.transferMoney("9876543210", "500") without knowing anything about locators. When the design team renamed the button from "Transfer" to "Send Money," I changed one line in TransferPage. Zero tests changed.
Key Point: Real page objects have locators, action methods, and verification helpers. Tests use method calls, never locators directly. Method names describe user actions, not technical steps.