API & Client - replace testcafe with selenium for e2e tests

This commit is contained in:
Sam 2020-09-13 21:39:44 +02:00
parent f626559e5e
commit 1ad847c857
12 changed files with 481 additions and 5 deletions

View File

@ -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)

View File

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

0
e2e/__init__.py Normal file
View File

38
e2e/test_activities.py Normal file
View File

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

17
e2e/test_index.py Normal file
View File

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

40
e2e/test_login.py Normal file
View File

@ -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)

19
e2e/test_logout.py Normal file
View File

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

20
e2e/test_profile.py Normal file
View File

@ -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
)

141
e2e/test_registration.py Normal file
View File

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

73
e2e/utils.py Normal file
View File

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

View File

@ -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"},

View File

@ -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"