Selenium Webdriver Page Object Model with Page Factory and @FindBys and @FindAll annotation. Also how to use different page for prod and preprod?

Sameera De Silva
5 min readFeb 10, 2025

--

LoginPage.Java

To honer the SRP Single responsibility SOLID principle, this class must only contains web elements.

package pages;

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindAll;
import org.openqa.selenium.support.FindBys;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactory;

public class LoginPage {
// WebElements with @CacheLookup using How strategy
@FindBy(how = How.ID, using = "username")
@CacheLookup
public WebElement usernameField;

@FindBy(how = How.ID, using = "password")
@CacheLookup
public WebElement passwordField;

@FindBy(how = How.ID, using = "rememberMe")
@CacheLookup
public WebElement rememberMeCheckbox;

// Using multiple locators for submitButton with AND condition
@FindBys({
@FindBy(how = How.XPATH, using = "//button[@type='submit']"),
@FindBy(how = How.ID, using = "submit")
})
@CacheLookup
public WebElement submitButton;

// Using multiple locators for staySignedIn checkbox with OR condition
@FindAll({
@FindBy(how = How.ID, using = "staySignedIn"),
@FindBy(how = How.XPATH, using = "//label[text()='Stay Signed In']/preceding-sibling::input")
})
@CacheLookup
public WebElement staySignedInCheckbox;
}

Let’s breakdown each annotation.

@FindBy use when you need to identify a web element using a single locator.

@FindBy(how = How.ID, using = "password")
@CacheLookup
public WebElement passwordField;

// @FindBys When the required WebElement objects need to match all of the given criteria use. So its an AND condtion

    // Using multiple locators for submitButton with AND condition
@FindBys({
@FindBy(how = How.XPATH, using = "//button[@type='submit']"),
@FindBy(how = How.ID, using = "submit")
})
@CacheLookup
public WebElement submitButton;

// @FindAll : When required WebElement objects need to match at least one of the given criteria

    // Using multiple locators for staySignedIn checkbox with OR condition
@FindAll({
@FindBy(how = How.ID, using = "staySignedIn"),
@FindBy(how = How.XPATH, using = "//label[text()='Stay Signed In']/preceding-sibling::input")
})
@CacheLookup
public WebElement staySignedInCheckbox;

What is @CacheLookup?

@CacheLookup is an annotation provided by Selenium to cache the web element after its first lookup. Once the element is found, it is stored in memory, avoiding the need to locate it again during subsequent interactions.

Why Do We Use It?

It enhances performance by reducing the time required to repeatedly locate the same element on the DOM, especially when dealing with static elements that do not change after page load.

Advantages:

🚀 Improved Performance: Reduces the time taken to locate elements during test execution.
⚡ Efficient for Static Elements: Ideal for elements like login fields, submit buttons, etc., that don’t reload or change frequently.
Disadvantages:

❌ Not Suitable for Dynamic Elements: If the element is refreshed, hidden, or removed from the DOM (due to page reloads or dynamic content updates), it may throw StaleElementReferenceException.
🔄 Can Cause Inconsistencies: Tests might fail if the cached element becomes outdated without re-locating the fresh one.
Best Practices:

✅ Use @CacheLookup only for static elements.
❌ Avoid it for elements that frequently update or reload dynamically.

LoginLibrary.java

package libraries;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
import pages.LoginPage;

public class LoginLibrary {
LoginPage loginPage;

// Initialize LoginPage using inbuilt PageFactory in the constructor.
public LoginLibrary(WebDriver driver) {
this.loginPage = PageFactory.initElements(driver, LoginPage.class);
}

// Reusable login method with parameterized credentials
public void loginToApplication(String username, String password, boolean rememberMe, boolean staySignedIn) {
loginPage.usernameField.clear();
loginPage.usernameField.sendKeys(username);

loginPage.passwordField.clear();
loginPage.passwordField.sendKeys(password);

if (rememberMe && !loginPage.rememberMeCheckbox.isSelected()) {
loginPage.rememberMeCheckbox.click();
}

if (staySignedIn && !loginPage.staySignedInCheckbox.isSelected()) {
loginPage.staySignedInCheckbox.click();
}

loginPage.submitButton.click();
}
}

If there are common methods, you can add them to a libreary class based on the pages .

what does this mean?

LoginPage loginPage = PageFactory.initElements(driver, LoginPage.class);

