Robot Framework with SeleniumLibrary: Web Testing Guide

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-seleniumlibrary

For browser drivers, the cleanest approach is webdriver-manager:

pip install webdriver-manager

Or 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/releases

Alternatively, 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 Screenshot

The 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 Browser

For 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 Browsers

Locator 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=button

Practical locator guidelines:

Prefer stable, semantic locators. In order from most to least stable:

  1. id= — fastest, most stable when IDs are stable
  2. css= with data-testid attributes — resilient to styling changes
  3. css= with semantic structure — css=form.login button[type=submit]
  4. xpath= with text content — xpath=//button[text()='Login']
  5. 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 URL

Input:

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    ENTER

Clicks:

Click Element       css=button.submit
Double Click Element    id=item
Click Element At Coordinates    id=canvas    100    200
Mouse Over    id=dropdown-trigger

Dropdowns 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    option1

Checkboxes and radio buttons:

Select Checkbox    id=agree-terms
Unselect Checkbox    id=newsletter
Click Element    id=radio-option-b

File upload:

Choose File    id=file-input    /path/to/test-file.pdf

Assertions:

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=5

Waiting 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    10

Wait 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 override

Handling 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
    END

Infinite 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} scrolls

iframes:

Select Frame    id=embedded-content
Input Text    id=inner-field    value
Unselect Frame
Click Element    id=outer-element

Alerts 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    ACCEPT

JavaScript 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')).color

Page 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 required

This 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 Screenshot

Manual capture:

Capture Page Screenshot    filename=login-failure-{index}.png

Capture the entire page (including content below the fold):

Capture Page Screenshot    filename=full-page.png

For debugging during test development, use:

Pause Execution    # Pauses until you press a key in terminal — useful for debugging

Or log the current page source:

${source}=    Get Source
Log    ${source}    level=DEBUG

Running 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-content

Dropdown 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-menu

Browser 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.

Read more

Start now free