From 1ad847c8573b55fdb2e5a8c6d92041a02bfd30c5 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 13 Sep 2020 21:39:44 +0200 Subject: [PATCH] API & Client - replace testcafe with selenium for e2e tests --- Makefile | 9 ++- Makefile.config | 2 +- e2e/__init__.py | 0 e2e/test_activities.py | 38 +++++++++ e2e/test_index.py | 17 ++++ e2e/test_login.py | 40 ++++++++++ e2e/test_logout.py | 19 +++++ e2e/test_profile.py | 20 +++++ e2e/test_registration.py | 141 ++++++++++++++++++++++++++++++++++ e2e/utils.py | 73 ++++++++++++++++++ fittrackee_api/poetry.lock | 126 +++++++++++++++++++++++++++++- fittrackee_api/pyproject.toml | 1 + 12 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 e2e/__init__.py create mode 100644 e2e/test_activities.py create mode 100644 e2e/test_index.py create mode 100644 e2e/test_login.py create mode 100644 e2e/test_logout.py create mode 100644 e2e/test_profile.py create mode 100644 e2e/test_registration.py create mode 100644 e2e/utils.py diff --git a/Makefile b/Makefile index 06e256fe..3f9ea61d 100644 --- a/Makefile +++ b/Makefile @@ -58,10 +58,10 @@ lint-all: lint-python lint-react lint-all-fix: lint-python-fix lint-react-fix lint-python: - $(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee_api --ignore=fittrackee_api/migrations + $(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee_api e2e --ignore=fittrackee_api/migrations lint-python-fix: - $(BLACK) fittrackee_api + $(BLACK) fittrackee_api e2e lint-react: $(NPM) lint @@ -103,7 +103,10 @@ serve-dev: $(MAKE) P="serve-react serve-python-dev" make-p test-e2e: init-db - $(NPM) test + $(PYTEST) e2e --driver firefox $(PYTEST_ARGS) $(E2E_ARGS) + +test-e2e-client: init-db + E2E_ARGS=client $(PYTEST) e2e --driver firefox $(PYTEST_ARGS) test-python: $(PYTEST) fittrackee_api --cov-config .coveragerc --cov=fittrackee_api --cov-report term-missing $(PYTEST_ARGS) diff --git a/Makefile.config b/Makefile.config index 5129fcbd..ad97579d 100644 --- a/Makefile.config +++ b/Makefile.config @@ -5,7 +5,7 @@ CLIENT_PORT = 3000 export FLASK_APP = $(PWD)/fittrackee_api/server.py export APP_SETTINGS=fittrackee_api.config.DevelopmentConfig export FLASK_ENV=development -export TEST_URL = http://$(HOST):$(API_PORT) +export TEST_APP_URL = http://$(HOST):$(API_PORT) export TEST_CLIENT_URL = http://$(HOST):$(CLIENT_PORT) export DATABASE_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee export DATABASE_TEST_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee_test diff --git a/e2e/__init__.py b/e2e/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e/test_activities.py b/e2e/test_activities.py new file mode 100644 index 00000000..8214188e --- /dev/null +++ b/e2e/test_activities.py @@ -0,0 +1,38 @@ +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import Select, WebDriverWait + +from .utils import TEST_URL, register_valid_user + + +class TestActivity: + def test_user_can_add_activity_without_gpx(self, selenium): + register_valid_user(selenium) + nav_items = selenium.find_elements_by_class_name('nav-item') + + nav_items[3].click() + selenium.implicitly_wait(1) + radio_buttons = selenium.find_elements_by_class_name( + 'add-activity-radio' + ) + radio_buttons[1].click() + + selenium.find_element_by_name('title').send_keys('Activity title') + select = Select(selenium.find_element_by_name('sport_id')) + select.select_by_index(1) + selenium.find_element_by_name('activity_date').send_keys('2018-12-20') + selenium.find_element_by_name('activity_time').send_keys('14:05') + selenium.find_element_by_name('duration').send_keys('01:00:00') + selenium.find_element_by_name('distance').send_keys('10') + selenium.find_element_by_class_name('btn-primary').click() + + WebDriverWait(selenium, 10).until( + EC.url_changes(f"{TEST_URL}/activities/add") + ) + + activity_details = selenium.find_element_by_class_name( + 'activity-details' + ).text + assert 'Duration: 1:00:00' in activity_details + assert 'Distance: 10 km' in activity_details + assert 'Average speed: 10 km/h' in activity_details + assert 'Max. speed: 10 km/h' in activity_details diff --git a/e2e/test_index.py b/e2e/test_index.py new file mode 100644 index 00000000..960d1ab4 --- /dev/null +++ b/e2e/test_index.py @@ -0,0 +1,17 @@ +from .utils import TEST_URL + + +class TestIndex: + def test_title_contains_fittrackee(self, selenium): + selenium.get(TEST_URL) + assert 'FitTrackee' in selenium.title + + def test_navbar_contains_all_links(self, selenium): + selenium.get(TEST_URL) + + nav = selenium.find_element_by_tag_name('nav').text + assert "FitTrackee" in nav + assert "Dashboard" in nav + assert "Login" in nav + assert "Register" in nav + assert "en" in nav diff --git a/e2e/test_login.py b/e2e/test_login.py new file mode 100644 index 00000000..7975c168 --- /dev/null +++ b/e2e/test_login.py @@ -0,0 +1,40 @@ +from .utils import TEST_URL, assert_navbar, login_valid_user + +URL = f'{TEST_URL}/login' + + +class TestLogin: + def test_navbar_contains_login(self, selenium): + selenium.get(URL) + + nav = selenium.find_element_by_tag_name('nav').text + assert 'Login' in nav + + def test_h1_contains_login(self, selenium): + selenium.get(URL) + + title = selenium.find_element_by_tag_name('h1').text + assert 'Login' in title + + def test_it_displays_login_form(self, selenium): + selenium.get(URL) + + inputs = selenium.find_elements_by_tag_name('input') + assert len(inputs) == 3 + assert inputs[0].get_attribute('name') == 'email' + assert inputs[0].get_attribute('type') == 'email' + assert inputs[1].get_attribute('name') == 'password' + assert inputs[1].get_attribute('type') == 'password' + assert inputs[2].get_attribute('name') == '' + assert inputs[2].get_attribute('type') == 'submit' + + def test_user_can_log_in(self, selenium): + user = { + 'username': 'admin', + 'email': 'admin@example.com', + 'password': 'mpwoadmin', + } + + login_valid_user(selenium, user) + + assert_navbar(selenium, user) diff --git a/e2e/test_logout.py b/e2e/test_logout.py new file mode 100644 index 00000000..41ca4c6b --- /dev/null +++ b/e2e/test_logout.py @@ -0,0 +1,19 @@ +from .utils import register_valid_user + + +class TestLogout: + def test_user_can_log_out(self, selenium): + user = register_valid_user(selenium) + nav_items = selenium.find_elements_by_class_name('nav-item') + + nav_items[5].click() + selenium.implicitly_wait(1) + + nav = selenium.find_element_by_tag_name('nav').text + assert 'Register' in nav + assert 'Login' in nav + assert user['username'] not in nav + assert 'Logout' not in nav + + message = selenium.find_element_by_class_name('card-body').text + assert 'You are now logged out. Click here to log back in.' in message diff --git a/e2e/test_profile.py b/e2e/test_profile.py new file mode 100644 index 00000000..0ce4dd80 --- /dev/null +++ b/e2e/test_profile.py @@ -0,0 +1,20 @@ +from .utils import TEST_URL, register_valid_user + +URL = f'{TEST_URL}/profile' + + +class TestProfile: + def test_it_displays_user_profile(self, selenium): + user = register_valid_user(selenium) + + selenium.get(URL) + + assert 'Profile' in selenium.find_element_by_tag_name('h1').text + assert ( + user['username'] + in selenium.find_element_by_class_name('userName').text + ) + assert ( + user['username'] + in selenium.find_element_by_class_name('userName').text + ) diff --git a/e2e/test_registration.py b/e2e/test_registration.py new file mode 100644 index 00000000..4820bd4d --- /dev/null +++ b/e2e/test_registration.py @@ -0,0 +1,141 @@ +from .utils import ( + TEST_URL, + assert_navbar, + random_string, + register, + register_valid_user, +) + +URL = f'{TEST_URL}/register' + + +class TestRegistration: + def test_it_displays_registration_form(self, selenium): + selenium.get(URL) + selenium.implicitly_wait(1) + + inputs = selenium.find_elements_by_tag_name('input') + assert len(inputs) == 5 + assert inputs[0].get_attribute('name') == 'username' + assert inputs[0].get_attribute('type') == 'text' + assert inputs[1].get_attribute('name') == 'email' + assert inputs[1].get_attribute('type') == 'email' + assert inputs[2].get_attribute('name') == 'password' + assert inputs[2].get_attribute('type') == 'password' + assert inputs[3].get_attribute('name') == 'password_conf' + assert inputs[3].get_attribute('type') == 'password' + assert inputs[4].get_attribute('name') == '' + assert inputs[4].get_attribute('type') == 'submit' + + def test_user_can_register(self, selenium): + user = register_valid_user(selenium) + + assert_navbar(selenium, user) + + def test_user_can_not_register_with_invalid_email(self, selenium): + user_name = random_string() + user_infos = { + 'username': user_name, + 'email': user_name, + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + nav = selenium.find_element_by_tag_name('nav').text + assert 'Register' in nav + assert 'Login' in nav + + def test_user_can_not_register_if_username_is_already_taken( + self, selenium + ): + user_name = random_string() + user_infos = { + 'username': 'admin', + 'email': f'{user_name}@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert 'Sorry. That user already exists.' in errors + + def test_user_can_not_register_if_email_is_already_taken(self, selenium): + user_name = random_string() + user_infos = { + 'username': user_name, + 'email': 'admin@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert 'Sorry. That user already exists.' in errors + + def test_user_can_not_register_if_username_is_too_short(self, selenium): + user_name = random_string(2) + user_infos = { + 'username': user_name, + 'email': 'admin@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert '3 to 12 characters required for username.' in errors + + def test_user_can_not_register_if_username_is_too_long(self, selenium): + user_name = random_string(13) + user_infos = { + 'username': user_name, + 'email': 'admin@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert '3 to 12 characters required for username.' in errors + + def test_it_displays_error_if_passwords_do_not_match(self, selenium): + user_name = random_string() + user_infos = { + 'username': user_name, + 'email': f'{user_name}@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'password', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert 'Password and password confirmation don\'t match' in errors + + def test_it_displays_error_if_password_is_too_short(self, selenium): + user_name = random_string() + user_infos = { + 'username': user_name, + 'email': f'{user_name}@example.com', + 'password': 'p@ss', + 'password_conf': 'p@ss', + } + + register(selenium, user_infos) + + assert selenium.current_url == URL + errors = selenium.find_element_by_tag_name('code').text + assert '8 characters required for password.' in errors diff --git a/e2e/utils.py b/e2e/utils.py new file mode 100644 index 00000000..81a729f7 --- /dev/null +++ b/e2e/utils.py @@ -0,0 +1,73 @@ +import os +import random +import string + +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +TEST_APP_URL = os.getenv('TEST_APP_URL') +TEST_CLIENT_URL = os.getenv('TEST_CLIENT_URL') +E2E_ARGS = os.getenv('E2E_ARGS') +TEST_URL = TEST_CLIENT_URL if E2E_ARGS == 'client' else TEST_APP_URL + + +def random_string(length=8): + return ''.join(random.choice(string.ascii_letters) for x in range(length)) + + +def register(selenium, user): + selenium.get(f'{TEST_URL}/register') + selenium.implicitly_wait(1) + username = selenium.find_element_by_name('username') + username.send_keys(user.get('username')) + email = selenium.find_element_by_name('email') + email.send_keys(user.get('email')) + password = selenium.find_element_by_name('password') + password.send_keys(user.get('password')) + password_conf = selenium.find_element_by_name('password_conf') + password_conf.send_keys(user.get('password_conf')) + submit_button = selenium.find_element_by_class_name('btn') + submit_button.click() + + +def login(selenium, user): + selenium.get(f'{TEST_URL}/login') + selenium.implicitly_wait(1) + email = selenium.find_element_by_name('email') + email.send_keys(user.get('email')) + password = selenium.find_element_by_name('password') + password.send_keys(user.get('password')) + submit_button = selenium.find_element_by_class_name('btn') + submit_button.click() + + +def register_valid_user(selenium): + user_name = random_string() + user = { + 'username': user_name, + 'email': f'{user_name}@example.com', + 'password': 'p@ssw0rd', + 'password_conf': 'p@ssw0rd', + } + register(selenium, user) + WebDriverWait(selenium, 10).until(EC.url_changes(f"{TEST_URL}/register")) + return user + + +def login_valid_user(selenium, user): + login(selenium, user) + WebDriverWait(selenium, 10).until(EC.url_changes(f"{TEST_URL}/login")) + return user + + +def assert_navbar(selenium, user): + nav = selenium.find_element_by_tag_name('nav').text + assert 'Register' not in nav + assert 'Login' not in nav + assert 'Dashboard' in nav + assert 'Workouts' in nav + assert 'Statistics' in nav + assert 'Add workout' in nav + assert user['username'] in nav + assert 'Logout' in nav + assert 'en' in nav diff --git a/fittrackee_api/poetry.lock b/fittrackee_api/poetry.lock index b1379f73..83e567f0 100644 --- a/fittrackee_api/poetry.lock +++ b/fittrackee_api/poetry.lock @@ -651,6 +651,18 @@ toml = "*" version = ">=0.12" python = "<3.8" +[[package]] +name = "pytest-base-url" +version = "1.4.2" +description = "pytest plugin for URL based testing" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=2.7.3" +requests = ">=2.9" + [[package]] name = "pytest-black" version = "0.3.11" @@ -694,6 +706,18 @@ python-versions = "*" flake8 = ">=3.5" pytest = ">=3.5" +[[package]] +name = "pytest-html" +version = "2.1.1" +description = "pytest plugin for generating HTML reports" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" +pytest-metadata = "*" + [[package]] name = "pytest-isort" version = "1.2.0" @@ -708,6 +732,17 @@ tests = ["mock"] [package.dependencies] isort = ">=4.0" +[[package]] +name = "pytest-metadata" +version = "1.10.0" +description = "pytest plugin for test session metadata" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +pytest = ">=2.9.0" + [[package]] name = "pytest-runner" version = "5.2" @@ -720,6 +755,42 @@ python-versions = ">=2.7" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "pytest-virtualenv"] +[[package]] +name = "pytest-selenium" +version = "2.0.0" +description = "pytest plugin for Selenium" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +appium = ["appium-python-client (>=0.44)"] + +[package.dependencies] +pytest = ">=5.0.0" +pytest-base-url = "*" +pytest-html = ">=1.14.0" +pytest-variables = ">=1.5.0" +requests = "*" +selenium = ">=3.0.0" +tenacity = ">=6,<7" + +[[package]] +name = "pytest-variables" +version = "1.9.0" +description = "pytest plugin for providing variables to tests/fixtures" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +hjson = ["hjson"] +toml = ["toml"] +yaml = ["pyyaml"] + +[package.dependencies] +pytest = ">=2.4.2" + [[package]] name = "python-dateutil" version = "2.8.1" @@ -825,6 +896,17 @@ requests = ">=2.0" six = "*" urllib3 = ">=1.25.10" +[[package]] +name = "selenium" +version = "3.141.0" +description = "Python bindings for Selenium" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +urllib3 = "*" + [[package]] name = "six" version = "1.15.0" @@ -999,6 +1081,20 @@ python-versions = "*" Pillow = "*" requests = "*" +[[package]] +name = "tenacity" +version = "6.2.0" +description = "Retry code until it succeeds" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[package.dependencies] +six = ">=1.9.0" + [[package]] name = "toml" version = "0.10.1" @@ -1075,7 +1171,7 @@ testing = ["jaraco.itertools", "func-timeout"] [metadata] lock-version = "1.0" python-versions = "^3.7" -content-hash = "0d1e7030730e202fc78148ad1e14d5a022d8d37e165c6ace6e2c4b4f8f62cb3c" +content-hash = "2dd6612c53553c2dc00eb59a85b3f29d958ae8c7d45ba682392dd0baf06ff824" [metadata.files] alabaster = [ @@ -1476,6 +1572,10 @@ pytest = [ {file = "pytest-6.0.2-py3-none-any.whl", hash = "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40"}, {file = "pytest-6.0.2.tar.gz", hash = "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"}, ] +pytest-base-url = [ + {file = "pytest-base-url-1.4.2.tar.gz", hash = "sha256:7f1f32e08c2ee751e59e7f5880235b46e83496adc5cba5a01ca218c6fe81333d"}, + {file = "pytest_base_url-1.4.2-py2.py3-none-any.whl", hash = "sha256:8b6523a1a3af73c317bdae97b722dfb55a7336733d1ad411eb4a4931347ba77a"}, +] pytest-black = [ {file = "pytest-black-0.3.11.tar.gz", hash = "sha256:595eb0e7908b8a858a8564a5c8f0eae853c3926a4ec7b2afdfcedfa6fec65dd6"}, ] @@ -1487,14 +1587,30 @@ pytest-flake8 = [ {file = "pytest-flake8-1.0.6.tar.gz", hash = "sha256:1b82bb58c88eb1db40524018d3fcfd0424575029703b4e2d8e3ee873f2b17027"}, {file = "pytest_flake8-1.0.6-py2.py3-none-any.whl", hash = "sha256:2e91578ecd9b200066f99c1e1de0f510fbb85bcf43712d46ea29fe47607cc234"}, ] +pytest-html = [ + {file = "pytest-html-2.1.1.tar.gz", hash = "sha256:6a4ac391e105e391208e3eb9bd294a60dd336447fd8e1acddff3a6de7f4e57c5"}, + {file = "pytest_html-2.1.1-py2.py3-none-any.whl", hash = "sha256:9e4817e8be8ddde62e8653c8934d0f296b605da3d2277a052f762c56a8b32df2"}, +] pytest-isort = [ {file = "pytest-isort-1.2.0.tar.gz", hash = "sha256:f0fcf9674f3a627b36e07466d335e82b0f7c4f9e0f7ec39f2a1750b0189d5371"}, {file = "pytest_isort-1.2.0-py3-none-any.whl", hash = "sha256:2c6a1d210e8c478e418ab25df2408c235c97b1b8958fb0b139d790d0ec246f58"}, ] +pytest-metadata = [ + {file = "pytest-metadata-1.10.0.tar.gz", hash = "sha256:b7e6e0a45adacb17a03a97bf7a2ef60cc1f4e172bcce9732ce5e814191932315"}, + {file = "pytest_metadata-1.10.0-py2.py3-none-any.whl", hash = "sha256:fcbcc5781aee450107c620c79c57e50796b6777b82b3c504be9cbc3017201169"}, +] pytest-runner = [ {file = "pytest-runner-5.2.tar.gz", hash = "sha256:96c7e73ead7b93e388c5d614770d2bae6526efd997757d3543fe17b557a0942b"}, {file = "pytest_runner-5.2-py2.py3-none-any.whl", hash = "sha256:5534b08b133ef9a5e2c22c7886a8f8508c95bb0b0bdc6cc13214f269c3c70d51"}, ] +pytest-selenium = [ + {file = "pytest-selenium-2.0.0.tar.gz", hash = "sha256:6a7c655c9202fa5964b872859d8aad18a67ccbdfbaa078d154cab82914d70504"}, + {file = "pytest_selenium-2.0.0-py2.py3-none-any.whl", hash = "sha256:114bc1df383b0bb841a62ad03b222aa57d0866d57f03f81b539e59fcf43231b8"}, +] +pytest-variables = [ + {file = "pytest-variables-1.9.0.tar.gz", hash = "sha256:f79851e4c92a94c93d3f1d02377b5ac97cc8800392e87d108d2cbfda774ecc2a"}, + {file = "pytest_variables-1.9.0-py2.py3-none-any.whl", hash = "sha256:ccf4afcd70de1f5f18b4463758a19f24647a9def1805f675e80db851c9e00ac0"}, +] python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, @@ -1552,6 +1668,10 @@ responses = [ {file = "responses-0.12.0-py2.py3-none-any.whl", hash = "sha256:0de50fbf600adf5ef9f0821b85cc537acca98d66bc7776755924476775c1989c"}, {file = "responses-0.12.0.tar.gz", hash = "sha256:e80d5276011a4b79ecb62c5f82ba07aa23fb31ecbc95ee7cad6de250a3c97444"}, ] +selenium = [ + {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, + {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, +] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, @@ -1632,6 +1752,10 @@ sqlalchemy = [ staticmap = [ {file = "staticmap-0.5.4.tar.gz", hash = "sha256:9d05a1739cffa0cf6ab8f64873e6dacb36c593c23f2a70053115ef344954b315"}, ] +tenacity = [ + {file = "tenacity-6.2.0-py2.py3-none-any.whl", hash = "sha256:5a5d3dcd46381abe8b4f82b5736b8726fd3160c6c7161f53f8af7f1eb9b82173"}, + {file = "tenacity-6.2.0.tar.gz", hash = "sha256:29ae90e7faf488a8628432154bb34ace1cca58244c6ea399fd33f066ac71339a"}, +] toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, diff --git a/fittrackee_api/pyproject.toml b/fittrackee_api/pyproject.toml index 183098b2..ebc297d2 100644 --- a/fittrackee_api/pyproject.toml +++ b/fittrackee_api/pyproject.toml @@ -37,6 +37,7 @@ sphinx-bootstrap-theme = "^0.7.1" recommonmark = "^0.6.0" pyopenssl = "^19.0" freezegun = "^1.0.0" +pytest-selenium = "^2.0.0" [tool.pytest] norecursedirs = "fittrackee_api/.venv"