Skip to main content
SeleniumDecoded

Page Factory

Use Page Factory pattern to initialize page elements with annotations for cleaner page objects.

Selenium 3 & 4 Stable

Page Factory is an extension of the Page Object Model that uses annotations to declare web elements. This reduces boilerplate code and enables lazy initialization of elements.

Page Factory vs Page Object

AspectPage ObjectPage Factory
Element declarationBy locatorsAnnotations
InitializationManual findElementAutomatic via initElements
Lazy loadingManualBuilt-in
Language supportAllJava, C# (Python limited)

Java Page Factory

Java Page Factory Implementation
Selenium 3 & 4 Stable
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class LoginPage {
private WebDriver driver;
private WebDriverWait wait;
// Elements declared with annotations
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(className = "error-message")
private WebElement errorMessage;
@FindBy(xpath = "//div[@class='welcome']")
private WebElement welcomeMessage;
// Constructor initializes elements
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
PageFactory.initElements(driver, this); // Magic happens here
}
public void navigate() {
driver.get("https://example.com/login");
}
public void login(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
}
public String getErrorMessage() {
wait.until(ExpectedConditions.visibilityOf(errorMessage));
return errorMessage.getText();
}
public boolean isWelcomeDisplayed() {
wait.until(ExpectedConditions.visibilityOf(welcomeMessage));
return welcomeMessage.isDisplayed();
}
}
# Python doesn't have built-in Page Factory, but you can use page-factory library
# pip install page-factory
from page_factory import PageFactory
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage(PageFactory):
# Define locators as class attributes
locators = {
"username_input": ("ID", "username"),
"password_input": ("ID", "password"),
"login_button": ("CSS_SELECTOR", "button[type='submit']"),
"error_message": ("CLASS_NAME", "error-message"),
"welcome_message": ("XPATH", "//div[@class='welcome']")
}
def __init__(self, driver):
super().__init__(driver)
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def navigate(self):
self.driver.get("https://example.com/login")
def login(self, username, password):
self.username_input.send_keys(username)
self.password_input.send_keys(password)
self.login_button.click()
def get_error_message(self):
self.wait.until(EC.visibility_of(self.error_message))
return self.error_message.text
def is_welcome_displayed(self):
self.wait.until(EC.visibility_of(self.welcome_message))
return self.welcome_message.is_displayed()
# Alternative: Simple decorator-based approach
from functools import cached_property
class LoginPageSimple:
def __init__(self, driver):
self.driver = driver
@cached_property
def username_input(self):
return self.driver.find_element(By.ID, "username")
@cached_property
def password_input(self):
return self.driver.find_element(By.ID, "password")
// JavaScript doesn't have native Page Factory
// But you can create a similar pattern with getters
class LoginPage {
constructor(driver) {
this.driver = driver;
}
// Lazy-loaded elements via getters
get usernameInput() {
return this.driver.findElement(By.id('username'));
}
get passwordInput() {
return this.driver.findElement(By.id('password'));
}
get loginButton() {
return this.driver.findElement(By.css("button[type='submit']"));
}
get errorMessage() {
return this.driver.findElement(By.className('error-message'));
}
async navigate() {
await this.driver.get('https://example.com/login');
}
async login(username, password) {
await (await this.usernameInput).sendKeys(username);
await (await this.passwordInput).sendKeys(password);
await (await this.loginButton).click();
}
async getErrorMessage() {
const element = await this.driver.wait(
until.elementLocated(By.className('error-message')),
10000
);
return element.getText();
}
}
using OpenQA.Selenium;
using OpenQA.Selenium.Support.PageObjects;
using SeleniumExtras.PageObjects;
using OpenQA.Selenium.Support.UI;
public class LoginPage
{
private IWebDriver _driver;
private WebDriverWait _wait;
// Elements declared with annotations
[FindsBy(How = How.Id, Using = "username")]
private IWebElement UsernameInput;
[FindsBy(How = How.Id, Using = "password")]
private IWebElement PasswordInput;
[FindsBy(How = How.CssSelector, Using = "button[type='submit']")]
private IWebElement LoginButton;
[FindsBy(How = How.ClassName, Using = "error-message")]
private IWebElement ErrorMessage;
[FindsBy(How = How.XPath, Using = "//div[@class='welcome']")]
private IWebElement WelcomeMessage;
// Constructor initializes elements
public LoginPage(IWebDriver driver)
{
_driver = driver;
_wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
PageFactory.InitElements(driver, this); // Initialize elements
}
public void Navigate()
{
_driver.Navigate().GoToUrl("https://example.com/login");
}
public void Login(string username, string password)
{
UsernameInput.SendKeys(username);
PasswordInput.SendKeys(password);
LoginButton.Click();
}
public string GetErrorMessage()
{
_wait.Until(d => ErrorMessage.Displayed);
return ErrorMessage.Text;
}
}

