Left CoastTech
January 2025 10 min read

Beyond Page Objects: Why Your Next Framework Should Use a Component-Object Model

For years, the Page-Object Model (POM) has been the bedrock of UI test automation, and for good reason. It provides a crucial layer of abstraction, separating test logic from the messy details of the DOM. But in today's world of complex, component-based frontends built with frameworks like Angular and React, the classic POM is starting to show its cracks.

If you've ever found yourself with a BasePage file that's thousands of lines long, or you've had to update the same CSS selector for a date-picker in ten different page objects after a minor UI change, you've felt this pain. You're spending more time on maintenance than on building new, valuable test coverage.

The problem is that a pure Page-Object Model forces us to model a component-based application in a page-centric way. It's like trying to describe a city built with LEGOs by only talking about the finished buildings, ignoring the reusable bricks themselves.

There is a better way. By evolving our approach to a Component-Object Model (COM), we can create frameworks that are not only more powerful and resilient but also dramatically faster to build with.

The Breaking Point: The Overloaded Base Page

Visualization of a monolithic, overloaded BasePage file

The classic symptom of an aging POM is an out-of-control BasePage. This is where we tend to place the logic for every generic, reusable element that appears across the site—headers, footers, navigation menus, and search bars. Initially, this seems logical. But over time, this file becomes a monolithic dumping ground for dozens of unrelated UI elements. It grows impossible to navigate, violates the Single Responsibility Principle, and turns a simple update into a high-risk archeological dig.

When a single date-picker widget appears on ten different pages, you're faced with a terrible choice: either bloat your BasePage even more, or duplicate the date-picker's logic and locators across ten different page-object files. Both paths lead to technical debt and a brittle, frustrating developer experience for your SDETs.

The Solution: Think in Components, Not Just Pages

Component-based framework architecture with reusable building blocks

A Component-Object Model isn't a replacement for POM; it's a powerful enhancement. The philosophy is simple: model your test framework the same way your frontend developers model the application.

Instead of just having "Page" objects, you introduce "Component" objects.

A Component is a self-contained, interactive piece of the UI. It has its own elements, its own methods, and its own logic. Good candidates are the reusable, interactive widgets your application is built from: a complex data grid, a user profile card, a search bar, a date-picker.

In our Angular-based projects, for example, any interactive Material component is a prime candidate for a COM class.

The "Before and After" in Practice

Reusable component architecture showing single source of truth

Imagine you need to test sorting a column in a complex data table and verifying the first row's data.

The Old Way (POM)

Your UsersPage.ts file would have a dozen methods and locators: getTable(), getSortableHeader(), clickSortIcon(), waitForTableToReload(), getFirstRowData(), getCellsFromRow(), and so on. If another page has a similar table, you'd duplicate all that logic.

The New Way (COM)

You create a single, reusable DataTable.ts component class. Your test code becomes stunningly simple and readable:

// In UsersPage.ts
public userTable = new DataTable(by.css('app-user-table'));

// In your test file
await usersPage.userTable.sortByColumn('Last Name');
const firstRow = await usersPage.userTable.getRow(0);
expect(firstRow['Last Name']).toBe('Aardvark');

Now, when your frontend team decides to redesign the table's sorting icon, you update one fileDataTable.ts—and every test that uses a table is instantly fixed. No hunting through dozens of page objects. No duplication. Just clean, maintainable code.

The Next Level: Composition over Inheritance with Mixins

Holographic blueprint showing modular composition and mixins architecture

This component-based approach unlocks an even more powerful pattern. What if one data table needs sorting, another needs filtering, and a third needs both? With traditional inheritance, you'd end up in a messy web of classes like SortableDataTable, FilterableDataTable, and SortableFilterableDataTable.

Instead, we favor composition over inheritance. We create small, single-purpose Mixins that can be applied to a base component to add new functionality. A Mixin is essentially a function that takes a component class and returns a new class with added capabilities.

// A Mixin that adds sorting capabilities
function withSorting<T extends BaseComponent>(base: T) {
  return class extends base {
    async sortByColumn(columnName: string) {
      // ... logic to click the sort header for that column
    }
  };
}

// A Mixin that adds filtering
function withFiltering<T extends BaseComponent>(base: T) {
  return class extends base {
    async filterBy(column: string, value: string) {
      // ... logic to apply a filter
    }
  };
}

// Now, compose the exact component you need
const SortableAndFilterableTable = withFiltering(withSorting(DataTable));

// Use it on your page
public adminTable = new SortableAndFilterableTable(by.css('app-admin-table'));

This approach gives you a clean, modular, and incredibly flexible API for your framework. You build small, reusable behaviors and compose them as needed, keeping your core components lean and your tests easy to understand.

Real-World Benefits: Speed, Resilience, and Clarity

The Component-Object Model delivers concrete, measurable improvements:

1. Massive Reduction in Code Duplication

Instead of reimplementing a modal dialog's "close" behavior on five different pages, you implement it once in a Modal.ts component. Multiply that across every reusable widget in your app, and you've just cut your framework's size in half while doubling its stability.

2. Faster Test Development

