Robot Framework with SeleniumLibrary: Web Testing Guide
SeleniumLibrary is the standard way to do web UI testing with Robot Framework. It wraps Selenium WebDriver in RF's keyword model, giving you browser automation through readable keyword calls instead of raw Python. This guide covers everything from installation through production-ready patterns.
Installation and Setup
You need Python, Robot Framework, SeleniumLibrary, and a WebDriver:
pip install robotframework
pip install robotframework-seleniumlibraryFor browser drivers, the cleanest approach is webdriver-manager:
pip install webdriver-managerOr install drivers directly:
# ChromeDriver (must match Chrome version)
# Download from: https://chromedriver.chromium.org/downloads
# GeckoDriver for Firefox
# Download from: https://github.com/mozilla/geckodriver/releasesAlternatively, use the newer robotframework-browser (Playwright-based) for better reliability. But SeleniumLibrary has wider adoption and more documentation, so this guide sticks with it.
Import SeleniumLibrary in your test file:
*** Settings ***
Library SeleniumLibrary timeout=10s implicit_wait=0s run_on_failure=Capture Page ScreenshotThe run_on_failure argument is important — it specifies what keyword to run automatically when any SeleniumLibrary keyword fails. Capture Page Screenshot is the standard choice.
Browser Setup and Configuration
Opening and closing browsers:
*** Variables ***
${BROWSER} chrome
${BASE_URL} https://example.com
${HEADLESS} ${FALSE}
*** Keywords ***
Open Test Browser
${options}= Evaluate sys.modules['selenium.webdriver'].ChromeOptions() sys
Run Keyword If ${HEADLESS} Call Method ${options} add_argument --headless
Call Method ${options} add_argument --no-sandbox
Call Method ${options} add_argument --disable-dev-shm-usage
Call Method ${options} add_argument --window-size\=1920,1080
Create Webdriver Chrome options=${options}
Set Window Size 1920 1080
Go To ${BASE_URL}
Close Test Browser
Close BrowserFor headless CI execution, pass the variable at runtime:
robot --variable HEADLESS:True tests/Multiple browsers in one suite:
*** Test Cases ***
Cross Browser Login Test
Open Browser ${BASE_URL}/login chrome alias=chrome_browser
Open Browser ${BASE_URL}/login firefox alias=firefox_browser
Switch Browser chrome_browser
Input Text id=username admin
Switch Browser firefox_browser
Input Text id=username admin
[Teardown] Close All BrowsersLocator Strategies
SeleniumLibrary supports all Selenium locator strategies with a consistent syntax:
*** Test Cases ***
Locator Strategy Examples
# ID (default if no prefix)
Click Element id=submit-btn
# CSS selector
Click Element css=button.primary[type='submit']
# XPath
Click Element xpath=//button[contains(text(),'Submit')]
# Name attribute
Input Text name=username admin
# Class name (first match)
Click Element class=submit-button
# Link text (for <a> tags)
Click Element link=Forgot Password?
# Partial link text
Click Element partial link=Forgot
# Tag name
Click Element tag=buttonPractical locator guidelines:
Prefer stable, semantic locators. In order from most to least stable:
id=— fastest, most stable when IDs are stablecss=withdata-testidattributes — resilient to styling changescss=with semantic structure —css=form.login button[type=submit]xpath=with text content —xpath=//button[text()='Login']- Fragile: positional XPath like
xpath=/html/body/div[3]/form/button[2]
Work with your dev team to add data-testid attributes to key elements:
<button data-testid="login-submit-btn" type="submit">Login</button>Then in Robot Framework:
Click Element css=[data-testid='login-submit-btn']Core Keywords Reference
Navigation:
Go To https://example.com/login
Reload Page
Go Back
Go Forward
Get Location # Returns current URLInput:
Input Text id=username admin
Input Password id=password secret123 # Masked in logs
Clear Element Text id=search
Press Keys id=search hello${SPACE}world ENTERClicks:
Click Element css=button.submit
Double Click Element id=item
Click Element At Coordinates id=canvas 100 200
Mouse Over id=dropdown-triggerDropdowns and selects:
Select From List By Value id=country US
Select From List By Label id=country United States
Select From List By Index id=country 2
Unselect From List By Value id=multiselect option1Checkboxes and radio buttons:
Select Checkbox id=agree-terms
Unselect Checkbox id=newsletter
Click Element id=radio-option-bFile upload:
Choose File id=file-input /path/to/test-file.pdfAssertions:
Page Should Contain Welcome, admin
Page Should Not Contain Error
Page Should Contain Element css=.success-banner
Element Should Be Visible id=user-menu
Element Should Not Be Visible css=.loading-spinner
Element Should Be Enabled id=submit-btn
Element Should Be Disabled id=submit-btn
# Text assertions
Element Text Should Be css=h1 Dashboard
Element Should Contain css=.notification 3 new messages
# Attribute assertions
Element Attribute Value Should Be id=checkbox checked true
# Count
Page Should Contain Element css=.list-item limit=5Waiting Strategies
This is where most web testing bugs live. Selenium is faster than the browser, so you need explicit waits.
Never use Sleep. Use these instead:
# Wait for element to appear
Wait Until Element Is Visible css=.success-message timeout=15s
# Wait for element to disappear
Wait Until Element Is Not Visible css=.loading-spinner timeout=30s
# Wait for element to exist in DOM (not necessarily visible)
Wait Until Page Contains Element id=data-table timeout=10s
# Wait for text to appear
Wait Until Page Contains Data loaded successfully timeout=10s
# Wait for element to be clickable
Wait Until Element Is Enabled id=submit-btn timeout=5s
# Custom condition (Python lambda via Evaluate)
Wait Until Keyword Succeeds 30s 1s Element Count Should Be css=tr.data-row 10Wait Until Keyword Succeeds is your escape hatch for anything not covered by built-in waits:
*** Keywords ***
Wait For API Response To Update Table
Wait Until Keyword Succeeds 20s 500ms
... Table Row Count Should Be Greater Than css=tbody tr 0
Table Row Count Should Be Greater Than
[Arguments] ${locator} ${min_count}
${rows}= Get Element Count ${locator}
Should Be True ${rows} > ${min_count}
... msg=Table has ${rows} rows, expected more than ${min_count}Setting timeouts: Global timeout is set in the Library import. Override per-keyword with the timeout argument, or change globally mid-test:
Set Selenium Timeout 30s # Changes global timeout for subsequent calls
Wait Until Element Is Visible css=.slow-widget timeout=45s # One-off overrideHandling Dynamic Content
Stale element references: Elements that are re-rendered between the find and the interact will throw StaleElementReferenceException. SeleniumLibrary handles most of these transparently by re-finding elements, but in loops you may need to handle it:
*** Keywords ***
Click Each Item In List
[Arguments] ${list_locator}
${count}= Get Element Count ${list_locator}
FOR ${i} IN RANGE 1 ${count}+1
# Re-find each time — list may re-render after each click
Click Element css=${list_locator}:nth-child(${i})
Wait Until Element Is Not Visible css=.processing-indicator
ENDInfinite scroll:
*** Keywords ***
Scroll Until Element Visible
[Arguments] ${locator} ${max_scrolls}=10
FOR ${i} IN RANGE ${max_scrolls}
${visible}= Run Keyword And Return Status Element Should Be Visible ${locator}
Return From Keyword If ${visible}
Execute Javascript window.scrollBy(0, 500)
Sleep 0.3s
END
Fail Element ${locator} not visible after ${max_scrolls} scrollsiframes:
Select Frame id=embedded-content
Input Text id=inner-field value
Unselect Frame
Click Element id=outer-elementAlerts and dialogs:
Click Element id=delete-btn
Handle Alert action=ACCEPT
# or
${alert_text}= Get Alert Message
Should Contain ${alert_text} Are you sure
Handle Alert ACCEPTJavaScript execution for edge cases:
# Click element hidden under sticky header
Execute Javascript document.getElementById('hidden-btn').click()
# Scroll element into view
Execute Javascript arguments[0].scrollIntoView(true) ARGUMENTS id=target-element
# Get computed style
${color}= Execute Javascript return window.getComputedStyle(document.querySelector('.status')).colorPage Object Pattern in Robot Framework
RF's Page Object equivalent is resource files with keywords organized per page. Unlike Python PageObject classes, RF uses keyword composition:
# resources/pages/login_page.resource
*** Settings ***
Library SeleniumLibrary
*** Variables ***
${LOGIN_URL} /login
${USERNAME_INPUT} id=username
${PASSWORD_INPUT} id=password
${SUBMIT_BTN} css=button[type='submit']
${ERROR_MSG} css=.error-message
${SUCCESS_REDIRECT} /dashboard
*** Keywords ***
Navigate To Login Page
Go To ${BASE_URL}${LOGIN_URL}
Wait Until Element Is Visible ${USERNAME_INPUT}
Login With Credentials
[Arguments] ${username} ${password}
Navigate To Login Page
Input Text ${USERNAME_INPUT} ${username}
Input Password ${PASSWORD_INPUT} ${password}
Click Element ${SUBMIT_BTN}
Login Should Succeed
Wait Until Location Contains ${SUCCESS_REDIRECT} timeout=10s
Element Should Be Visible css=.user-menu
Login Should Fail With Message
[Arguments] ${expected_message}
Wait Until Element Is Visible ${ERROR_MSG} timeout=5s
Element Text Should Be ${ERROR_MSG} ${expected_message}# resources/pages/dashboard_page.resource
*** Settings ***
Library SeleniumLibrary
*** Variables ***
${WELCOME_HEADER} css=h1.welcome
${PROJECT_LIST} css=.project-card
${NEW_PROJECT_BTN} css=[data-testid='new-project-btn']
*** Keywords ***
Dashboard Should Be Loaded
Wait Until Element Is Visible ${WELCOME_HEADER} timeout=10s
Element Should Be Visible ${NEW_PROJECT_BTN}
Get Visible Project Count
${count}= Get Element Count ${PROJECT_LIST}
RETURN ${count}Using page resources in tests:
# tests/login_tests.robot
*** Settings ***
Resource ../resources/pages/login_page.resource
Resource ../resources/pages/dashboard_page.resource
*** Test Cases ***
Successful Login Lands On Dashboard
Login With Credentials admin@example.com correct_password
Login Should Succeed
Dashboard Should Be Loaded
Invalid Password Shows Error
Login With Credentials admin@example.com wrong_password
Login Should Fail With Message Invalid email or password
Empty Username Shows Validation Error
Navigate To Login Page
Input Password ${PASSWORD_INPUT} password123
Click Element ${SUBMIT_BTN}
Login Should Fail With Message Username is requiredThis approach keeps locators and page-specific logic isolated. When the UI changes, you update the resource file — the test cases stay the same.
Screenshots and Debugging
Automatic screenshots on failure are configured via run_on_failure:
Library SeleniumLibrary run_on_failure=Capture Page ScreenshotManual capture:
Capture Page Screenshot filename=login-failure-{index}.pngCapture the entire page (including content below the fold):
Capture Page Screenshot filename=full-page.pngFor debugging during test development, use:
Pause Execution # Pauses until you press a key in terminal — useful for debuggingOr log the current page source:
${source}= Get Source
Log ${source} level=DEBUGRunning Tests and Generating Reports
# Run with specific browser
robot --variable BROWSER:firefox tests/
# Run headless
robot --variable HEADLESS:True tests/
# Run only smoke tests
robot --include smoke tests/
# Capture screenshots to specific directory
robot --variable SCREENSHOT_DIR:/tmp/screenshots tests/
# Full run with clean output
robot --outputdir results/ --loglevel DEBUG tests/After execution, results/log.html contains a full execution log with embedded screenshots. results/report.html has a summary. These are the most useful artifacts for debugging CI failures.
CI Configuration
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install robotframework robotframework-seleniumlibrary webdriver-manager
- name: Install Chrome
uses: browser-actions/setup-chrome@v1
- name: Run tests
run: |
robot \
--variable BROWSER:headlesschrome \
--variable BASE_URL:${{ secrets.STAGING_URL }} \
--outputdir results/ \
tests/
continue-on-error: true
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: robot-results
path: results/The headlesschrome browser string is SeleniumLibrary's built-in shorthand for Chrome in headless mode — no need to configure ChromeOptions manually.
Common Gotchas
Implicit vs explicit waits: Setting implicit_wait in the Library import can interfere with explicit waits and produce unpredictable timing. Keep implicit_wait=0s and always use explicit Wait Until keywords.
Tab/window handling: New tabs opened by clicks need explicit switching:
Click Element id=open-new-tab-btn
${handles}= Get Window Handles
Switch Window ${handles}[1]
Wait Until Element Is Visible css=.new-tab-contentDropdown vs custom select: Many modern UIs use custom dropdown components (not <select>) that don't work with Select From List. Use Click Element + Wait Until Element Is Visible + Click Element for these:
Open Custom Dropdown And Select Option
[Arguments] ${dropdown_trigger} ${option_text}
Click Element ${dropdown_trigger}
Wait Until Element Is Visible css=.dropdown-menu
Click Element xpath=//li[text()='${option_text}']
Wait Until Element Is Not Visible css=.dropdown-menuBrowser version mismatch: ChromeDriver must match Chrome's major version. In CI, pin Chrome version or use webdriver-manager to auto-manage it.
SeleniumLibrary is mature and stable. Its keyword set covers most web testing needs, and when it doesn't, dropping into Execute Javascript or writing a Python keyword handles the rest. The patterns here will carry you through most production web testing scenarios.