advanced21 min

Page Objects, Component Objects, and Test Data

Structuring a growing Selenium suite around page objects and smaller component objects, and designing test data with the same isolation discipline every real automation tool needs.

What you'll learn

  • Design a page object class encapsulating one page's locators and actions
  • Extract a repeated UI fragment into a component object shared across multiple page objects
  • Design test data that avoids collisions between tests

Prerequisites

Explanation

A page object in Selenium follows the same core principle as in any browser-automation tool: a class encapsulating one page's locators and the actions available on it, so tests read as intent rather than raw Selenium calls. class LoginPage { private WebDriver driver; private By usernameField = By.id("username"); public LoginPage(WebDriver driver) { this.driver = driver; } public DashboardPage signIn(String user, String pass) { driver.findElement(usernameField).sendKeys(user); ... return new DashboardPage(driver); } } — note the return type: a well-designed Selenium page object's action methods often return the next page object the action navigates to, letting a test chain calls in a way that mirrors the real navigation flow: DashboardPage dashboard = new LoginPage(driver).signIn(user, pass);.

A component object applies the identical principle to a UI fragment that's reused across multiple, different pages — a navigation header, a search bar, a modal dialog — rather than one whole page: class SearchBarComponent { private WebElement root; ... public SearchBarComponent(WebElement root) { this.root = root; } public void search(String query) { root.findElement(By.cssSelector("input")).sendKeys(query); ... } }, constructed from a WebElement representing that fragment's root, and usable from any page object that contains it, without duplicating the search bar's locators and interaction logic in every page that happens to include it. This is exactly the same "genuine reuse earns the structure" principle covered for browser-automation page objects generally: a component object pays for itself specifically because multiple page objects share it, not automatically for any repeated element.

Test data design for Selenium suites needs the identical discipline covered for browser-automation tools generally, for the identical underlying reason: tests running in parallel (JUnit supports this too, covered later in this course) or repeatedly over time must not collide on shared, hard-coded data. Generating a unique value per test run — combining a timestamp with a random or incrementing suffix for a test account's email, or using UUID.randomUUID() — avoids two tests both trying to register the exact same account and one failing with an unrelated "already exists" error. This isn't a Playwright-specific or a Selenium-specific concern; it's a fundamental property of test-data design that applies to essentially any automation tool capable of running tests in parallel or repeatedly.

Example

Modeling a page object returning the next page object, and a component object reused across multiple pages, as data.

class DashboardPageModel {
  constructor(driver) { this.driver = driver; }
  describe() { return "on the dashboard"; }
}
class LoginPageModel {
  constructor(driver) { this.driver = driver; }
  signIn(user, pass) {
    this.driver.actions.push({ action: "sendKeys", field: "username", value: user });
    this.driver.actions.push({ action: "sendKeys", field: "password", value: pass });
    this.driver.actions.push({ action: "click", target: "Sign in" });
    return new DashboardPageModel(this.driver); // returns the NEXT page -- enables chaining
  }
}

const driver = { actions: [] };
const dashboard = new LoginPageModel(driver).signIn("alice", "secret");
console.log(dashboard.describe()); // "on the dashboard" -- chained directly from signIn's return value
console.log(driver.actions.length); // 3

class SearchBarComponent {
  constructor(root) { this.root = root; }
  search(query) { this.root.actions.push({ action: "search", query }); }
}
// Reused identically from TWO different page objects that both contain a search bar:
const headerSearchBar = new SearchBarComponent(driver);
const sidebarSearchBar = new SearchBarComponent(driver);
headerSearchBar.search("playwright");
sidebarSearchBar.search("selenium");
console.log(driver.actions.length); // 5 -- both component instances shared the same underlying locator/interaction logic

Try it yourself

Chain a second action after signIn: call dashboard.describe() to confirm the returned object is genuinely usable.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Model a page object returning the next page: write class SearchResultsPage with a method openFirstResult() that returns a new DetailPage instance. Write class DetailPage with a method title() returning 'detail page'.

Checks: openFirstResult returns a real, chainable DetailPage instance

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write generateUniqueTestData(prefix, counter) returning prefix + '-' + counter + '@example.com' -- a simple, deterministic unique-data generator. Then write allUnique(emails) returning true only if every email in the array is distinct from every other (use a Set to check).

Checks: generates the correct email format · confirms generated emails from distinct counters are all unique · detects a genuine duplicate

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Writing a page object action method that returns void when the action genuinely navigates to a new page -- returning the next page object instead enables chaining that mirrors the real navigation flow and catches a mismatched return type as a compile error if the wrong page is returned.
  • Duplicating a repeated UI fragment's locators and logic across every page object that contains it, instead of extracting a component object -- this means a change to that fragment now requires updating every page object separately, and copies can drift out of sync.
  • Hard-coding identical test data across tests that might run in parallel or repeatedly -- the exact same collision risk this course has covered in the context of browser sessions applies to any shared, non-unique test data.

Knowledge check

Knowledge check

1. Why does a well-designed Selenium page object's navigation action often return the NEXT page object, rather than void?
2. When does a component object (as opposed to a full page object) earn its structure?
3. Why does test-data collision risk apply to Selenium suites just as much as any other automation tool?

Takeaway

A Selenium page object's navigation methods should return the destination page object to enable safe, chainable test code; component objects earn their structure through genuine reuse across multiple pages, just like page objects do; and test-data collision risk under parallel or repeated execution is a universal concern, not specific to any one automation tool.

Summary

Page objects encapsulate a page's locators/actions, with navigation methods returning the next page object to enable chaining and catch mismatches at compile time. Component objects extract a UI fragment reused across multiple page objects. Unique, generated test data (not hard-coded values) avoids collisions under parallel or repeated test execution.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.