Skip to content

Commit fe49c29

Browse files
authored
Merge pull request #100 from unicef/feature/setup-seleniumbase
Setup seleniumbase
2 parents a738a38 + ef25459 commit fe49c29

File tree

8 files changed

+1132
-1196
lines changed

8 files changed

+1132
-1196
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ jobs:
147147
-v "./tests:/app/tests" \
148148
-v "./pytest.ini:/app/pytest.ini" \
149149
-t ${{env.IMAGE}} \
150-
pytest tests/ --create-db --selenium -v --maxfail=5 --migrations --cov-report xml:./output/coverage.xml --record-mode none
150+
pytest tests/ --create-db -v --maxfail=5 --migrations --cov-report xml:./output/coverage.xml --record-mode none
151151
152152
- name: Upload coverage to Codecov
153153
uses: codecov/codecov-action@v4

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ dev-dependencies = [
8989
"pytest-factoryboy>=2.7.0",
9090
"pytest-mock>=3.14.0",
9191
"pytest-recording>=0.13.2",
92-
"pytest-selenium>=4.1.0",
9392
"pytest-xdist>=3.6.1",
9493
"pytest>=8.2.2",
9594
"responses>=0.25.3",
9695
"types-python-dateutil>=2.9.0.20241003",
9796
"types-requests>=2.31.0.6",
9897
"vcrpy>=6.0.2",
9998
"ruff>=0.9.3",
99+
"seleniumbase>=4.35.7",
100100
]
101101

102102

tests/conftest.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,6 @@
1717
sys.path.insert(0, str(here / "extras"))
1818

1919

20-
def pytest_addoption(parser):
21-
parser.addoption(
22-
"--selenium",
23-
action="store_true",
24-
dest="enable_selenium",
25-
default=False,
26-
help="enable selenium tests",
27-
)
28-
29-
parser.addoption(
30-
"--show-browser",
31-
"-S",
32-
action="store_true",
33-
dest="show_browser",
34-
default=False,
35-
help="will not start browsers in headless mode",
36-
)
37-
38-
3920
class MockStorage(Storage):
4021
def __init__(self):
4122
self._file_content = None
@@ -69,11 +50,6 @@ def patch_asyncjob(mock_storage):
6950

7051

7152
def pytest_configure(config):
72-
if not config.option.enable_selenium and ("selenium" not in getattr(config.option, "markexpr", None)):
73-
if config.option.markexpr:
74-
config.option.markexpr += " and not selenium"
75-
else:
76-
config.option.markexpr = "not selenium"
7753
os.environ["DJANGO_SETTINGS_MODULE"] = "country_workspace.config.settings"
7854
os.environ.setdefault("STATIC_URL", "/static/")
7955
os.environ.setdefault("MEDIA_ROOT", "/tmp/static/")

tests/extras/testutils/selenium.py

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,69 @@
1-
from django.urls import reverse
1+
from seleniumbase import BaseCase
22

33

4-
def check_link_by_class(selenium, cls, view_name):
5-
link = selenium.find_element_by_class_name(cls)
6-
url = reverse(f"{view_name}")
7-
return f' href="{url}"' in link.get_attribute("innerHTML")
4+
class CountryWorkspaceSeleniumTC(BaseCase):
5+
live_server_url: str = ""
86

7+
def setUp(self, masterqa_mode=False):
8+
super().setUp()
9+
from testutils.factories.user import SuperUserFactory
910

10-
def wait_for(driver, *args):
11-
from selenium.webdriver.support import expected_conditions as ec
12-
from selenium.webdriver.support.ui import WebDriverWait
11+
super().setUpClass()
12+
self.admin_user = SuperUserFactory()
13+
self.admin_user._password = "password"
1314

14-
wait = WebDriverWait(driver, 10)
15-
wait.until(ec.visibility_of_element_located((*args,)))
16-
return driver.find_element(*args)
15+
def tearDown(self):
16+
self.save_teardown_screenshot()
17+
super().tearDown()
18+
self.admin_user.delete()
1719

20+
def base_method(self):
21+
pass
1822

19-
def wait_for_url(driver, url):
20-
from selenium.webdriver.support import expected_conditions as ec
21-
from selenium.webdriver.support.ui import WebDriverWait
23+
def open(self, url: str):
24+
self.maximize_window()
25+
return super().open(f"{self.live_server_url}{url}")
2226

23-
if "://" not in url:
24-
url = f"{driver.live_server.url}{url}"
25-
wait = WebDriverWait(driver, 10)
26-
wait.until(ec.url_contains(url))
27+
def select2_select(self, element_id: str, value: str):
28+
self.slow_click(f"span[aria-labelledby=select2-{element_id}-container]")
29+
self.wait_for_element_visible("input.select2-search__field")
30+
self.click(f"li.select2-results__option:contains('{value}')")
31+
self.wait_for_element_absent("input.select2-search__field")
2732

33+
def login_as_user(self, user=None):
34+
if user is not None:
35+
self.admin_user = user
36+
self.open("/login/")
37+
self.type("input[name=username]", f"{self.admin_user.username}")
38+
self.type("input[name=password]", f"{self.admin_user._password}")
39+
self.submit("#login-form")
40+
self.wait_for_ready_state_complete()
2841

