Implementing the Page Object Model (POM)
Implement the Page Object Model pattern for maintainable Maestro test suites.
As the number of test grows, one problem shows up quickly, you will use selectors in every Flow file.
Hardcoded ids spread across dozens of YAML files are easy at first, but the moment a developer renames a button or changes a label, you may find yourself updating several tests just to fix one UI change. This is an example of a non-scalable approach.
This is exactly the problem the Page Object Model (POM) is designed to solve.
POM is a design pattern that introduces an abstraction layer between your test logic and your UI selectors. Instead of referencing element ids directly in your Flows, you reference a central JavaScript object. When the UI changes, you update the selector in one place, and every test that depends on it keeps working.
The core idea
The guiding principle behind POM is simple:
Separate what an element is from how your test interacts with it.
You achieve this by:
Defining selectors in JavaScript files using the
outputobject.Loading those definitions at runtime.
Referencing variables (not raw
ids) in your Flows.
Once you adopt this pattern, your Flows describe user intent, not technical implementation details.
The basic workflow
At a high level, implementing POM follows this workflow:
Store
id,text, or regex patterns in.jsfiles usingoutput.Group selectors logically by screen, feature, or platform.
Use
runScriptdirectly or a centralized loader Flow to make selectors available.Reference selectors using
${output...}intapOn,inputText,assertVisible, and other commands.
The following sections describe how you can adopt the POM approach.
1. Define your first page object
Start by creating a JavaScript file that represents a screen in your app. For example, a login screen.
This file acts like a dictionary for the Login screen. Each key describes what the element is, while the value defines how Maestro can find it.
To use the element selectors, you need to use runScript to load the screen element variables. As a result, there are no raw IDs scattered throughout the test, and the intent of each action is immediately clear.
2. Organize complex screens with nested objects
As screens grow more complex, a flat list of selectors can become difficult to manage. Using POM, you can nest objects, mirroring the structure of your UI.
This is especially useful for reusable components like cards, toolbars, or navigation menus. The following example shows an object used to describe a card:
The structure reflects the UI hierarchy. Related elements live together, and naming collisions are avoided.
The process of using nested selectors in a Flow is similar to working with a JavaScript object defined in a JSON file. The selector path itself documents the UI structure, making tests easier to understand and maintain.
POM benefits and trade-offs
Adopting POM is a strategic choice for suite maintainability, but it involves a specific trade-off:
Centralized maintenance: If
btn_create_virtualchanges tofab_add, you update one file, not dozens of tests.Namespacing: Nested objects prevent collisions between common names like
title,submit, orbutton.Regex support: Maestro treats selector strings as regular expressions by default. You can store dynamic patterns like:
and reuse them consistently across tests.
Readable failures: When a test fails, variable names such as
loginBtnare far more informative than raw IDs.
While POM reduces maintenance, it can come at a small cost to test readability. The direct approach is Readable but fragile:
Meanwhile, POM approach is abstracted but robust:
Choose POM when the stability and scalability of your automation suite outweigh the benefit of reading raw text within the YAML file.
Recommended folder structure
Keeping selectors separate from test logic will improve your organization. A dedicated elements folder makes this separation explicit.
For cross-platform apps, organizing by platform is also strongly recommended. Your tests remain platform-agnostic, while selectors stay platform-specific.
The loader strategy
As your project grows, manually calling runScript in every Flow becomes repetitive and error-prone. To solve this problem, you can use a Loader Flow. The loader’s only responsibility is to load all page objects once, as in the following example:
If you use the loader as a subflow, it initializes your entire selector schema at the beginning of a test run. This way, every test starts from a known, consistent state, and no selector is accidentally forgotten.
Handle cross-platform differences
Android, iOS, and Web often use different IDs for the same functional element. POM makes this manageable by keeping variable names consistent while swapping implementations.
By applying this approach, each platform loads its own selectors, but your test logic never changes.
Related content
To master the logic used in the Page Object Model, explore these deep-dives:
JavaScript: Learn how to use JavaScript to manage shared data and complex structures.
Parameters and constants: Learn how the
outputobject persists across Flows.Conditions: Master the
whenclause for platform-specific logic.
Last updated