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
| Aspect | Page Object | Page Factory |
|---|---|---|
| Element declaration | By locators | Annotations |
| Initialization | Manual findElement | Automatic via initElements |
| Lazy loading | Manual | Built-in |
| Language support | All | Java, 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 PageFactoryfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom 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 approachfrom 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 libraryclass 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 attributepublic 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 classpublic 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 patternclass 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 locatorclass 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 locatorpublic 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 onceWebElement 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 accessedBest Practices
| Practice | Description |
|---|---|
| Initialize in constructor | Always call PageFactory.initElements() |
| Use meaningful names | loginButton not btn1 |
| Combine with waits | Add explicit waits in methods |
| Keep locators simple | Prefer ID/CSS over complex XPath |
| Document elements | Comment 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
- Page Object Model - Foundation pattern
- Data-Driven Testing - External test data
- Explicit Waits - Synchronization strategies