Reference guide for writing SeleniumBase E2E tests in the HOPE project.
Key files:
tests/e2e/helpers/selenium_base.py—HopeTestBrowser(extendsseleniumbase.BaseCase)tests/e2e/new_selenium/conftest.py— sharedbrowser,login, fixturestests/e2e/conftest.py— autouse DB fixtures (create_super_user,create_unicef_partner, etc.)
These rules are non-negotiable. Violations will cause test failures or flaky runs.
- No
autouse=Trueon test data fixtures. Only infrastructure fixtures (e.g.test_failed_check,clear_default_cache) may be autouse. Test data fixtures must be explicitly requested by the test function. - No
browser.sleep()— usewait_for_element_clickable,wait_for_element_visible, orwait_for_element_absentinstead. Only as a last resort for CSS animations. - Tests MUST be plain functions (
def test_*()), never classes. - One test = one scenario. Use
pytest.mark.parametrizeinstead of loops. - No
if / for / whileinside test bodies or test helper functions. Loops in helpers are acceptable only for repetitive DOM actions (e.g. filling N round-name inputs). - Test data created exclusively in fixtures using factories from
extras.test_utils.factories(notold_factories). - Use
dbfixture, NOTtransaction=True/transactional_db. - Prefer CSS selectors with
data-cyattributes over XPath. - Use
loginfixture directly — do not alias (browser = login). - Mock only external dependencies (network, S3, Celery). Never mock code under test.
HopeTestBrowser (extends seleniumbase.BaseCase)
└── browser fixture (tests/e2e/new_selenium/conftest.py)
└── login fixture
└── test functions using login.click(), login.type(), login.assert_text()
HopeTestBrowser provides HOPE-specific helpers on top of SeleniumBase:
login(username, password)— logs in via Django admin, clears browser storageselect_listbox_element(name)— selects from MUIul[role="listbox"]dropdownsselect_option_by_name(name)— selects fromdata-cy="select-option-*"dropdownsscroll_main_content(scroll_by)— scrolls the MUI main content area
Existing raw Selenium tests (Common → BaseComponents → PageObject) are unaffected.
The browser and login fixtures are defined in tests/e2e/new_selenium/conftest.py.
Do NOT redefine them. Create local conftest.py only for domain-specific data fixtures.
These fixtures already exist and run automatically. Do not add new autouse fixtures — this is prohibited by the new-style test rules (see rule 1 above).
| Fixture | Creates |
|---|---|
create_super_user |
User (superuser/testtest2), partners, roles, DCTs |
create_unicef_partner |
UNICEF + UNICEF HQ partners |
create_role_with_all_permissions |
Role with all permissions |
clear_default_cache |
Clears Django cache |
| Fixture | Creates |
|---|---|
business_area |
Afghanistan BA with flags, settings |
live_server_with_static |
Django live server with static files |
Full docs: seleniumbase.io/help_docs/method_summary
# BAD
browser.click(INPUT_NAME) # dismiss picker
browser.sleep(0.3) # flaky
browser.click(INPUT_END_DATE)
# GOOD
browser.click(INPUT_NAME) # dismiss picker
browser.wait_for_element_clickable(INPUT_END_DATE)
browser.click(INPUT_END_DATE)# BAD — two DOM lookups
browser.click(INPUT_START_DATE)
browser.send_keys(INPUT_START_DATE, "2024-01-01")
# GOOD — one lookup
el = browser.find_element(INPUT_START_DATE)
el.click()
el.send_keys("2024-01-01")All interactive elements use data-cy attributes. Common patterns:
# Buttons
'a[data-cy="button-new-program"]'
'button[data-cy="button-next"]'
'button[data-cy="button-save"]'
# Inputs
'input[data-cy="input-name"]'
'textarea[data-cy="input-description"]'
'input[name="startDate"]' # date inputs use name attr
# Dropdowns (click to open, then use helper)
'div[data-cy="select-sector"]' # → browser.select_option_by_name("Child Protection")
'div[data-cy="input-beneficiary-group"]' # → browser.select_listbox_element("Main Menu")
# Labels / display values
'div[data-cy="label-Sector"]'
'h5[data-cy="page-header-title"]'
'div[data-cy="status-container"]'
# Navigation
'a[data-cy="nav-Programmes"]'
'a[data-cy="nav-Programme Details"]'Discover selectors in the frontend source or browser DevTools — search for data-cy.
See tests/e2e/new_selenium/program_details/test_create_program.py as the first example of a new-style Selenium test.
import pytest
from extras.test_utils.selenium import HopeTestBrowser
pytestmark = pytest.mark.django_db()
# Selectors as module-level constants
HEADER = 'h5[data-cy="page-header-title"]'
BTN_ACTION = 'button[data-cy="button-action"]'
def test_feature_scenario(login: HopeTestBrowser) -> None:
# Navigate
login.click('a[data-cy="nav-Programmes"]')
login.wait_for_text("Expected Title", HEADER)
# Interact
login.click(BTN_ACTION)
login.type('input[data-cy="input-field"]', "value")
# Assert
login.assert_text("Expected Text", 'div[data-cy="label-Field"]')# Run SeleniumBase tests
tox -e tests -- tests/e2e/new_selenium/
# Single test file
tox -e tests -- tests/e2e/new_selenium/program_details/test_create_program.py
# Single test
tox -e tests -- tests/e2e/new_selenium/program_details/test_create_program.py::test_name
# Visible browser
tox -e tests -- tests/e2e/new_selenium/ --headed
# Slow mode (demo)
tox -e tests -- tests/e2e/new_selenium/ --headed --demo
# Lint after changes
tox -e linttox runs: pytest -q --create-db --no-migrations --dist=loadgroup {posargs:tests}