A lot of Robot Framework projects still look like plain Selenium scripts with .robot file extensions.
Someone imports webdriver, creates driver = webdriver.Chrome(), then calls find_element and send_keys in Python helpers. Robot Framework runs the suite, but readable keywords, shared libraries, and consistent waits never show up in the tests.
If you already use Robot Framework with SeleniumLibrary, you do not need the raw WebDriver API. SeleniumLibrary gives you high-level keywords. The Page Object Model gives you structure. Together they keep tests short and UI changes localized.
We published a small MIT template that shows the layout: rf-seleniumlibrary-pageobject-template. It targets Sauce Demo — clone it, run four tests, fork the folder structure.
What breaks when you mix in raw WebDriver
driver = webdriver.Chrome()
driver.find_element(By.ID, "user-name").send_keys("standard_user")
driver.find_element(By.ID, "password").send_keys("secret_sauce")
driver.find_element(By.ID, "login-button").click()
Fine for a script. Painful in a growing suite.
Locators spread across helpers and test files. Waits become time.sleep(2) in one place and missing in another. You end up maintaining SeleniumLibrary and a parallel WebDriver stack. CI fails on a Tuesday night and you are not sure which path opened the browser.
Before and after
| Before | After |
|---|---|
driver.find_element(...).send_keys(...) |
Login With Valid Credentials ${VALID_USER} ${VALID_PASSWORD} |
| Locators in every file |
LoginLocators.USERNAME in one module |
| Ad-hoc sleeps |
wait_until_element_is_visible in BasePage.click()
|
| Two browser stacks | One SeleniumLibrary instance per suite |
Four layers
| Layer | Job | Example |
|---|---|---|
| Locators | Selectors per screen | login_locators.py |
| BasePage | Shared waits and actions |
click(), enter_text()
|
| Page library | Screen keywords | LoginPage.login() |
| Robot test | Scenario only | Inventory Should Be Visible |
Folder layout in the repo:
resources/locators/ → selectors
pages/ → Python page libraries
tests/ → thin .robot files
Locators stay in one place
class LoginLocators:
USERNAME = "id:user-name"
PASSWORD = "id:password"
SUBMIT = "id:login-button"
Prefer stable attributes like data-test when the app exposes them.
BasePage owns the waits
def click(self, locator):
self.seleniumlib.wait_until_element_is_visible(locator)
self.seleniumlib.click_element(locator)
@property
def seleniumlib(self):
return BuiltIn().get_library_instance("SeleniumLibrary")
Import SeleniumLibrary once in common.resource. Page objects look up that same instance — no driver argument passed between keywords. If you create driver = webdriver.Chrome() outside SeleniumLibrary, your page keywords will not see that browser.
A thin test
*** Test Cases ***
Valid Login
Login With Valid Credentials ${VALID_USER} ${VALID_PASSWORD}
Inventory Should Be Visible
No locators. No driver setup. Browser open/close in Test Setup / Test Teardown.
Quick start
git clone https://github.com/gitoza-io/rf-seleniumlibrary-pageobject-template.git
cd rf-seleniumlibrary-pageobject-template
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
robot --pythonpath . --outputdir results tests/
Use --pythonpath . from the repo root so pages.* imports resolve. Four passing tests = layout is wired.
Migrating without a rewrite
- Move locators to
resources/locators/ - Add
BasePagewithclick/enter_text - Wrap login as the first page library
- Import
SeleniumLibraryonce incommon.resource - Rewrite one
.robotfile, deletewebdriverimports from its helpers
Repeat screen by screen. Leave the rest alone until you touch that feature.
Playwright and Cypress are valid for greenfield work. Plenty of teams still run RF and SeleniumLibrary in production — for them, dropping find_element for Page Objects is a low-risk upgrade that pays off on the next UI refactor.
Template repo: github.com/gitoza-io/rf-seleniumlibrary-pageobject-template