29-
def force_login(user, driver, base_url):
30-
from importlib import import_module
42+
def login(self, url=None):
43+
self.open("/admin/")
44+
if self.get_current_url() == f"{self.live_server_url}/admin/login/?next=/admin/":
45+
self.type("input[name=username]", f"{self.admin_user.username}")
46+
self.type("input[name=password]", f"{self.admin_user._password}")
47+
self.submit('input[value="Log in"]')
48+
self.wait_for_ready_state_complete()
3149

32-
from django.conf import settings
33-
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
50+
def is_required(self, element: str) -> bool:
51+
el = self.wait_for_element_visible(element)
52+
return el.parent.find_element("label>span").text == "(required)"
3453

35-
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore # noqa: N806
36-
with driver.with_timeouts(page=5):
37-
driver.get(base_url)
54+
def get_field_error(self, element: str) -> bool:
55+
return self.wait_for_element_visible(f"fieldset.{element} ul.errorlist").text
3856

39-
session = SessionStore()
40-
session[SESSION_KEY] = user._meta.pk.value_to_string(user)
41-
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
42-
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
43-
session.save()
44-
45-
driver.add_cookie(
46-
{
47-
"name": settings.SESSION_COOKIE_NAME,
48-
"value": session.session_key,
49-
"path": "/",
50-
}
51-
)
52-
driver.refresh()
57+
def get_pixel_colors(self):
58+
# Return the RGB colors of the canvas element's top left pixel
59+
x = 0
60+
y = 0
61+
if self.browser == "safari":
62+
x = 1
63+
y = 1
64+
color = self.execute_script(
65+
"return document.querySelector('canvas').getContext('2d').getImageData(%s,%s,1,1).data;" % (x, y)
66+
)
67+
if self.is_chromium():
68+
return [color[0], color[1], color[2]]
69+
return [color["0"], color["1"], color["2"]]

tests/functional/conftest.py

Lines changed: 39 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,46 @@
1-
import contextlib
2-
import time
3-
from collections import namedtuple
4-
from typing import TYPE_CHECKING
1+
from typing import Generator
52

3+
from django.conf import settings
4+
from seleniumbase import config as sb_config
5+
from seleniumbase.core import session_helper
66
import pytest
7-
from selenium.webdriver import Keys
8-
from selenium.webdriver.common.by import By
97

10-
if TYPE_CHECKING:
11-
from selenium.webdriver.common.timeouts import Timeouts
12-
13-
Proxy = namedtuple("Proxy", "host,port")
14-
15-
16-
def pytest_configure(config):
17-
if not config.option.driver:
18-
config.option.driver = "chrome"
19-
20-
21-
@contextlib.contextmanager
22-
def timeouts(driver, wait=None, page=None, script=None):
23-
_current: Timeouts = driver.timeouts
24-
if wait:
25-
driver.implicitly_wait(wait)
26-
if page:
27-
driver.set_page_load_timeout(page)
28-
if script:
29-
driver.set_script_timeout(script)
30-
yield
31-
driver.timeouts = _current
32-
33-
34-
def set_input_value(driver, *args):
35-
rules = args[:-1]
36-
el = driver.find_element(*rules)
37-
el.clear()
38-
el.send_keys(args[-1])
39-
40-
41-
def find_by_css(selenium, *args):
42-
from testutils.selenium import wait_for
43-
44-
return wait_for(selenium, By.CSS_SELECTOR, *args)
45-
46-
47-
def select2(driver, by, selector, value):
48-
el = driver.find_element(by, selector)
49-
el.click()
50-
time.sleep(1)
51-
driver.switch_to.active_element.send_keys(value)
52-
driver.find_element(By.XPATH, f"//div[contains(text(),'{value}')]").click()
53-
time.sleep(1)
54-
driver.switch_to.active_element.send_keys(Keys.TAB)
55-
time.sleep(1)
56-
driver.switch_to.active_element.send_keys(Keys.TAB)
8+
from testutils.selenium import CountryWorkspaceSeleniumTC
579

5810

