The Page Object Model (POM) is a design pattern that maps each page — or significant section of a page — to a dedicated Java class. Locators and interactions live inside that class; test logic lives in step definitions. This separation means that when the UI changes, you update one class, not every test that touches that page.
Why POM matters
- Single source of truth — a locator is defined once and reused everywhere.
- Readable tests —
loginModal.typeName("Fernando") is clearer than driver.findElement(By.id("name_input_input")).sendKeys("Fernando").
- Easier maintenance — a changed selector requires editing one field, not hunting through dozens of step definitions.
- Safe abstractions — page methods can bundle multiple low-level calls into a single intent-revealing operation.
The BasePage class
BasePage is the foundation every page object inherits from. It wraps raw Selenium calls with a 10-second WebDriverWait, so individual page objects never need to worry about timing.
public class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
Core methods
find
click
type
isDisplayed
isClickable
isChecked
Waits up to 10 seconds for the element to be visible in the DOM before returning it. Used internally by other BasePage methods.protected WebElement find(By locator) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
Waits until the element is clickable (visible and enabled) before dispatching the click. Prevents “element not interactable” errors on loading states.public void click(By locator) {
wait.until(ExpectedConditions.elementToBeClickable(locator)).click();
}
Clears any existing value before typing. This avoids silent concatenation bugs when a test re-runs in the same browser session.public void type(By locator, String text) {
WebElement element = find(locator);
element.clear();
element.sendKeys(text);
}
Returns false instead of throwing when the element is absent. Use this for soft assertions and conditional flows.public boolean isDisplayed(By locator) {
try {
return find(locator).isDisplayed();
} catch (Exception e) {
return false;
}
}
Returns false if the wait expires or the element is disabled. Safe for asserting button states without crashing the test.public boolean isClickable(By locator) {
try {
wait.until(ExpectedConditions.elementToBeClickable(locator));
return true;
} catch (Exception e) {
return false;
}
}
Inspects the selected state of a checkbox or radio button. Returns false on any exception rather than failing the test.public boolean isChecked(By locator) {
try {
return find(locator).isSelected();
} catch (Exception e) {
return false;
}
}
All boolean methods (isDisplayed, isClickable, isChecked) swallow exceptions intentionally. They are designed for assertions, not for control flow that must react to errors.
Concrete page objects
RiuHome
Represents the RIU Hotels & Resorts home page. Locators are private final fields; public methods expose named actions.
public class RiuHome extends BasePage {
private final By menuButton = By.xpath("//div/button[@aria-label='Menú']");
private final By accessButton = By.xpath("//div/button[@aria-label='RIU pro']");
private final By acceptCookiesBtn = By.id("onetrust-accept-btn-handler");
public RiuHome(WebDriver driver) {
super(driver);
}
public void navigateToRiu() { navigateTo("https://www.riu.com/es"); }
public void clickAcceptCookies() { click(this.acceptCookiesBtn); }
public void clickRegister() { click(this.accessButton); }
public boolean isAcceptCookiesButtonVisible() { return isDisplayed(this.acceptCookiesBtn); }
public boolean isAcceptCookiesButtonClickable() { return isClickable(this.acceptCookiesBtn); }
public boolean isAccessButtonVisible() { return isDisplayed(this.accessButton); }
public boolean isAccessButtonClickable() { return isClickable(this.accessButton); }
}
LoginModal
Represents the registration/login modal dialog. It maps the full registration form — name, surname, email, country, date-of-birth calendar, gender, marketing consent, and terms checkbox.
public class LoginModal extends BasePage {
private final By loginModal = By.id("dialog");
private final By registerTab = By.id("riuClassRegister");
private final By registerTitle = By.xpath("//h2/b");
private final By nameTextBox = By.id("name_input_input");
private final By surnameTextBox = By.id("surname_input_input");
private final By emailTextBox = By.id("email_input_input");
private final By countrySelect = By.id("country_select_menuButton");
private final By Argentina = By.id("AR");
private final By genreSelect = By.id("genderSelect_menuButton");
private final By male = By.id("H");
private final By salesInfoRadioBtn = By.id("controlInformationYes");
private final By termsCheckBox = By.id("acceptConditionsCheck");
private final By registerBtn = By.xpath("//div[@id='dialog']//button[@type='submit']");
private final By reqField = By.xpath(
"//riu-ui-calendar[@formcontrolname='birthDate']//div[text()=' Este campo es obligatorio. ']"
);
private final By calendar = By.xpath(" //riu-ui-icon[@class='u-pointer']/i");
private final By noDateBtn = By.xpath(
"//riu-ui-button//button[@data-qa='calendar-apply-button']"
);
public LoginModal(WebDriver driver) { super(driver); }
// Navigation
public void clickRegisterTab() { click(this.registerTab); }
public void clickRegisterBtn() { click(this.registerBtn); }
// Form filling
public void typeName(String name) { type(this.nameTextBox, name); }
public void typeLastName(String lastname) { type(this.surnameTextBox, lastname); }
public void typeEmail(String email) { type(this.emailTextBox, email); }
// Dropdowns
public void clickCountrySelect() { click(this.countrySelect); }
public void selectArgentina() { click(this.Argentina); }
public void clickGenreSelect() { click(this.genreSelect); }
public void selectMale() { click(this.male); }
// Calendar
public void openCalendar() { click(this.calendar); }
public void continueWithoutDate() { click(this.noDateBtn); }
// Consent
public void clickSalesRadioBtn() { click(this.salesInfoRadioBtn); }
public void clickTermsCheck() { click(this.termsCheckBox); }
public boolean isTermsUnchecked() { return !isChecked(this.termsCheckBox); }
// Visibility / state checks
public boolean isLoginModalVisible() { return isDisplayed(this.loginModal); }
public boolean isRegisterTabVisible() { return isDisplayed(this.registerTab); }
public boolean isRegisterTabClickable() { return isClickable(this.registerTab); }
public boolean isRegisterTitleVisible() { return isDisplayed(this.registerTitle); }
public boolean isNameTextBoxVisible() { return isDisplayed(this.nameTextBox); }
public boolean isNameTextBoxClickable() { return isClickable(this.nameTextBox); }
public boolean isLastNameTextBoxVisible() { return isDisplayed(this.surnameTextBox); }
public boolean isLastNameTextBoxClickable(){ return isClickable(this.surnameTextBox); }
public boolean isEmailTextBoxVisible() { return isDisplayed(this.emailTextBox); }
public boolean isEmailTextBoxClickable() { return isClickable(this.emailTextBox); }
public boolean isCountrySelectVisible() { return isDisplayed(this.countrySelect); }
public boolean isCountrySelectClickable() { return isClickable(this.countrySelect); }
public boolean isArgOptVisible() { return isDisplayed(this.Argentina); }
public boolean isNoDateBtnVisible() { return isDisplayed(this.noDateBtn); }
public boolean isGenreSelectVisible() { return isDisplayed(this.genreSelect); }
public boolean isGenreSelectClickable() { return isClickable(this.genreSelect); }
public boolean isMaleOptVisible() { return isDisplayed(this.male); }
public boolean isSalesRadioBtnVisible() { return isDisplayed(this.salesInfoRadioBtn); }
public boolean isSalesRadioBtnClickable() { return isClickable(this.salesInfoRadioBtn); }
public boolean isTermsCheckVisible() { return isDisplayed(this.termsCheckBox); }
public boolean isTermsCheckClickable() { return isClickable(this.termsCheckBox); }
public boolean isReqFielVisible() { return isDisplayed(this.reqField); }
// Data extraction
public String extractTitle() { return locatorText(this.registerTitle); }
}
Inheritance chain
BasePage
├── RiuHome
└── LoginModal
All page objects inherit driver, wait, and every helper method from BasePage. They never interact with WebDriver directly — only through the inherited methods.
Extending BasePage for a new page
Create the class
Create a new file under src/test/java/pages/. Name it after the page it represents.package pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import utilities.BasePage;
public class SearchResults extends BasePage {
public SearchResults(WebDriver driver) {
super(driver);
}
}
Add locators as private fields
Declare each locator as a private final By field at the top of the class. Keep them close together so it is easy to see what the page maps.private final By resultCards = By.cssSelector(".hotel-card");
private final By filtersPanel = By.id("filters-sidebar");
private final By sortDropdown = By.id("sort-select");
Write intent-revealing public methods
Wrap locator interactions in descriptive methods. Avoid leaking By references into step definitions.public boolean areResultsVisible() {
return isDisplayed(this.resultCards);
}
public void selectSortOption(String optionId) {
click(this.sortDropdown);
click(By.id(optionId));
}
Instantiate it in a step definition
Create the page object inside the relevant step method, passing context.driver.@When("the user searches for a hotel")
public void searchForHotel() {
searchResults = new SearchResults(context.driver);
Assert.assertTrue(searchResults.areResultsVisible());
}
Never store WebDriver as a static field. Static driver references cause test isolation failures when scenarios run in parallel. Always receive the driver through the constructor via TestContext.