Implementing the Page Object Model (POM)
Implement the Page Object Model pattern to keep large flow suites maintainable.
As the number of flows grows, selectors show up in every flow file. Writing ids directly into dozens of YAML files is quick at first, but when a developer renames a button or a label, you have to update many flows for one small UI change. This does not scale.
The Page Object Model (POM) solves exactly this problem. POM is a way to organize flows: you collect the element names of each screen (each "page") in its own file, and flows use those names instead of raw ids. When the UI changes, you fix one place, and every flow that depends on it keeps working.
In Astra Scout, these files are JavaScript files. They store selectors in the output object (the place that holds variables shared across the steps of a flow).
The core idea
The 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.
As a result, flows describe what the user wants to do, not technical details.
The basic workflow
To apply POM:
- Store
id,text, or regex patterns in.jsfiles usingoutput. - Group selectors logically by screen, feature, or platform.
- Use
runScript, or a shared loader flow, to load the selectors into the flow. - Reference selectors using
${output...}intapOn,inputText,assertVisible, and other commands.
The sections below go through each step.
1. Define your first page object
Start by creating a JavaScript file that represents a screen in your app. For example, a login screen.
// login.js
output.login = {
email: 'email_text',
password: 'password_text',
loginBtn: 'loginButton',
registerBtn: 'registerButton'
}This file is the "dictionary" of the Login screen. The key says what the element is, and the value tells Astra Scout how to find it.
Use runScript to load the file into the flow. After that the flow has no raw IDs, and each line shows its intent clearly.
# login_flow.yaml
---
- runScript: login.js
- tapOn:
id: ${output.login.email}
- inputText: "simon@maestro.com"
- tapOn:
id: ${output.login.password}
- inputText: ${PASSWORD}
- tapOn:
id: ${output.login.loginBtn}2. Organize complex screens with nested objects
On complex screens, a flat list of selectors is hard to manage. You can nest objects to follow the structure of the UI. This suits reusable components such as cards, toolbars, or navigation menus. The following example describes a card:
// cards.js
output.cards = {
cardTitle: 'card_title',
virtualCards: {
createBtn: 'btn_create_virtual',
details: {
number: 'card_number',
expiry: 'card_expiry'
}
}
}This structure matches the UI hierarchy: related elements sit together and names do not clash. In a flow, you follow the path as you would in a JavaScript object. The path itself shows where the element sits in the UI, so the flow is easier to read and update.
- runScript: cards.js
- tapOn:
id: ${output.cards.virtualCards.createBtn}
- assertVisible:
id: ${output.cards.virtualCards.details.number}POM benefits and trade-offs
POM makes a suite easier to maintain, but it has one drawback:
Fix in one place: If
btn_create_virtualchanges tofab_add, you update one file, not dozens of tests.No name clashes: Nested objects prevent collisions between common names like
title,submit, orbutton.Regex support: Astra Scout reads selector strings as regex (patterns that describe text, see Selectors). You can store patterns for changing text, like:
javascriptoutput.welcomeBanner = 'Welcome back, .*'and reuse them across flows.
Readable failures: when a flow fails, a name such as
loginBtnis easier to understand than a raw ID.
The drawback: flows are slightly less direct to read. Writing the text directly is easy to read but breaks easily when the UI changes:
- tapOn: Add to BasketWith POM it is a little less direct, but it lasts:
- tapOn: ${output.itemPage.addButton}Choose POM when your suite is large and needs to last more than it needs to be readable line by line in YAML.
Recommended folder structure
Keep selectors apart from the flows, for example in a dedicated elements folder.
For apps that run on both Android and iOS, divide the folders by platform. The flows stay the same for both platforms, while the selectors are specific to each platform.
android/
elements/
login.js
home.js
loadElements.yaml
tests/
login_flow.yaml
ios/
elements/
login.js
home.js
loadElements.yaml
tests/
login_flow.yamlThe loader strategy
As your project grows, calling runScript in every flow becomes repetitive and easy to get wrong. Use a loader flow instead. Its only job is to load all page objects once:
# elements/loadElements.yaml
appId: com.example.app
---
- runScript: cards.js
- runScript: login.js
- runScript: home.js
- runScript: nav.jsCall the loader as a subflow at the start of a flow. All selectors are loaded, so every flow starts the same way and no selector is forgotten.
---
- runFlow: elements/loadElements.yaml
- launchApp
# All selectors are now available via ${output...}Handle cross-platform differences
Android and iOS often use different IDs for the same element. With POM you keep the names the same and only change which file is loaded.
- runFlow:
when:
platform: Android
commands:
- runScript: android/elements/login.js
- runFlow:
when:
platform: iOS
commands:
- runScript: ios/elements/login.jsEach platform loads its own selectors, while the steps of the flow stay the same.
- tapOn:
id: ${output.login.loginBtn}Related content
Related pages:
- JavaScript: use JavaScript to keep shared data.
- Parameters and constants: how the
outputobject is kept across flows. - Conditions: use
whenfor platform-specific parts.