5911
@pytest.fixture
60-
def chrome_options(request, chrome_options):
61-
if not request.config.getvalue("show_browser"):
62-
chrome_options.add_argument("--headless")
63-
chrome_options.add_argument("--allow-insecure-localhost")
64-
chrome_options.add_argument("--disable-browser-side-navigation")
65-
chrome_options.add_argument("--disable-dev-shm-usage")
66-
chrome_options.add_argument("--disable-gpu")
67-
chrome_options.add_argument("--disable-translate")
68-
chrome_options.add_argument("--ignore-certificate-errors")
69-
chrome_options.add_argument("--lang=en-GB")
70-
chrome_options.add_argument("--no-sandbox")
71-
chrome_options.add_argument("--proxy-bypass-list=*")
72-
chrome_options.add_argument("--proxy-server='direct://'")
73-
chrome_options.add_argument("--start-maximized")
74-
75-
prefs = {"profile.default_content_setting_values.notifications": 1} # explicitly allow notifications
76-
chrome_options.add_experimental_option("prefs", prefs)
77-
78-
return chrome_options
79-
80-
81-
SELENIUM_DEFAULT_PAGE_LOAD_TIMEOUT = 5
82-
SELENIUM_DEFAULT_IMPLICITLY_WAIT = 1
83-
SELENIUM_DEFAULT_SCRIPT_TIMEOUT = 1
84-
85-
86-
@pytest.fixture
87-
def selenium(monkeypatch, live_server, settings, driver):
88-
from testutils.selenium import wait_for, wait_for_url
89-
12+
def browser(live_server, request) -> Generator[CountryWorkspaceSeleniumTC, None, None]:
13+
"""SeleniumBase as a pytest fixture.
14+
Usage example: "def test_one(sb):"
15+
You may need to use this for tests that use other pytest fixtures."""
9016
settings.FLAGS = {"LOCAL_LOGIN": [("boolean", True)]}
91-
92-
driver.with_timeouts = timeouts.__get__(driver)
93-
driver.set_input_value = set_input_value.__get__(driver)
94-
driver.live_server = live_server
95-
driver.wait_for = wait_for.__get__(driver)
96-
driver.wait_for_url = wait_for_url.__get__(driver)
97-
driver.find_by_css = find_by_css.__get__(driver)
98-
driver.select2 = select2.__get__(driver)
99-
100-
return driver
17+
if request.cls:
18+
if sb_config.reuse_class_session:
19+
the_class = str(request.cls).split(".")[-1].split("'")[0]
20+
if the_class != sb_config._sb_class:
21+
session_helper.end_reused_class_session_as_needed()
22+
sb_config._sb_class = the_class
23+
request.cls.sb = CountryWorkspaceSeleniumTC("base_method")
24+
request.cls.sb.live_server_url = str(live_server)
25+
request.cls.sb.setUp()
26+
request.cls.sb._needs_tearDown = True
27+
request.cls.sb._using_sb_fixture = True
28+
request.cls.sb._using_sb_fixture_class = True
29+
sb_config._sb_node[request.node.nodeid] = request.cls.sb
30+
yield request.cls.sb
31+
if request.cls.sb._needs_tearDown:
32+
request.cls.sb.tearDown()
33+
request.cls.sb._needs_tearDown = False
34+
else:
35+
sb = CountryWorkspaceSeleniumTC("base_method")
36+
sb.live_server_url = str(live_server)
37+
sb.setUp()
38+
sb._needs_tearDown = True
39+
sb._using_sb_fixture = True
40+
sb._using_sb_fixture_no_class = True
41+
sb_config._sb_node[request.node.nodeid] = sb
42+
sb.maximize_window()
43+
yield sb
44+
if sb._needs_tearDown:
45+
sb.tearDown()
46+
sb._needs_tearDown = False

tests/functional/test_f_household.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import TYPE_CHECKING
22

33
import pytest
4-
from selenium.webdriver.common.by import By
5-
from selenium.webdriver.support.select import Select
64

75
if TYPE_CHECKING:
86
from country_workspace.workspaces.models import CountryHousehold
@@ -39,7 +37,7 @@ def household(program):
3937

4038
@pytest.mark.selenium
4139
@pytest.mark.xfail
42-
def test_list_household(selenium, admin_user, household: "CountryHousehold"):
40+
def test_list_household(browser, admin_user, household: "CountryHousehold"):
4341
from testutils.perms import user_grant_permissions
4442

4543
with user_grant_permissions(
@@ -51,22 +49,14 @@ def test_list_household(selenium, admin_user, household: "CountryHousehold"):
5149
],
5250
household.program.country_office,
5351
):
54-
selenium.get(f"{selenium.live_server.url}")
55-
# Login
56-
selenium.find_by_css("input[name=username").send_keys(admin_user.username)
57-
selenium.find_by_css("input[name=password").send_keys(admin_user._password)
58-
selenium.find_by_css("button.primary").click()
52+
browser.login()
5953
# Select Tenant
60-
Select(selenium.wait_for(By.CSS_SELECTOR, "select[name=tenant]")).select_by_visible_text(
61-
household.program.country_office.name
62-
)
63-
selenium.select2(By.ID, "select2-id_program-container", household.program.name)
64-
selenium.wait_for(By.LINK_TEXT, "Households").click()
65-
66-
selenium.wait_for(By.LINK_TEXT, str(household.name)).click()
67-
selenium.wait_for_url(household.get_change_url())
68-
selenium.wait_for(
69-
By.CSS_SELECTOR,
70-
"a.closelink",
71-
).click()
72-
selenium.wait_for_url("/workspaces/countryhousehold/")
54+
browser.select_option_by_text("select[name=tenant]", household.program.country_office.name)
55+
browser.select2_select("id_program", household.program.name)
56+
57+
browser.click_link("Households")
58+
browser.click_link(str(household.name))
59+
browser.assert_current_url(household.get_change_url())
60+
61+
browser.click("a.closelink")
62+
browser.assert_current_url("/workspaces/countryhousehold/")

0 commit comments

Comments
 (0)