Initializing LoginPage using PageFactory
This line initializes all the WebElements declared in the LoginPage class with @FindBy annotations.
PageFactory uses the provided WebDriver instance to locate elements when accessed.
The ‘LoginPage.class’ refers to the LoginPage class type, which tells PageFactory which class to initialize.
This ensures all WebElements inside LoginPage are properly mapped with the corresponding elements on the web page.

Here is the LoginTest.java which is the actual test cases

package tests;

import libraries.LoginLibrary;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

public class LoginTest {
WebDriver driver;
LoginLibrary loginLibrary;

@BeforeClass
public void setup() {
driver = new ChromeDriver(); // Ensure chromedriver path is set in system properties
driver.get("https://example.com/login");
loginLibrary = new LoginLibrary(driver);
}

@Test
public void loginWithValidCredentials() {
loginLibrary.loginToApplication("testuser", "securePassword", true, true);
}

@AfterClass
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}

Now look at the scenario where in Prepod page there is an additional field to enter OTP after press login button ? How can I have 2 pages based on prod and preprod based on the environment with bare minimum code duplication.

Approach:

Base Class (Common Elements):
Create a BaseLoginPage that contains all the common elements and methods shared by both environments.

Environment-Specific Classes:
ProdLoginPage will extend BaseLoginPage without any additional fields.
PreprodLoginPage will extend BaseLoginPage and include the additional OTP field and related actions.

Dynamic Page Initialization:
Based on the environment, initialize the corresponding login page using a configuration parameter or system property.

BaseLoginPage.java this will have common elements for both prod and prepod.


package pages;

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;

public class BaseLoginPage {
@FindBy(how = How.ID, using = "username")
@CacheLookup
public WebElement usernameField;

@FindBy(how = How.ID, using = "password")
@CacheLookup
public WebElement passwordField;

@FindBy(how = How.ID, using = "rememberMe")
@CacheLookup
public WebElement rememberMeCheckbox;

@FindBy(how = How.ID, using = "submit")
@CacheLookup
public WebElement submitButton;
}

Since ProdLoginPage.java has an extra element . lets only initiate it . The rest can be accessed by extending BaseLoginPage.

package pages;

import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.WebDriver;

public class ProdLoginPage extends BaseLoginPage {
public ProdLoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
}


package pages;

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.WebDriver;

public class PreprodLoginPage extends BaseLoginPage {
@FindBy(how = How.ID, using = "otp")
@CacheLookup
public WebElement otpField;

public PreprodLoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
}

LoginLibrary.java based on the environment, select the page.

package libraries;

import org.openqa.selenium.WebDriver;
import pages.BaseLoginPage;
import pages.ProdLoginPage;
import pages.PreprodLoginPage;

public class LoginLibrary {
BaseLoginPage loginPage;

public LoginLibrary(WebDriver driver, String environment) {
if (environment.equalsIgnoreCase("preprod")) {
loginPage = new PreprodLoginPage(driver);
} else {
loginPage = new ProdLoginPage(driver);
}
}

public void login(String username, String password, boolean rememberMe, String otp) {
loginPage.usernameField.clear();
loginPage.usernameField.sendKeys(username);

loginPage.passwordField.clear();
loginPage.passwordField.sendKeys(password);

if (rememberMe && !loginPage.rememberMeCheckbox.isSelected()) {
loginPage.rememberMeCheckbox.click();
}

loginPage.submitButton.click();

// Handle OTP if present
if (loginPage instanceof PreprodLoginPage) {
PreprodLoginPage preprodPage = (PreprodLoginPage) loginPage;
preprodPage.otpField.clear();
preprodPage.otpField.sendKeys(otp);
}
}
}

LoginTest.java here we send the environment

package tests;

import libraries.LoginLibrary;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

public class LoginTest {
WebDriver driver;
LoginLibrary loginLibrary;

@BeforeClass
@Parameters({"environment"})
public void setup(String environment) {
driver = new ChromeDriver();
driver.get("https://example.com/login");
loginLibrary = new LoginLibrary(driver, environment);
}

@Test
@Parameters({"username", "password", "rememberMe", "otp"})
public void loginTest(String username, String password, boolean rememberMe, String otp) {
loginLibrary.login(username, password, rememberMe, otp);
}

@AfterClass
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}

--

--

No responses yet