FindBy Annotations

Available Annotations (Java/C#)

// Single locator strategies
@FindBy(id = "username")
@FindBy(name = "email")
@FindBy(className = "form-input")
@FindBy(css = "input[type='email']")
@FindBy(xpath = "//input[@placeholder='Email']")
@FindBy(linkText = "Sign Up")
@FindBy(partialLinkText = "Sign")
@FindBy(tagName = "input")
// Alternative syntax
@FindBy(how = How.ID, using = "username")
@FindBy(how = How.CSS, using = ".form-input")

FindBys and FindAll

// FindBys - AND condition (chained locators)
// Finds element matching ALL criteria
@FindBys({
@FindBy(className = "form"),
@FindBy(tagName = "input"),
@FindBy(name = "email")
})
private WebElement emailInForm;
// FindAll - OR condition
// Finds element matching ANY criteria
@FindAll({
@FindBy(id = "email"),
@FindBy(name = "email"),
@FindBy(css = "[type='email']")
})
private WebElement emailField;

List of Elements

Find Multiple Elements
Selenium 3 & 4 Stable
public class SearchResultsPage {
@FindBy(css = ".search-result")
private List<WebElement> searchResults;
@FindBy(css = ".search-result .title")
private List<WebElement> resultTitles;
public SearchResultsPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
public int getResultCount() {
return searchResults.size();
}
public List<String> getResultTitles() {
return resultTitles.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
}
public void clickResult(int index) {
searchResults.get(index).click();
}
}
# Using page-factory library
class SearchResultsPage(PageFactory):
locators = {
"search_results": ("CSS_SELECTOR", ".search-result"),
"result_titles": ("CSS_SELECTOR", ".search-result .title")
}
def __init__(self, driver):
super().__init__(driver)
self.driver = driver
def get_result_count(self):
# For lists, use find_elements
results = self.driver.find_elements(By.CSS_SELECTOR, ".search-result")
return len(results)
def get_result_titles(self):
titles = self.driver.find_elements(By.CSS_SELECTOR, ".search-result .title")
return [title.text for title in titles]
class SearchResultsPage {
constructor(driver) {
this.driver = driver;
}
async getSearchResults() {
return this.driver.findElements(By.css('.search-result'));
}
async getResultCount() {
const results = await this.getSearchResults();
return results.length;
}
async getResultTitles() {
const titles = await this.driver.findElements(
By.css('.search-result .title')
);
return Promise.all(titles.map(t => t.getText()));
}
}
public class SearchResultsPage
{
[FindsBy(How = How.CssSelector, Using = ".search-result")]
private IList<IWebElement> SearchResults;
[FindsBy(How = How.CssSelector, Using = ".search-result .title")]
private IList<IWebElement> ResultTitles;
public SearchResultsPage(IWebDriver driver)
{
PageFactory.InitElements(driver, this);
}
public int GetResultCount()
{
return SearchResults.Count;
}
public List<string> GetResultTitles()
{
return ResultTitles.Select(t => t.Text).ToList();
}
}

Custom Element Locator

Custom Page Factory Locator
Selenium 4 Stable
import org.openqa.selenium.support.pagefactory.*;
// Custom locator for data-testid attribute
public class TestIdLocator implements ElementLocatorFactory {
private final SearchContext searchContext;
public TestIdLocator(SearchContext searchContext) {
this.searchContext = searchContext;
}
@Override
public ElementLocator createLocator(Field field) {
TestId annotation = field.getAnnotation(TestId.class);
if (annotation != null) {
By locator = By.cssSelector("[data-testid='" + annotation.value() + "']");
return new DefaultElementLocator(searchContext, locator);
}
return new DefaultElementLocator(searchContext, field);
}
}
// Custom annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TestId {
String value();
}
// Usage in page class
public class DashboardPage {
@TestId("user-avatar")
private WebElement userAvatar;
@TestId("notification-bell")
private WebElement notificationBell;
public DashboardPage(WebDriver driver) {
PageFactory.initElements(new TestIdLocator(driver), this);
}
}
# Python custom locator pattern
class TestIdLocator:
"""Custom locator for data-testid attributes."""
def __init__(self, driver):
self.driver = driver
def find(self, test_id):
return self.driver.find_element(
By.CSS_SELECTOR, f"[data-testid='{test_id}']"
)
def find_all(self, test_id):
return self.driver.find_elements(
By.CSS_SELECTOR, f"[data-testid='{test_id}']"
)
class DashboardPage:
def __init__(self, driver):
self.driver = driver
self.locator = TestIdLocator(driver)
@property
def user_avatar(self):
return self.locator.find("user-avatar")
@property
def notification_bell(self):
return self.locator.find("notification-bell")
// JavaScript custom locator
class TestIdLocator {
constructor(driver) {
this.driver = driver;
}
find(testId) {
return this.driver.findElement(
By.css(`[data-testid='${testId}']`)
);
}
findAll(testId) {
return this.driver.findElements(
By.css(`[data-testid='${testId}']`)
);
}
}
class DashboardPage {
constructor(driver) {
this.driver = driver;
this.locator = new TestIdLocator(driver);
}
get userAvatar() {
return this.locator.find('user-avatar');
}
get notificationBell() {
return this.locator.find('notification-bell');
}
}
// C# custom locator
public class TestIdAttribute : Attribute
{
public string Value { get; }
public TestIdAttribute(string value) => Value = value;
}
public class TestIdLocator
{
private readonly IWebDriver _driver;
public TestIdLocator(IWebDriver driver) => _driver = driver;
public IWebElement Find(string testId)
{
return _driver.FindElement(
By.CssSelector($"[data-testid='{testId}']")
);
}
}
public class DashboardPage
{
private readonly TestIdLocator _locator;
public DashboardPage(IWebDriver driver)
{
_locator = new TestIdLocator(driver);
}
public IWebElement UserAvatar => _locator.Find("user-avatar");
public IWebElement NotificationBell => _locator.Find("notification-bell");
}

Handling Stale Elements

Page Factory uses proxies that re-find elements on each access:

// Standard approach - element is found once
WebElement button = driver.findElement(By.id("submit"));
// If page refreshes, button becomes stale
// Page Factory - proxy re-finds element on each interaction
@FindBy(id = "submit")
private WebElement submitButton;
// Element is re-found each time submitButton is accessed

Best Practices

PracticeDescription
Initialize in constructorAlways call PageFactory.initElements()
Use meaningful namesloginButton not btn1
Combine with waitsAdd explicit waits in methods
Keep locators simplePrefer ID/CSS over complex XPath
Document elementsComment non-obvious locators

When to Use Page Factory

Use Page Factory when:

  • Working with Java or C#
  • You want cleaner, declarative code
  • Elements are relatively stable
  • You need automatic element re-lookup

Avoid Page Factory when:

  • Elements require complex logic to locate
  • You need fine-grained control over finding
  • Working with highly dynamic pages

Next Steps