Add functional tests

This commit is contained in:
Gabriel Augendre 2022-09-24 19:11:01 +01:00
parent 30774d0dca
commit ecf3c4dce2
7 changed files with 680 additions and 9 deletions

287
poetry.lock generated
View file

@ -17,6 +17,22 @@ python-versions = ">=3.7"
[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]]
name = "async-generator"
version = "1.10"
description = "Async generators and context managers for Python 3.5+"
category = "dev"
optional = false
python-versions = ">=3.5"
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "22.1.0"
@ -97,6 +113,17 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
@ -296,6 +323,32 @@ python-versions = ">=3.6"
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "factory-boy"
version = "3.2.1"
description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
Faker = ">=0.7.0"
[package.extras]
dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"]
doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
[[package]]
name = "faker"
version = "14.2.1"
description = "Faker is a Python package that generates fake data for you."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
python-dateutil = ">=2.4"
[[package]]
name = "filelock"
version = "3.8.0"
@ -366,6 +419,14 @@ gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "h11"
version = "0.13.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "identify"
version = "2.5.5"
@ -467,6 +528,17 @@ category = "main"
optional = false
python-versions = ">=3.8"
[[package]]
name = "outcome"
version = "1.2.0"
description = "Capture the outcome of Python function calls."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
[[package]]
name = "packaging"
version = "21.3"
@ -567,6 +639,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.13.0"
@ -602,25 +682,46 @@ beautifulsoup4 = ">=4.5,<5.0"
packaging = ">=20"
requests = ">=2.20,<3.0"
[[package]]
name = "pysocks"
version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pytest"
version = "7.1.3"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
toml = "*"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-base-url"
version = "2.0.0"
description = "pytest plugin for URL based testing"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
pytest = ">=3.0.0,<8.0.0"
requests = ">=2.9"
[[package]]
name = "pytest-cov"
@ -652,6 +753,65 @@ pytest = ">=5.4.0"
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["django", "django-configurations (>=2.0)"]
[[package]]
name = "pytest-html"
version = "3.1.1"
description = "pytest plugin for generating HTML reports"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytest = ">=5.0,<6.0.0 || >6.0.0"
pytest-metadata = "*"
[[package]]
name = "pytest-metadata"
version = "2.0.2"
description = "pytest plugin for test session metadata"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
pytest = ">=3.0.0,<8.0.0"
[[package]]
name = "pytest-selenium"
version = "4.0.0"
description = "pytest plugin for Selenium"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
pytest = ">=6.0.0,<7.0.0"
pytest-base-url = ">=2.0.0,<3.0.0"
pytest-html = ">=2.0.0"
pytest-variables = ">=2.0.0,<3.0.0"
requests = ">=2.26.0,<3.0.0"
selenium = ">=4.0.0,<5.0.0"
tenacity = ">=6.0.0,<7.0.0"
[package.extras]
appium = ["appium-python-client (>=2.0.0,<3.0.0)"]
[[package]]
name = "pytest-variables"
version = "2.0.0"
description = "pytest plugin for providing variables to tests/fixtures"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
[package.dependencies]
pytest = ">=3.0.0,<8.0.0"
[package.extras]
toml = ["toml"]
yaml = ["pyyaml"]
hjson = ["hjson"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
@ -724,6 +884,20 @@ python-versions = "*"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
name = "selenium"
version = "4.4.3"
description = ""
category = "dev"
optional = false
python-versions = "~=3.7"
[package.dependencies]
certifi = ">=2021.10.8"
trio = ">=0.17,<1.0"
trio-websocket = ">=0.9,<1.0"
urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
[[package]]
name = "setuptools-scm"
version = "7.0.5"
@ -749,6 +923,22 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "soupsieve"
version = "2.3.2.post1"
@ -765,6 +955,20 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "tenacity"
version = "6.3.1"
description = "Retry code until it succeeds"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.9.0"
[package.extras]
doc = ["reno", "sphinx", "tornado (>=4.5)"]
[[package]]
name = "toml"
version = "0.10.2"
@ -781,6 +985,36 @@ category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "trio"
version = "0.21.0"
description = "A friendly Python library for async concurrency and I/O"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
async-generator = ">=1.9"
attrs = ">=19.2.0"
cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""}
idna = "*"
outcome = "*"
sniffio = "*"
sortedcontainers = "*"
[[package]]
name = "trio-websocket"
version = "0.9.2"
description = "WebSocket library for Trio"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
async-generator = ">=1.10"
trio = ">=0.11"
wsproto = ">=0.14"
[[package]]
name = "typing-extensions"
version = "4.3.0"
@ -805,6 +1039,9 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
[package.dependencies]
PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""}
[package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
@ -849,10 +1086,21 @@ Brotli = {version = "*", optional = true, markers = "extra == \"brotli\""}
[package.extras]
brotli = ["brotli"]
[[package]]
name = "wsproto"
version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
category = "dev"
optional = false
python-versions = ">=3.7.0"
[package.dependencies]
h11 = ">=0.9.0,<1"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "b0fdaacb7c4e3ecc472664d3091567919a34505639c79e6ecfe8f58a00020566"
content-hash = "a7f456302a8e5cd5023a9f6ced2930e9ec4a2e964daae04f24a486b2071fd3a0"
[metadata.files]
ansicon = []
@ -860,6 +1108,8 @@ asgiref = [
{file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
{file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
]
async-generator = []
atomicwrites = []
attrs = []
beautifulsoup4 = [
{file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
@ -932,6 +1182,7 @@ brotli = [
{file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
]
certifi = []
cffi = []
cfgv = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
@ -970,6 +1221,8 @@ django-environ = [
{file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"},
]
django-extensions = []
factory-boy = []
faker = []
filelock = []
fonttools = []
freezegun = []
@ -978,6 +1231,7 @@ gunicorn = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
]
h11 = []
identify = []
idna = []
iniconfig = [
@ -994,6 +1248,7 @@ matplotlib = []
model-bakery = []
nodeenv = []
numpy = []
outcome = []
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@ -1014,13 +1269,19 @@ py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycparser = []
pygments = []
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pypi-simple = []
pytest = []
pysocks = []
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
pytest-base-url = []
pytest-cov = [
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
@ -1029,6 +1290,13 @@ pytest-django = [
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
]
pytest-html = [
{file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"},
{file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"},
]
pytest-metadata = []
pytest-selenium = []
pytest-variables = []
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
@ -1075,16 +1343,20 @@ requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
selenium = []
setuptools-scm = []
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sniffio = []
sortedcontainers = []
soupsieve = [
{file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
]
sqlparse = []
tenacity = []
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@ -1093,6 +1365,8 @@ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
trio = []
trio-websocket = []
typing-extensions = []
tzdata = []
urllib3 = []
@ -1105,3 +1379,4 @@ whitenoise = [
{file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"},
{file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"},
]
wsproto = []

View file

@ -24,12 +24,15 @@ freezegun = "^1.2.1"
[tool.poetry.dev-dependencies]
pre-commit = "^2.7"
pytest = "^7.1"
pytest = "^6.0"
pytest-django = "^4.5"
model-bakery = "^1.1"
pytest-cov = "^3.0"
poetry-deps-scanner = "^2.0"
invoke = "^1.7.0"
factory-boy = "^3.2.1"
pytest-selenium = "^4.0.0"
selenium = "^4.4.3"
[tool.black]
target-version = ['py310']
@ -38,7 +41,7 @@ target-version = ['py310']
profile = "black"
[tool.pytest.ini_options]
addopts = "--color=yes"
addopts = "--color=yes --driver Firefox"
minversion = "6.0"
DJANGO_SETTINGS_MODULE = "checkout.settings"
testpaths = [

19
src/conftest.py Normal file
View file

@ -0,0 +1,19 @@
import pytest
from django.core.management import call_command
@pytest.fixture(scope="session")
def _collectstatic():
call_command("collectstatic", interactive=False, verbosity=0)
@pytest.fixture()
def live_server(settings, live_server):
settings.STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
return live_server
@pytest.fixture()
def selenium(selenium):
selenium.implicitly_wait(3)
return selenium

View file

@ -1 +0,0 @@
# Create your tests here.

View file

View file

@ -0,0 +1,84 @@
import random
from functools import partial
import factory
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group, Permission
from common.models import User
from purchase.models import Basket, BasketItem, PaymentMethod, Product
USER_PASSWORD = "test_password"
class CashierFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Faker("user_name")
password = make_password(USER_PASSWORD)
is_active = True
is_staff = True
@factory.post_generation
def groups(self, create, extracted, **kwargs):
if create:
self.groups.add(CashierGroupFactory())
class ProductFactory(factory.django.DjangoModelFactory):
class Meta:
model = Product
name = factory.Faker("text", max_nb_chars=80)
unit_price_cents = factory.LazyFunction(partial(random.randint, 80, 650))
class PaymentMethodFactory(factory.django.DjangoModelFactory):
class Meta:
model = PaymentMethod
name = factory.Faker("text", max_nb_chars=30)
class BasketWithItemsFactory(factory.django.DjangoModelFactory):
class Meta:
model = Basket
payment_method = factory.Iterator(PaymentMethod.objects.all())
@factory.post_generation
def items(self, create, extracted, **kwargs):
if create:
products = Product.objects.order_by("?")
quantity = random.randint(1, len(products))
for product in products[:quantity]:
BasketItem.objects.create(
product=product,
basket=self,
quantity=random.randint(1, 4),
unit_price_cents=product.unit_price_cents,
)
class CashierGroupFactory(factory.django.DjangoModelFactory):
class Meta:
model = Group
name = "Caissier"
@factory.post_generation
def permissions(self, create, extracted, **kwargs):
if create:
self.permissions.add(
Permission.objects.get(codename="add_basket"),
Permission.objects.get(codename="change_basket"),
Permission.objects.get(codename="delete_basket"),
Permission.objects.get(codename="view_basket"),
Permission.objects.get(codename="add_basketitem"),
Permission.objects.get(codename="change_basketitem"),
Permission.objects.get(codename="delete_basketitem"),
Permission.objects.get(codename="view_basketitem"),
Permission.objects.get(codename="view_paymentmethod"),
Permission.objects.get(codename="view_product"),
)

View file

@ -0,0 +1,291 @@
import time
import freezegun
from django.urls import reverse
from pytest_django.live_server_helper import LiveServer
from selenium.webdriver import ActionChains, Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from common.models import User
from purchase.models import Basket
from purchase.tests.factories import (
USER_PASSWORD,
BasketWithItemsFactory,
CashierFactory,
PaymentMethodFactory,
ProductFactory,
)
@freezegun.freeze_time("2022-09-24 19:01:00+0200")
def test_cashier_create_and_update_basket(live_server: LiveServer, selenium: WebDriver):
wait = WebDriverWait(selenium, 10)
assert Basket.objects.count() == 0
# Setup data
cashier = CashierFactory()
products = [
ProductFactory(),
ProductFactory(),
ProductFactory(),
]
payment_methods = [
PaymentMethodFactory(),
PaymentMethodFactory(),
PaymentMethodFactory(),
]
login(live_server, selenium, cashier)
# Assert products are displayed
redirect_url = live_reverse(live_server, "purchase:new")
wait.until(lambda driver: driver.current_url == redirect_url)
displayed_products = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100")
assert len(displayed_products) == len(products)
for product, displayed_product in zip(products, displayed_products):
assert (
product.name
== displayed_product.find_element(By.CLASS_NAME, "card-title").text
)
# Assert quantity of all products is 0
for displayed_product in displayed_products:
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 0
# Click on - on product 1
displayed_product = displayed_products[0]
displayed_product.find_element(By.CLASS_NAME, "btn-danger").click()
# Assert quantity is still 0
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 0
# Click two times on + on product 1
button_plus = displayed_product.find_element(By.CLASS_NAME, "btn-success")
button_plus.click()
button_plus.click()
# Assert quantity is 2
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 2
# Adjust manually quantity for product 2: 4
displayed_product = displayed_products[1]
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
chain = ActionChains(selenium)
chain.double_click(quantity_input).perform()
quantity_input.send_keys("4")
# Don't add payment method
# Save
selenium.find_element(By.ID, "submit-id-submit").click()
# Assert entries saved in DB (new basket with proper products)
assert Basket.objects.count() == 1
basket = Basket.objects.priced().first()
assert basket.payment_method is None
assert basket.items.count() == 2
assert basket.items.get(product=products[0]).quantity == 2
assert (
basket.items.get(product=products[0]).unit_price_cents
== products[0].unit_price_cents
)
assert basket.items.get(product=products[1]).quantity == 4
assert (
basket.items.get(product=products[1]).unit_price_cents
== products[1].unit_price_cents
)
# Assert redirected to basket update view
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
wait.until(lambda driver: driver.current_url == redirect_url)
# Assert message in green for successful basket creation
created_message = selenium.find_element(By.CSS_SELECTOR, ".messages .alert-success")
assert created_message.text == "Panier correctement créé."
# Assert message in red for missing payment method
missing_payment = selenium.find_element(By.CSS_SELECTOR, ".alert.alert-danger")
assert missing_payment.text == "Moyen de paiement manquant."
# Assert ID, price, date & product quantities
# Selected products have a green background
title = selenium.find_element(By.TAG_NAME, "h1")
assert title.text == f"Panier n°{basket.pk} {basket.price/100:.2f}"
date = selenium.find_element(By.CLASS_NAME, "metadata")
assert date.text == "24 septembre 2022 19:01"
displayed_products = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100")
displayed_product = displayed_products[0]
assert "bg-success" in displayed_product.get_attribute("class")
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 2
displayed_product = displayed_products[1]
assert "bg-success" in displayed_product.get_attribute("class")
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 4
displayed_product = displayed_products[2]
assert "bg-success" not in displayed_product.get_attribute("class")
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 0
# Click on - on product 2
displayed_product = displayed_products[1]
displayed_product.find_element(By.CLASS_NAME, "btn-danger").click()
# Assert quantity is 3
quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput")
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 3
# Add payment method
selenium.find_element(By.TAG_NAME, "html").send_keys(Keys.END)
time.sleep(1)
selenium.find_element(By.ID, f"id_payment_method_{payment_methods[1].pk}").click()
# Save
selenium.find_element(By.ID, "submit-id-submit").click()
# Assert changed in DB
assert Basket.objects.count() == 1
basket = Basket.objects.priced().first()
assert basket.payment_method == payment_methods[1]
assert basket.items.count() == 2
assert basket.items.get(product=products[0]).quantity == 2
assert (
basket.items.get(product=products[0]).unit_price_cents
== products[0].unit_price_cents
)
assert basket.items.get(product=products[1]).quantity == 3
assert (
basket.items.get(product=products[1]).unit_price_cents
== products[1].unit_price_cents
)
# Assert redirected to same view
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
wait.until(lambda driver: driver.current_url == redirect_url)
# Assert message in green for successful basket update
created_message = selenium.find_element(By.CSS_SELECTOR, ".messages .alert-success")
assert created_message.text == "Panier correctement modifié."
# Assert no more red message
missing_payment = selenium.find_elements(By.CSS_SELECTOR, ".alert.alert-danger")
assert len(missing_payment) == 0
def login(
live_server: LiveServer, selenium: WebDriver, cashier: User, url: str = "/"
) -> None:
# Go to page
url = live_url(live_server, url)
selenium.get(url)
# Login
selenium.find_element(By.ID, "id_username").send_keys(cashier.username)
selenium.find_element(By.ID, "id_password").send_keys(USER_PASSWORD)
selenium.find_element(By.ID, "id_password").send_keys(Keys.RETURN)
@freezegun.freeze_time("2022-09-24 19:03:00+0200")
def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
wait = WebDriverWait(selenium, 10)
# Setup test data
cashier = CashierFactory()
_ = [
ProductFactory(),
ProductFactory(),
ProductFactory(),
]
_ = [
PaymentMethodFactory(),
PaymentMethodFactory(),
PaymentMethodFactory(),
]
with freezegun.freeze_time("2022-09-24 19:01:00+0200"):
basket_with_payment_method = BasketWithItemsFactory()
basket_with_payment_method = Basket.objects.priced().get(
pk=basket_with_payment_method.pk
)
with freezegun.freeze_time("2022-09-24 19:02:00+0200"):
basket_no_payment_method = BasketWithItemsFactory(payment_method=None)
basket_no_payment_method = Basket.objects.priced().get(
pk=basket_no_payment_method.pk
)
# Login
url = reverse("purchase:list")
login(live_server, selenium, cashier, url)
# Assert first basket (last created) has yellow background
# Assert basket info displayed
displayed_baskets = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100")
first_basket = displayed_baskets[0]
assert "bg-warning" in first_basket.get_attribute("class")
text = first_basket.text.replace("\n", " ")
assert f"{basket_no_payment_method.pk} " in text
expected_articles_count = basket_no_payment_method.items.count()
assert f" {expected_articles_count} article" in text
expected_price = basket_no_payment_method.price / 100
assert f" {expected_price:.2f}" in text
expected_payment_method = "-"
assert f" {expected_payment_method} " in text
assert "19:02" in text
# Assert second basket (first created) doesn't have yellow background
# Assert basket info displayed including payment method
second_basket = displayed_baskets[1]
assert "bg-warning" not in second_basket.get_attribute("class")
text = second_basket.text.replace("\n", " ")
assert f"{basket_with_payment_method.pk} " in text
expected_articles_count = basket_with_payment_method.items.count()
assert f" {expected_articles_count} article" in text
expected_price = basket_with_payment_method.price / 100
assert f" {expected_price:.2f}" in text
expected_payment_method = basket_with_payment_method.payment_method.name
assert f" {expected_payment_method} " in text
assert "19:01" in text
# Click on delete on second basket
second_basket.find_element(By.CLASS_NAME, "btn-danger").click()
# Confirm deletion
selenium.find_element(By.CLASS_NAME, "btn-danger").click()
# Assert object deleted in DB
assert Basket.objects.count() == 1
assert Basket.objects.first() == basket_no_payment_method
# Assert redirected to list view
wait.until(
lambda driver: driver.current_url == live_reverse(live_server, "purchase:list")
)
# Click on edit on remaining basket
displayed_baskets = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100")
displayed_baskets[0].find_element(By.CLASS_NAME, "btn-primary").click()
# Assert redirected to edit view
redirect_url = live_reverse(
live_server, "purchase:update", pk=basket_no_payment_method.pk
)
wait.until(lambda driver: driver.current_url == redirect_url)
def live_reverse(live_server: LiveServer, url_name: str, **kwargs) -> str:
path = reverse(url_name, kwargs=kwargs)
return live_url(live_server, path)
def live_url(live_server: LiveServer, path: str) -> str:
return live_server.url + path