Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a701342
[AB#307495]-setup selenium base + add create program test
mmaciekk Mar 27, 2026
1b32f33
fix tox.ini / pipeline setup for headless selenium required by seleni…
mmaciekk Mar 27, 2026
463d279
admin panel url
mmaciekk Mar 27, 2026
cc1fd4e
add new rules to selenium_base, refactor test_create_program to adjus…
mmaciekk Mar 30, 2026
b675c81
replace sleep wit wait for element
mmaciekk Mar 30, 2026
93c9b9e
add rule about wait for element instead of sleep, and remove flag fro…
mmaciekk Mar 30, 2026
d6d3f4e
format, lint
mmaciekk Mar 30, 2026
8eb978f
PR comments addressed
mmaciekk Mar 31, 2026
076de75
use select_option_by_name instead of select_listbox_element
mmaciekk Mar 31, 2026
c4a8074
add new pattern
mmaciekk Mar 31, 2026
9661d3a
make selenium patterns more concise
mmaciekk Apr 1, 2026
870a41b
remove if inside test function
mmaciekk Apr 1, 2026
dc12f1f
remove comments scroll and if
mmaciekk Apr 1, 2026
2ca9194
final pr comments
mmaciekk Apr 1, 2026
a922ed1
Merge branch 'develop' into selenium-create-program
mmaciekk Apr 1, 2026
423f41f
test pdu fields and other fields from program form
mmaciekk Apr 1, 2026
3c136b9
fix add assert value
mmaciekk Apr 1, 2026
0a32d07
fix edit pdu test + button next instead of clicking on step button
mmaciekk Apr 1, 2026
b9128dc
Merge remote-tracking branch 'origin' into selenium-create-program
mmaciekk Apr 10, 2026
0e6587b
address Kamil's comments
mmaciekk Apr 10, 2026
05b2593
uv.lock update
mmaciekk Apr 10, 2026
9fa8449
rewrite flaky grievance test selenium base way
mmaciekk Apr 10, 2026
fab5d14
remove pytest.fixture(autouse=True)
mmaciekk Apr 15, 2026
54a9968
Merge branch 'develop' into selenium-create-program
mmaciekk Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ jobs:
./tox
key: ${{ runner.os }}-venv


- name: Run Test suite
run: uv run tox -e tests -- pytest ./tests/unit -q -rfE --no-header --tb=short --no-migrations --randomly-seed=42 --dist=loadgroup --create-db -n 4 --cov-report xml:test-coverage/coverage.xml --junit-xml=test-results.xml --cov-config=.coveragerc --cov=hope --dist loadgroup

Expand Down Expand Up @@ -232,7 +231,7 @@ jobs:
key: ${{ runner.os }}-venv

- name: Run Selenium suite
run: uv run tox -e tests -- pytest ./tests/e2e -q -rfE --no-header --tb=short --no-migrations --randomly-seed=42 --dist=loadgroup --create-db -n auto --cov-report xml:test-coverage/coverage.xml --junit-xml=test-results.xml --cov-config=.coveragerc --cov=hope
run: uv run tox -e tests -- pytest ./tests/e2e -q -rfE --no-header --tb=short --no-migrations --randomly-seed=42 --dist=loadgroup --create-db -n auto --headless --cov-report xml:test-coverage/coverage.xml --junit-xml=test-results.xml --cov-config=.coveragerc --cov=hope
- name: Test Report
uses: dorny/test-reporter@v2
if: success() || failure()
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ dev = [
"pytest-cov>=4.1",
"pytest-django>=4.5.2",
"pytest-echo>=1.7.1",
"pytest-html>=4.1.1",
"pytest-html>=4.0.2",
"pytest-mock",
"pytest-randomly>=3.15",
"pytest-repeat>=0.9.3",
Expand All @@ -157,6 +157,7 @@ dev = [
"responses>=0.22",
"ruff>=0.11.8",
"selenium>=4.29",
"seleniumbase>=4.34",
"snapshottest>=1.0.0a0",
"tox>=4.25",
"types-freezegun>=1.1.10",
Expand Down Expand Up @@ -206,6 +207,9 @@ package-dir = { "" = "src" }
packages.find.where = [ "src" ]

[tool.uv]
override-dependencies = [
"pytest-html==4.0.2",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not a fan of these overrides

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domdinicola Me neither, but here is the reason why.
seleniumbase/SeleniumBase#3619

]
package = true

[tool.django-stubs]
Expand Down
201 changes: 201 additions & 0 deletions tests/e2e/SELENIUM_E2E_PATTERNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Selenium E2E Test Patterns — HOPE Project

Reference guide for writing SeleniumBase E2E tests in the HOPE project.

**Key files:**

- `tests/e2e/helpers/selenium_base.py` — `HopeTestBrowser` (extends `seleniumbase.BaseCase`)
- `tests/e2e/new_selenium/conftest.py` — shared `browser`, `login`,
fixtures
- `tests/e2e/conftest.py` — autouse DB fixtures (`create_super_user`, `create_unicef_partner`, etc.)

---

## Test Style Rules (MANDATORY)

**These rules are non-negotiable. Violations will cause test failures or flaky runs.**

1. **No `autouse=True`** on 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.
2. **No `browser.sleep()`** — use `wait_for_element_clickable`, `wait_for_element_visible`,
or `wait_for_element_absent` instead. Only as a last resort for CSS animations.
3. Tests MUST be plain functions (`def test_*()`), never classes.
4. One test = one scenario. Use `pytest.mark.parametrize` instead of loops.
5. No `if / for / while` inside test bodies or test helper functions.
Loops in helpers are acceptable only for repetitive DOM actions (e.g. filling N round-name inputs).
6. Test data created exclusively in fixtures using factories from
`extras.test_utils.factories` (not `old_factories`).
7. Use `db` fixture, NOT `transaction=True` / `transactional_db`.
8. Prefer CSS selectors with `data-cy` attributes over XPath.
9. Use `login` fixture directly — do not alias (`browser = login`).
10. Mock only external dependencies (network, S3, Celery). Never mock code under test.

---

## Architecture

```
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 storage
- `select_listbox_element(name)` — selects from MUI `ul[role="listbox"]` dropdowns
- `select_option_by_name(name)` — selects from `data-cy="select-option-*"` dropdowns
- `scroll_main_content(scroll_by)` — scrolls the MUI main content area

Existing raw Selenium tests (`Common → BaseComponents → PageObject`) are unaffected.

---

## Fixture Setup

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.

### Autouse fixtures (from parent conftest.py)

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 |

### On-demand fixtures

| Fixture | Creates |
| ------------------------- | ------------------------------------ |
| `business_area` | Afghanistan BA with flags, settings |
| `live_server_with_static` | Django live server with static files |

---

## SeleniumBase API Quick Reference

Full docs: [seleniumbase.io/help_docs/method_summary](https://seleniumbase.io/help_docs/method_summary/)

### Wait instead of sleep

```python
# 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)
```

### Fetch element once for click + type

```python
# 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")
```

---

## Selector Conventions

All interactive elements use `data-cy` attributes. Common patterns:

```python
# 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`.

---

## Writing a New Test

### 1. File structure

See `tests/e2e/new_selenium/program_details/test_create_program.py` as the first example of a new-style Selenium test.

### 2. Template

```python
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"]')
```

## Running Tests

```bash
# 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 lint
```

tox runs: `pytest -q --create-db --no-migrations --dist=loadgroup {posargs:tests}`
13 changes: 2 additions & 11 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def create_session(host: str, username: str, password: str, csrf: str = "") -> o
"Content-Type": "application/x-www-form-urlencoded",
}
data = {"username": username, "password": password}
pytest.session.post(f"{host}/api/unicorn/login/", data=data, headers=headers)
pytest.session.post(f"{host}/api/{settings.ADMIN_PANEL_URL}/login/", data=data, headers=headers)
pytest.SESSION_ID = pytest.session.cookies.get_dict()["sessionid"]
return pytest.session

Expand Down Expand Up @@ -284,7 +284,7 @@ def browser(driver: Chrome, live_server_with_static) -> Chrome:

@pytest.fixture
def login(browser: Chrome) -> Chrome:
browser.get(f"{browser.live_server.url}/api/unicorn/")
browser.get(f"{browser.live_server.url}/api/{settings.ADMIN_PANEL_URL}/")

browser.execute_script(
"""
Expand Down Expand Up @@ -724,15 +724,6 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
report.extra = extra


@pytest.fixture(autouse=True)
def test_failed_check(request: FixtureRequest, browser: Chrome) -> None:
yield
if request.node.rep_setup.failed:
pass
elif request.node.rep_setup.passed and request.node.rep_call.failed:
screenshot(browser, request.node.nodeid)


def attach(data=None, path=None, name="attachment", mime_type=None, request=None):
"""Drop-in replacement for pytest_html_reporter's attach()."""
if request is None: # fallback: can't attach without test context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,38 +556,6 @@ def test_check_grievance_tickets_details_page_social_worker_program(
assert "IND-00-0000.0011" in page_grievance_details_page.get_ticket_target_id().text


@pytest.mark.usefixtures("login")
class TestGrievanceTicketsHappyPath:
def test_grievance_tickets_create_new_ticket_referral(
self,
page_grievance_tickets: GrievanceTickets,
page_grievance_new_ticket: NewTicket,
page_grievance_details_page: GrievanceDetailsPage,
social_worker_program: Program,
) -> None:
page_grievance_tickets.get_nav_grievance().click()
assert "Grievance Tickets" in page_grievance_tickets.get_grievance_title().text
page_grievance_tickets.get_button_new_ticket().click()
page_grievance_new_ticket.get_select_category().click()
page_grievance_new_ticket.select_option_by_name("Referral")
page_grievance_new_ticket.get_button_next().click()
page_grievance_new_ticket.wait_for_page_ready()
page_grievance_new_ticket.get_household_tab()
page_grievance_new_ticket.wait_for_page_ready()
assert page_grievance_new_ticket.wait_for_no_results()
page_grievance_new_ticket.get_button_next().click()
page_grievance_new_ticket.wait_for_page_ready()
page_grievance_new_ticket.get_received_consent().click()
page_grievance_new_ticket.get_button_next().click()
page_grievance_new_ticket.get_description().send_keys("Happy path test 1234!")
page_grievance_new_ticket.get_button_next().click()
assert "Happy path test 1234!" in page_grievance_details_page.get_ticket_description().text
assert "Referral" in page_grievance_details_page.get_ticket_category().text
assert "New" in page_grievance_details_page.get_ticket_status().text
assert "Not set" in page_grievance_details_page.get_ticket_priority().text
assert "Not set" in page_grievance_details_page.get_ticket_urgency().text


@pytest.mark.night
@pytest.mark.usefixtures("login")
class TestGrievanceTickets:
Expand Down
Empty file.
27 changes: 27 additions & 0 deletions tests/e2e/new_selenium/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Generator

import pytest
from seleniumbase import config as sb_config

from extras.test_utils.selenium import HopeTestBrowser


@pytest.fixture
def browser(live_server_with_static, request) -> Generator[HopeTestBrowser, None, None]:
sb = HopeTestBrowser("base_method")
sb.live_server_url = str(live_server_with_static)
sb.setUp()
sb._needs_tearDown = True
sb._using_sb_fixture = True
sb._using_sb_fixture_no_class = True
sb_config._sb_node[request.node.nodeid] = sb
yield sb
if sb._needs_tearDown:
sb.tearDown()
sb._needs_tearDown = False


@pytest.fixture
def login(browser: HopeTestBrowser) -> HopeTestBrowser:
browser.login()
return browser
Empty file.
23 changes: 23 additions & 0 deletions tests/e2e/new_selenium/grievance/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from datetime import datetime

from dateutil.relativedelta import relativedelta
import pytest

from extras.test_utils.factories.core import DataCollectingTypeFactory
from extras.test_utils.factories.program import ProgramFactory
from hope.models import BeneficiaryGroup, BusinessArea, DataCollectingType, Program


@pytest.fixture
def social_worker_program() -> Program:
dct = DataCollectingTypeFactory(type=DataCollectingType.Type.SOCIAL)
beneficiary_group = BeneficiaryGroup.objects.get(name="People")
return ProgramFactory(
name="Social Program",
status=Program.ACTIVE,
business_area=BusinessArea.objects.get(slug="afghanistan"),
data_collecting_type=dct,
beneficiary_group=beneficiary_group,
start_date=datetime.now() - relativedelta(months=1),
end_date=datetime.now() + relativedelta(months=1),
)
Loading
Loading