Now let us build real page classes. Each page extends BasePage and uses the inherited utility methods. Notice how clean the code is — no raw WebDriver calls, no waits scattered around.
package com.practice.pages.banking;
import com.practice.pages.BasePage;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class LoginPage extends BasePage {
// Locators — all in ONE place
private By usernameField = By.id("username");
private By passwordField = By.id("password");
private By loginButton = By.cssSelector("button[type='submit']");
private By errorMessage = By.cssSelector(".error-message");
public LoginPage(WebDriver driver) {
super(driver);
}
public void enterUsername(String username) {
type(usernameField, username);
}
public void enterPassword(String password) {
type(passwordField, password);
}
public DashboardPage clickLogin() {
click(loginButton);
return new DashboardPage(driver);
}
// Convenience method — most tests use this
public DashboardPage loginAs(String username, String password) {
enterUsername(username);
enterPassword(password);
return clickLogin();
}
public String getErrorMessage() {
return getText(errorMessage);
}
public boolean isErrorDisplayed() {
return isPresent(errorMessage);
}
public boolean isDisplayed() {
return isPresent(usernameField);
}
}package com.practice.pages.banking;
import com.practice.pages.BasePage;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class DashboardPage extends BasePage {
private By welcomeMessage = By.id("welcome-message");
private By accountBalance = By.id("account-balance");
private By accountNumber = By.id("account-number");
private By transferButton = By.id("transfer-btn");
private By logoutButton = By.id("logout-btn");
private By transactionHistory = By.id("transaction-history");
public DashboardPage(WebDriver driver) {
super(driver);
}
public boolean isLoaded() {
return isDisplayed(welcomeMessage);
}
public String getWelcomeMessage() {
return getText(welcomeMessage);
}
public String getAccountBalance() {
return getText(accountBalance);
}
public String getAccountNumber() {
return getText(accountNumber);
}
public boolean isTransactionHistoryVisible() {
return isDisplayed(transactionHistory);
}
// Returns TransferPage — the next page after clicking
public TransferPage clickTransfer() {
click(transferButton);
return new TransferPage(driver);
}
// Returns LoginPage — back to login after logout
public LoginPage logout() {
click(logoutButton);
return new LoginPage(driver);
}
}package com.practice.pages.banking;
import com.practice.pages.BasePage;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class TransferPage extends BasePage {
private By fromAccount = By.id("from-account");
private By toAccount = By.id("to-account");
private By amountField = By.id("amount");
private By descriptionField = By.id("description");
private By transferButton = By.id("transfer-submit");
private By confirmation = By.id("transfer-confirmation");
private By backButton = By.id("back-to-dashboard");
public TransferPage(WebDriver driver) {
super(driver);
}
public void selectFromAccount(String account) {
selectByVisibleText(fromAccount, account);
}
public void enterToAccount(String accountNumber) {
type(toAccount, accountNumber);
}
public void enterAmount(String amount) {
type(amountField, amount);
}
public void enterDescription(String desc) {
type(descriptionField, desc);
}
public void clickTransfer() {
click(transferButton);
}
// Convenience method for the full flow
public void performTransfer(String from, String to,
String amount, String desc) {
selectFromAccount(from);
enterToAccount(to);
enterAmount(amount);
enterDescription(desc);
clickTransfer();
}
public boolean isConfirmationDisplayed() {
return isDisplayed(confirmation);
}
public String getConfirmationMessage() {
return getText(confirmation);
}
public DashboardPage goBackToDashboard() {
click(backButton);
return new DashboardPage(driver);
}
}Notice how LoginPage.loginAs() returns a DashboardPage, and DashboardPage.logout() returns a LoginPage. The return types tell you the navigation flow. If a method stays on the same page (like filling a form), return "this" or void.
Key Point: Each page class has private locators and public action methods. Methods return the next page object for navigation.