Page Object Model: what it is and the rules
A Page Object is a class describing a page (or part of it) of the application: where its elements are and what can be done with it. The test speaks the user's language (loginPage.loginAs(user)), not the language of locators. When the markup changes, you fix one class instead of hundreds of tests.
public class LoginPage {
private final WebDriver driver;
private final By email = By.id("email");
private final By password = By.id("password");
private final By submit = By.cssSelector("[data-testid='login-submit']");
public LoginPage(WebDriver driver) { this.driver = driver; }
public DashboardPage loginAs(String user, String pass) {
driver.findElement(email).sendKeys(user);
driver.findElement(password).sendKeys(pass);
driver.findElement(submit).click();
return new DashboardPage(driver);
}
}
Rules (matching what the Selenium documentation advises):
- locators and interaction details stay inside the class, the test does not see them;
- methods are services of the page (
loginAs, addToCart), not "click the button"; a method that leads to another page returns its Page Object;
- assertions belong in tests, not in the Page Object; the exception is a check in the constructor that the expected page is open;
- a Page Object rarely exposes the
WebDriver itself or raw WebElements;
- repeated blocks (header, menu, table) are extracted into separate components that are embedded in pages.
A typical mistake is a "god" class for a whole page and a method for every button with no business meaning.
PageFactory, components and fluent style
PageFactory is a Selenium helper class: fields with @FindBy are initialised by calling PageFactory.initElements(driver, this). Each field is a proxy that looks the element up on every access (with @CacheLookup only once, and then it can go stale).
public class LoginPage {
@FindBy(id = "email") private WebElement email;
@FindBy(id = "password") private WebElement password;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
}
Why teams move away from it:
- forget
initElements and you get a NullPointerException instead of a clear error;
- a locator cannot be parameterised (a table row by name, a button by text) without separate methods using
By;
- reflection and proxies complicate debugging and composition, while the gain over a plain
By field is small;
- the Selenium documentation mentions it only in a legacy section and gives no recommendation.
The alternative is By fields and explicit lookup methods with waits; they are more transparent and fit Java composition better.
Components. A header, a side menu, a modal, a table row are separate classes working inside a root element (SearchContext) rather than the whole page. They are reused across many Page Objects.
public class HeaderComponent {
private final WebDriver driver;
private final WebElement root;
public HeaderComponent(WebDriver driver, WebElement root) {
this.driver = driver;
this.root = root;
}
public SearchPage search(String text) {
root.findElement(By.name("q")).sendKeys(text + Keys.ENTER);
return new SearchPage(driver);
}
}
Fluent style: a method returns this or the next page, so the scenario reads as a chain: new LoginPage(driver).open().loginAs(u, p).openCart(). It is convenient, but long chains are hard to debug, so break them with meaningful variables where checks are needed.