A data-driven Page Object Model framework for Robot Framework where locators and element definitions live in YAML files, not in .robot code. Adding a page, swapping an environment, or changing a locator strategy requires no changes to keyword logic.
- Why This Exists
- Architecture
- Quick Start
- Running Tests
- Core Registry Structure
- Register an App
- Register Urls
- Register Users
- Register Pages
- Object Registry
- Definition Registry
- Adding a New Page — Worked Example
Standard Robot Framework POM implementations hardcode locators inside .robot keyword files. This means a locator change requires a code change, locators for different environments must be branched or conditionally set, and adding a new page requires creating new keyword files.
This framework separates what to find (YAML) from how to find it (keyword logic):
| Concern | Where it lives |
|---|---|
| Locator strategies & XPath construction | LocatorBuilder.robot (written once) |
| Locator definitions per page | ObjectRegistry/{App}/{Page}.yaml |
| Assertion rules per page | DataSets/{App}/{Env}/{Page}Definitions.yaml |
| URL and user data per environment | DataSets/{App}/{Env}/UrlRegistry.yaml etc. |
The result: adding a new page is a YAML-only change. Swapping environments passes a single CLI variable.
Trade-off: The dynamic dispatch in LocatorBuilder.robot (using Run Keyword to call strategy-named keywords) makes the locator-build path less statically traceable than hardcoded POM files. This is a deliberate choice — the extensibility gain outweighs the traceability cost for teams adding pages frequently.
Tests/*.robot
│
└─► Bindings (Given/When/Then wrappers)
│
└─► Page.robot ← thin facade; consumers import this one file
│
┌─────────┼──────────────────┐
▼ ▼ ▼
Navigation LocatorBuilder ElementActions
.robot .robot .robot
(URL ↔ (Build Locator (Get Texts,
page name) dispatches to Assert Count,
Build Locator: Sort Order)
{Strategy})
│
┌─────────┴──────────────┐
▼ ▼
ObjectRegistry YAMLs DataSets YAMLs
(locator definitions) (URLs, users, element definitions)
Resources/PO/ Settings/DataSets/
ObjectRegistry/ {target_app}/{environment}/
{target_app}/{Page}.yaml
| File | Role |
|---|---|
| Settings/AppRegistry.py | RF variable provider — returns app config dict for ${target_app} |
| Settings/YamlValidator.py | Validates all YAML registries before the browser starts; also a CLI tool for CI |
| Settings/_Settings.robot | Imports AppRegistry and the active DataSet; defines default CLI variables |
| Resources/PO/_Keywords/Page.robot | Thin facade — consumers import this one file and get everything transitively |
| Resources/PO/_Keywords/Navigation.robot | PO: Page: Navigate To, PO: Page: Get (URL → page name) |
| Resources/PO/_Keywords/LocatorBuilder.robot | All Build Locator: {Strategy} keywords; dispatches via Run Keyword |
| Resources/PO/_Keywords/ElementActions.robot | Element text retrieval, count and sort assertions |
| Resources/PO/_Keywords/_AssertDefinitions.robot | Iterates ${PageName_Definitions} and dispatches assertion keywords |
| Resources/Bindings/PageBindings.robot | BDD-style Given/When/Then bindings consumed by test cases |
Prerequisites: Python 3.9+
pip install -r requirements.txt
rfbrowser initTests run against live demo sites (saucedemo.com and the-internet.herokuapp.com). An internet connection is required.
Each app must be run with its own target_app variable — the settings (URL registry, page objects, definitions) are all app-scoped and loaded at suite initialisation time, so mixing two apps in one robot . command is not supported.
Run all apps (uses run_tests.ps1):
.\run_tests.ps1 # headed
.\run_tests.ps1 -Headless # headlessRun a single app directly:
robot -v target_app:SwagLabs -i swaglabs -d Tests/Reports .
robot -v target_app:ChallengingDom -i ChallengingDom -d Tests/Reports .Override environment:
robot -v target_app:SwagLabs -v environment:Dev -i swaglabs -d Tests/Reports .Run a single test by name:
robot -v target_app:SwagLabs -t "Scenario: Assert Login Page Elements" .Available variables
| Variable | Default | Options |
|---|---|---|
target_app |
SwagLabs |
SwagLabs, ChallengingDom |
environment |
Staging |
Dev, Staging, UAT |
browser |
chromium |
chromium, firefox, webkit, msedge |
headless |
${FALSE} |
True, False |
└───Settings
│ │ _Settings.robot
│ │ AppRegistry.py
│ │
│ └───DataSets
│ │ │
│ │ └───ChallengingDom
│ │ │ └───Dev / Staging / UAT
│ │ │ _DatasetRegistry.robot
│ │ │ MainPageDefinitions.yaml
│ │ │ UrlRegistry.yaml
│ │ │ UserRegistry.yaml
│ │ │
│ │ └───SwagLabs
│ │ └───Dev / Staging / UAT
│ │ _DatasetRegistry.robot
│ │ LoginPageDefinitions.yaml
│ │ ProductsPageDefinitions.yaml
│ │ ShoppingCartPageDefinitions.yaml
│ │ UrlRegistry.yaml
│ │ UserRegistry.yaml
│
└───Resources
└───PO
├───ObjectRegistry
│ ├───ChallengingDom
│ │ MainPage.yaml
│ └───SwagLabs
│ LoginPage.yaml
│ ProductsPage.yaml
│ ShoppingCartPage.yaml
│
└───PageRegistry
_ChallengingDomVariables.robot
_SwagLabsVariables.robot
Settings/AppRegistry.py
Add a new entry in get_variables() keyed by the app name you'll pass as ${target_app}:
app3 = {
'default_page': 'MainPage', # landing page name (must match UrlsToPages key)
'dynamic_url_contains': None, # partial URL fragment for dynamic pages, or None
'dynamic_page_name': None # name for dynamic pages, or None
}
def get_variables(arg):
if arg == 'ExampleApp':
return app3Settings/DataSets/{target_app}/{environment}/UrlRegistry.yaml
BaseUrl:
ExampleApp: https://www.example_app.com/
UrlsToPages:
MainPage: BaseUrl # resolves to BaseUrl directly
ExamplePage: example_page # resolves to BaseUrl + fragmentSettings/DataSets/{target_app}/{environment}/UserRegistry.yaml
UserLogins:
Default:
UserName: standard_user
Password: secret_sauce
Locked:
UserName: locked_out_user
Password: secret_sauceResources/PO/PageRegistry/_{target_app}Variables.robot
Create a file named _{YourApp}Variables.robot — the name is resolved dynamically by LocatorBuilder.robot. It only needs Variables imports pointing to the ObjectRegistry YAMLs:
*** Settings ***
Variables ../ObjectRegistry/${target_app}/ExamplePage.yamlResources/PO/ObjectRegistry/{target_app}/{Page}.yaml
Each YAML file represents one page. The variable block must be named {PageName}_Objects.
LoginPage_Objects:
Username:
LocatorStrategy: XPathLookup
Xpath: //input[@data-test="username"]
Password:
LocatorStrategy: WithAttribute
ElementType: input
Attribute: data-test
Name: password| Strategy | Required fields | Description |
|---|---|---|
XPathLookup |
Xpath |
Direct XPath expression |
WithAttribute |
ElementType, Attribute, Name |
//type[@attr="name"] |
WithText |
ElementType, Text |
//type[normalize-space()="text"] |
WithContainsAttribute |
ElementType, Attribute, Name |
//type[contains(@attr, "name")] |
ParentReferenceWithXpathLookup |
ParentReference + XPathLookup fields |
Parent locator prefixed to child XPath |
ParentReferenceWithAttribute |
ParentReference + WithAttribute fields |
Parent locator prefixed to child attribute locator |
ParentReferenceWithText |
ParentReference + WithText fields |
Parent locator prefixed to child text locator |
ParentReferenceWithContainsAttribute |
ParentReference + WithContainsAttribute fields |
Parent locator prefixed to contains-attribute locator |
ParentReferenceWithType |
ParentReference, ElementType |
Parent locator prefixed to //type |
SelectFromGroupByCSSProperty |
GroupReference, CSSPropertyType, PropertyValue |
Iterates a group of elements and returns the one matching a CSS property value |
Settings/DataSets/{target_app}/{environment}/{Page}Definitions.yaml
Definition files describe what to assert on each page. The variable block must be named {PageName}_Definitions, and element keys must match the corresponding {PageName}_Objects keys.
LoginPage_Definitions:
Username:
ElementCountShouldBe: 1
LoginCredentials:
ElementCountShouldBe: 1
ShouldContain:
- standard_user
- locked_out_userImport each definition file in Settings/DataSets/{target_app}/{environment}/_DatasetRegistry.robot:
*** Settings ***
Variables LoginPageDefinitions.yaml
Variables ProductsPageDefinitions.yaml| Property | Description |
|---|---|
ElementCountShouldBe |
Asserts the element appears exactly N times on the page |
ShouldContain |
Asserts the element's text contains all listed strings |
EachInGroupShouldContain |
For a parent-reference group: asserts each indexed child contains the expected text |
TableContentShouldBe |
Asserts table column content matches a defined structure |
ImageGroupAttributes |
Asserts alt and src attributes for a group of images |
This walkthrough adds a CheckoutPage to the SwagLabs app. Every step is a file you create or extend — no changes to keyword logic required.
Settings/DataSets/SwagLabs/{Env}/UrlRegistry.yaml — repeat for Dev, Staging, UAT:
UrlsToPages:
CheckoutPage: checkout-step-one # appended to BaseUrlResources/PO/ObjectRegistry/SwagLabs/CheckoutPage.yaml
The variable block name must be CheckoutPage_Objects:
CheckoutPage_Objects:
FirstNameInput:
LocatorStrategy: WithAttribute
ElementType: input
Attribute: data-test
Name: firstName
LastNameInput:
LocatorStrategy: WithAttribute
ElementType: input
Attribute: data-test
Name: lastName
ContinueButton:
LocatorStrategy: XPathLookup
Xpath: //input[@data-test="continue"]
FormErrorMessage:
LocatorStrategy: WithContainsAttribute
ElementType: h3
Attribute: data-test
Name: errorResources/PO/PageRegistry/_SwagLabsVariables.robot — add one line:
*** Settings ***
Variables ../ObjectRegistry/${target_app}/CheckoutPage.yamlSettings/DataSets/SwagLabs/{Env}/CheckoutPageDefinitions.yaml — repeat for Dev, Staging, UAT.
Element keys must match CheckoutPage_Objects exactly:
CheckoutPage_Definitions:
FirstNameInput:
ElementCountShouldBe: 1
LastNameInput:
ElementCountShouldBe: 1
ContinueButton:
ElementCountShouldBe: 1Settings/DataSets/SwagLabs/{Env}/_DatasetRegistry.robot — add one line:
*** Settings ***
Variables CheckoutPageDefinitions.yamlPreview any locator without running the browser:
python tools/preview_locator.py SwagLabs CheckoutPage ContinueButton
# [CheckoutPage → ContinueButton]
# Strategy : XPathLookup
# Locator : //input[@data-test="continue"]Run the YAML validator to catch typos before launching the suite:
python Settings/YamlValidator.py SwagLabs Staging
# OK Registry validation passed for SwagLabs/Staging