Every page object does the same things: wait for elements, click buttons, type text, check if something is displayed, scroll to an element. Instead of writing these methods in every page class, you write them once in BasePage. All page objects extend BasePage and get these methods for free.
Think of BasePage like a toolbox that every electrician carries. Every electrician needs pliers, screwdrivers, and wire cutters. They do not make new tools for every job. BasePage is your toolbox — common actions that every page needs.
package com.testerrank.base;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
public class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
protected Logger logger;
public BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
this.logger = LogManager.getLogger(this.getClass());
}
// ---- Wait Methods ----
protected WebElement waitForVisible(By locator) {
logger.debug("Waiting for element visible: " + locator);
return wait.until(
ExpectedConditions.visibilityOfElementLocated(locator));
}
protected WebElement waitForClickable(By locator) {
logger.debug("Waiting for element clickable: " + locator);
return wait.until(
ExpectedConditions.elementToBeClickable(locator));
}
protected boolean waitForInvisible(By locator) {
logger.debug("Waiting for element invisible: " + locator);
return wait.until(
ExpectedConditions.invisibilityOfElementLocated(locator));
}
protected List<WebElement> waitForAllVisible(By locator) {
return wait.until(
ExpectedConditions.visibilityOfAllElementsLocatedBy(
locator));
}
// ---- Action Methods ----
protected void click(By locator) {
logger.info("Clicking: " + locator);
waitForClickable(locator).click();
}
protected void type(By locator, String text) {
logger.info("Typing '" + text + "' into: " + locator);
WebElement element = waitForVisible(locator);
element.clear();
element.sendKeys(text);
}
protected void clearAndType(By locator, String text) {
WebElement element = waitForVisible(locator);
element.sendKeys(Keys.CONTROL + "a");
element.sendKeys(text);
}
protected String getText(By locator) {
String text = waitForVisible(locator).getText();
logger.debug("Got text '" + text + "' from: " + locator);
return text;
}
protected String getAttribute(By locator, String attribute) {
return waitForVisible(locator).getAttribute(attribute);
}
// ---- State Check Methods ----
protected boolean isDisplayed(By locator) {
try {
return driver.findElement(locator).isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
protected boolean isEnabled(By locator) {
try {
return driver.findElement(locator).isEnabled();
} catch (NoSuchElementException e) {
return false;
}
}
protected boolean isSelected(By locator) {
return waitForVisible(locator).isSelected();
}
protected int getElementCount(By locator) {
return driver.findElements(locator).size();
}
// ---- Dropdown Methods ----
protected void selectByVisibleText(By locator, String text) {
logger.info("Selecting '" + text + "' from dropdown: "
+ locator);
new Select(waitForVisible(locator))
.selectByVisibleText(text);
}
protected void selectByValue(By locator, String value) {
new Select(waitForVisible(locator)).selectByValue(value);
}
protected String getSelectedText(By locator) {
return new Select(waitForVisible(locator))
.getFirstSelectedOption().getText();
}
// ---- Scroll Methods ----
protected void scrollToElement(By locator) {
WebElement element = driver.findElement(locator);
((JavascriptExecutor) driver).executeScript(
"arguments[0].scrollIntoView({behavior: 'smooth', "
+ "block: 'center'});", element);
}
protected void scrollToBottom() {
((JavascriptExecutor) driver).executeScript(
"window.scrollTo(0, document.body.scrollHeight)");
}
// ---- Resilient Click (handles intercepted clicks) ----
protected void clickWithRetry(By locator, int maxAttempts) {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
waitForClickable(locator).click();
return;
} catch (Exception e) {
logger.warn("Click attempt " + attempt + "/"
+ maxAttempts + " failed: " + e.getMessage());
if (attempt == maxAttempts) {
throw new RuntimeException(
"Failed to click " + locator
+ " after " + maxAttempts + " attempts", e);
}
try { Thread.sleep(500); }
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
// ---- Get List of Texts ----
protected List<String> getTexts(By locator) {
return waitForAllVisible(locator).stream()
.map(WebElement::getText)
.collect(Collectors.toList());
}
// ---- Hover ----
protected void hoverOver(By locator) {
new Actions(driver)
.moveToElement(waitForVisible(locator))
.perform();
}
// ---- Page Load ----
protected void waitForPageLoad() {
wait.until(d -> ((JavascriptExecutor) d)
.executeScript("return document.readyState")
.equals("complete"));
}
protected String getPageTitle() {
return driver.getTitle();
}
protected String getCurrentUrl() {
return driver.getCurrentUrl();
}
}When a page object extends BasePage, it inherits all these methods. The page object only defines its locators and business-specific methods. Look how clean the LoginPage becomes.
package com.testerrank.pages.banking;
import com.testerrank.base.BasePage;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class LoginPage extends BasePage {
// Locators — private, never accessed from tests
private final By usernameField = By.id("username");
private final By passwordField = By.id("password");
private final By loginButton = By.id("login-btn");
private final By errorMessage = By.cssSelector(".error-message");
private final By forgotPasswordLink = By.linkText("Forgot Password?");
public LoginPage(WebDriver driver) {
super(driver); // Pass driver to BasePage
}
// Actions — public, called from tests
public DashboardPage loginAs(String username, String password) {
logger.info("Logging in as: " + username);
type(usernameField, username); // inherited from BasePage
type(passwordField, password); // inherited from BasePage
click(loginButton); // inherited from BasePage
return new DashboardPage(driver);
}
public void enterUsername(String username) {
type(usernameField, username);
}
public void enterPassword(String password) {
type(passwordField, password);
}
public void clickLogin() {
click(loginButton);
}
public boolean isErrorDisplayed() {
return isDisplayed(errorMessage); // inherited from BasePage
}
public String getErrorText() {
return getText(errorMessage); // inherited from BasePage
}
}Q: What methods do you keep in your BasePage class?
A: BasePage contains wrapper methods around common Selenium actions: click (waits for clickable, then clicks), type (waits for visible, clears, then types), getText, isDisplayed (with try-catch to return false instead of throwing), waitForVisible, waitForClickable, scrollToElement, selectByVisibleText for dropdowns, and clickWithRetry for handling intercepted clicks. The key idea is that page objects call click(locator) instead of driver.findElement(locator).click() — so if we need to add logging, retry logic, or wait behavior, we change it once in BasePage.
BasePage methods should never contain business logic. isDisplayed() is fine. isLoginSuccessful() is not — that belongs in the LoginPage. If you find yourself adding page-specific logic to BasePage, you are doing it wrong.
Notice that BasePage uses this.getClass() for the logger. This means when LoginPage logs a message, the log shows "LoginPage" as the source, not "BasePage." Small detail, but it makes debugging much easier.
Key Point: BasePage is the toolbox. It holds common actions (click, type, wait, scroll) so page objects stay clean and focused on business logic.