When your SDETs can pull from a library of pre-built, well-tested components, they stop reinventing the wheel. Building a new test becomes a matter of composing existing, reliable building blocks. That means faster time-to-value and more test coverage for the same effort.

3. Resilience to UI Changes

Modern web apps change constantly. But component-based UIs change in predictable ways: a Material date-picker gets updated, or a search bar gets new autocomplete logic. When your framework mirrors that structure, you localize the impact of change. One component update fixes ten pages of tests.

4. Better Alignment with Dev Teams

When your frontend engineers talk about "the UserCard component," and your test code has a UserCard.ts class, everyone's speaking the same language. That shared mental model makes collaboration smoother and helps QA catch bugs earlier—before they propagate across the app.

How to Start Building with COM

You don't need to rewrite your entire framework overnight. Here's a practical, incremental approach:

Step 1: Identify Your Most Reused Widgets

Look at your existing page objects. What elements appear over and over? Common culprits:

  • Navigation menus
  • Search bars with autocomplete
  • Data tables or grids
  • Modal dialogs
  • Date pickers, dropdowns, multi-select widgets
  • User profile cards or info panels

Pick the one that causes you the most pain (usually the one you've duplicated the most) and start there.

Step 2: Extract a Component Class

Create a new file, like components/SearchBar.ts. Move all the locators and methods related to that widget into this class. Make it self-contained: it should know how to find itself on the page (via a passed-in root locator) and how to perform all its key interactions.

Example structure:

export class SearchBar {
  private root: Locator;
  private inputField: Locator;
  private suggestionsDropdown: Locator;

  constructor(rootLocator: Locator) {
    this.root = rootLocator;
    this.inputField = this.root.locator('input[type="search"]');
    this.suggestionsDropdown = this.root.locator('.autocomplete-suggestions');
  }

  async search(query: string): Promise<void> {
    await this.inputField.fill(query);
    await this.inputField.press('Enter');
  }

  async selectSuggestion(text: string): Promise<void> {
    await this.inputField.fill(text);
    await this.suggestionsDropdown.locator(`text=${text}`).click();
  }

  async isVisible(): Promise<boolean> {
    return await this.root.isVisible();
  }
}

Step 3: Use Composition in Your Page Objects

Now, instead of cramming search bar logic into every page, your page objects simply compose with the component:

export class HomePage extends BasePage {
  public searchBar = new SearchBar(this.page.locator('header .search-container'));

  // Other page-specific elements and methods...
}

// In your test:
await homePage.searchBar.search('test automation');
await expect(homePage.resultsHeader).toBeVisible();

Notice how clean this is. The page object is now focused on the page-level logic and layout, while the component handles the widget-level interactions.

Step 4: Iterate and Expand

Once you've proven the value with one component, keep going. Extract the next painful widget. Over time, you'll build a robust component library that makes new test development feel almost effortless.

Common Pitfalls (and How to Avoid Them)

Pitfall 1: Over-Abstracting Too Early

Don't try to make every tiny element a component. A simple button or text label doesn't need its own class. Components should be interactive, complex, and reused. If it's only on one page and has two lines of code, leave it in the page object.

Pitfall 2: Ignoring Component State

Components can have different states (loading, error, collapsed, expanded). Make sure your component classes can handle these gracefully. Use methods like waitForReady() or state checks to avoid flakiness.

Pitfall 3: Not Using Composition

Some teams build component classes but then use inheritance instead of composition. Resist this urge. Favor composition over inheritance. Page objects should have components, not be components. This keeps your architecture flexible and your code easy to reason about.

When POM Alone Is Still Fine

To be clear: not every project needs a full Component-Object Model. If you're testing a simple, page-centric application with minimal reuse—say, a basic marketing site or a legacy app with unique page layouts—a traditional POM may be perfectly adequate.

But if you're working on a modern SPA built with React, Angular, Vue, or similar, and you're seeing duplication and maintenance pain, COM is the evolution you need.

The Bottom Line: Future-Proof Your Framework

The Component-Object Model isn't just a trendy refactor—it's a recognition that our test frameworks should mirror the architecture of the applications we test. When frontend teams build in components, we should test in components.

The payoff is real:

  • Less code to maintain
  • Faster test development
  • Greater resilience to UI changes
  • Better collaboration between dev and QA

If you're starting a new framework today, build it with components from day one. If you're maintaining a legacy POM that's starting to crack under its own weight, start extracting components incrementally. Either way, you'll thank yourself six months from now when a major UI refactor doesn't bring your entire test suite to its knees.

By shifting your perspective from pages to components, you align your test automation strategy with modern web development. You stop fighting your application's architecture and start leveraging it. The result is a more resilient, maintainable, and scalable test suite that reduces duplication and frees up your engineers to focus on what matters: delivering quality software, faster.

About the Author: The founder of Left Coast Tech has spent over a decade designing test automation frameworks for companies ranging from startups to Fortune 500 enterprises, specializing in building frameworks that scale with modern component-based frontends.

Need Help Modernizing Your Test Framework?

We help teams migrate from brittle, page-centric frameworks to maintainable, component-based architectures. Whether you're starting fresh or refactoring an existing suite, we can guide you to a framework that scales.

Schedule a Consultation