From 42eda26e607e96b7097204a232181bb3d3f85846 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 14 Jul 2022 10:50:39 +0100 Subject: [PATCH 01/15] install selenium & a driver manager --- poetry.lock | 197 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 6838adc..d8d0fd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[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" @@ -28,6 +36,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 = "charset-normalizer" version = "2.1.0" @@ -55,6 +74,25 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "cryptography" +version = "37.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "flask" version = "2.0.3" @@ -73,6 +111,17 @@ Werkzeug = ">=2.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[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.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "idna" version = "3.3" @@ -136,6 +185,17 @@ category = "main" optional = false python-versions = ">=3.7" +[[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" @@ -170,6 +230,29 @@ 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 = "pyopenssl" +version = "22.0.0" +description = "Python wrapper module around the OpenSSL library" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=35.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -181,6 +264,14 @@ python-versions = ">=3.6.8" [package.extras] diagrams = ["railroad-diagrams", "jinja2"] +[[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.2" @@ -257,6 +348,19 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "selenium" +version = "4.3.0" +description = "" +category = "dev" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +urllib3 = {version = ">=1.26,<2.0", extras = ["secure", "socks"]} + [[package]] name = "six" version = "1.16.0" @@ -265,6 +369,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "tomli" version = "2.0.1" @@ -273,6 +393,36 @@ category = "dev" 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" @@ -289,11 +439,30 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +[package.dependencies] +certifi = {version = "*", optional = true, markers = "extra == \"secure\""} +cryptography = {version = ">=1.3.4", optional = true, markers = "extra == \"secure\""} +idna = {version = ">=2.0.0", optional = true, markers = "extra == \"secure\""} +pyOpenSSL = {version = ">=0.14", optional = true, markers = "extra == \"secure\""} +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", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "webdriver-manager" +version = "3.8.1" +description = "Library provides the way to automatically manage drivers for different browsers" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +python-dotenv = "*" +requests = "*" + [[package]] name = "werkzeug" version = "2.0.3" @@ -305,6 +474,17 @@ python-versions = ">=3.6" [package.extras] watchdog = ["watchdog"] +[[package]] +name = "wsproto" +version = "1.1.0" +description = "WebSockets state-machine based protocol implementation" +category = "dev" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +h11 = ">=0.9.0,<1" + [[package]] name = "zipp" version = "3.8.1" @@ -320,34 +500,49 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "afb7e4dcf79ed8f5c42861b1494fd99768c1008229e22613227d3c0264437b23" +content-hash = "f933ff07b6d0fd62f7827b6e5eabbea8e8c01b4ab339a92eab42dc37d01c3cf4" [metadata.files] +async-generator = [] atomicwrites = [] attrs = [] certifi = [] +cffi = [] charset-normalizer = [] click = [] colorama = [] +cryptography = [] flask = [] +h11 = [] idna = [] importlib-metadata = [] iniconfig = [] itsdangerous = [] jinja2 = [] markupsafe = [] +outcome = [] packaging = [] pluggy = [] py = [] +pycparser = [] +pyopenssl = [] pyparsing = [] +pysocks = [] pytest = [] pytest-mock = [] python-dateutil = [] python-dotenv = [] requests = [] +selenium = [] six = [] +sniffio = [] +sortedcontainers = [] tomli = [] +trio = [] +trio-websocket = [] typing-extensions = [] urllib3 = [] +webdriver-manager = [] werkzeug = [] +wsproto = [] zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 2c177b4..c4bc562 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ python-dateutil = "^2.8.2" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pytest-mock = "^3.8.2" +selenium = "^4.3.0" +webdriver-manager = "^3.8.1" [build-system] requires = ["poetry>=0.12"] From 32f769602de2b44b8752caf63525271e03d65387 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 14 Jul 2022 11:48:23 +0100 Subject: [PATCH 02/15] add simple selenium test just checks if the header is correct currently however just checks the header & uses a production board! bad! --- selenium_tests/__init__.py | 0 selenium_tests/app_test.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 selenium_tests/__init__.py create mode 100644 selenium_tests/app_test.py diff --git a/selenium_tests/__init__.py b/selenium_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py new file mode 100644 index 0000000..b4a5468 --- /dev/null +++ b/selenium_tests/app_test.py @@ -0,0 +1,42 @@ +from os import environ +from threading import Thread +from time import sleep + +from selenium import webdriver +from webdriver_manager.microsoft import EdgeChromiumDriverManager + +from todo_app import app + +import pytest + + +@pytest.fixture(scope='module') +def app_with_temp_board(): + # board_id = "abcd" + # environ['BOARD_ID'] = board_id + + todo_app = app.create_app() + + thread = Thread(target=lambda: todo_app.run(use_reloader=False)) + + thread.daemon = True + thread.start() + + sleep(1) + + yield todo_app + + thread.join(1) + # delete trello board? + + +@pytest.fixture(scope='module') +def driver(): + with webdriver.Edge(EdgeChromiumDriverManager().install()) as driver: + yield driver + + +def test_app_loads(driver, app_with_temp_board): + driver.get("http://localhost:5000") + + assert driver.title == 'To-Do App' From 62d3e252196d076baf9a422961427fe1683b62ee Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 14 Jul 2022 11:50:56 +0100 Subject: [PATCH 03/15] add new env variable ORGANIZATION_ID used for creating a test board in selenium tests --- .env | 12 ++++++++++++ .env.template | 6 +++++- todo_app/data/trello_tasks.py | 6 ++++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..7421224 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# Flask server configuration. +FLASK_APP=todo_app/app +FLASK_ENV=development + +# Change the following values for local development. +SECRET_KEY=#5fjA@kA#PnACdvM + +TRELLO_KEY=b2913e0f31ec7f6dbbed884d221d0c80 +TRELLO_TOKEN=ed0e04c8d3fd0e66e43c6a3bb8aa2ad890945d66a856916725280a7ff83d0b80 + +BOARD_ID=AVaJn7PF +ORGANIZATION_ID=project2290 \ No newline at end of file diff --git a/.env.template b/.env.template index d2b3880..140fbae 100644 --- a/.env.template +++ b/.env.template @@ -10,4 +10,8 @@ TRELLO_KEY=trello-key TRELLO_TOKEN=trello-token # The short board id to use for this app -BOARD_ID=abcdefgh \ No newline at end of file +BOARD_ID=abcdefgh + +# The organization (aka workspace) id to use for creating test boards +# Only needed for testing +ORGANIZATION_ID=abcdefg \ No newline at end of file diff --git a/todo_app/data/trello_tasks.py b/todo_app/data/trello_tasks.py index 994d5ca..407b34c 100644 --- a/todo_app/data/trello_tasks.py +++ b/todo_app/data/trello_tasks.py @@ -10,6 +10,7 @@ BASE_URL = "https://api.trello.com/1/" BOARD_ID = None +ORGANIZATION_ID = None BOARD_URL = None BOARD_LISTS_URL = None @@ -25,10 +26,11 @@ def init_env(): - global BOARD_ID, BASE_URL, BOARD_URL, BOARD_LISTS_URL, LISTS_URL, \ - LIST_CARDS_URL, CARD_URL, CARDS_URL, DEFAULT_PARAMS + global BOARD_ID, ORGANIZATION_ID, BASE_URL, BOARD_URL, BOARD_LISTS_URL, \ + LISTS_URL, LIST_CARDS_URL, CARD_URL, CARDS_URL, DEFAULT_PARAMS BOARD_ID = getenv("BOARD_ID") + ORGANIZATION_ID = getenv("ORGANIZATION_ID") BOARD_URL = "%sboards/{id}" % BASE_URL BOARD_LISTS_URL = "%sboards/{id}/lists" % BASE_URL From 0ff35bc7f2845397cb2bcde6093122e48b85607d Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 14 Jul 2022 11:59:11 +0100 Subject: [PATCH 04/15] make a start on trying to use a testing board however its still using the production one! --- selenium_tests/app_test.py | 55 ++++++++++++++++++++++++++++------- todo_app/data/trello_tasks.py | 4 ++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index b4a5468..085ddac 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -2,6 +2,8 @@ from threading import Thread from time import sleep +from _pytest.monkeypatch import MonkeyPatch +from requests import post, delete from selenium import webdriver from webdriver_manager.microsoft import EdgeChromiumDriverManager @@ -9,25 +11,58 @@ import pytest +from todo_app.data import session_tasks + + +def create_test_board(): + from todo_app.data.session_tasks import ORGANIZATION_ID, BOARDS_URL, DEFAULT_PARAMS + + create_params = { + **DEFAULT_PARAMS, + 'idOrganization': ORGANIZATION_ID, + 'defaultLists': "false", + 'name': "TEST" + } + + response = post(BOARDS_URL, params=create_params).json() + + return response['id'] + + +def delete_test_board(id): + from todo_app.data.session_tasks import BOARD_URL, DEFAULT_PARAMS + + delete(BOARD_URL.format(id=id), params=DEFAULT_PARAMS) + @pytest.fixture(scope='module') def app_with_temp_board(): - # board_id = "abcd" - # environ['BOARD_ID'] = board_id + with MonkeyPatch().context() as monkeypatch: + session_tasks.init_env() + + board_id = create_test_board() + + monkeypatch.setattr(session_tasks, "BOARD_ID", board_id) + monkeypatch.setattr(session_tasks, "init_env", lambda: None) + environ["BOARD_ID"] = board_id + + thread = Thread(target=lambda: todo_app.run(use_reloader=False)) + + todo_app = app.create_app() - todo_app = app.create_app() + print(session_tasks.BOARD_ID) + print(board_id) - thread = Thread(target=lambda: todo_app.run(use_reloader=False)) + thread.daemon = True + thread.start() - thread.daemon = True - thread.start() + sleep(1) - sleep(1) + yield todo_app - yield todo_app + thread.join(1) - thread.join(1) - # delete trello board? + delete_test_board(board_id) @pytest.fixture(scope='module') diff --git a/todo_app/data/trello_tasks.py b/todo_app/data/trello_tasks.py index 407b34c..2b57b43 100644 --- a/todo_app/data/trello_tasks.py +++ b/todo_app/data/trello_tasks.py @@ -13,6 +13,7 @@ ORGANIZATION_ID = None BOARD_URL = None +BOARDS_URL = None BOARD_LISTS_URL = None LISTS_URL = None LIST_CARDS_URL = None @@ -26,13 +27,14 @@ def init_env(): - global BOARD_ID, ORGANIZATION_ID, BASE_URL, BOARD_URL, BOARD_LISTS_URL, \ + global BOARD_ID, ORGANIZATION_ID, BASE_URL, BOARD_URL, BOARDS_URL, BOARD_LISTS_URL, \ LISTS_URL, LIST_CARDS_URL, CARD_URL, CARDS_URL, DEFAULT_PARAMS BOARD_ID = getenv("BOARD_ID") ORGANIZATION_ID = getenv("ORGANIZATION_ID") BOARD_URL = "%sboards/{id}" % BASE_URL + BOARDS_URL = "%sboards" % BASE_URL BOARD_LISTS_URL = "%sboards/{id}/lists" % BASE_URL LISTS_URL = "%slists" % BASE_URL LIST_CARDS_URL = "%slists/{id}/cards" % BASE_URL From fcdb6121653f145a882b245a064631c62e029967 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 09:56:13 +0100 Subject: [PATCH 05/15] make app never cache env constants, instead loading at each usage this way the env consts can be easily switched out without micromanaging import order --- todo_app/app.py | 2 -- todo_app/data/env.py | 16 +++++++++ todo_app/data/trello_tasks.py | 61 +++++++++-------------------------- 3 files changed, 32 insertions(+), 47 deletions(-) create mode 100644 todo_app/data/env.py diff --git a/todo_app/app.py b/todo_app/app.py index 65dedf0..ba34817 100644 --- a/todo_app/app.py +++ b/todo_app/app.py @@ -11,8 +11,6 @@ def create_app(): app = Flask(__name__) app.config.from_object(Config()) - session_tasks.init_env() - @app.route('/') def index(): tasks_view_model = ViewModel(get_tasks()) diff --git a/todo_app/data/env.py b/todo_app/data/env.py new file mode 100644 index 0000000..88b7eec --- /dev/null +++ b/todo_app/data/env.py @@ -0,0 +1,16 @@ +from os import getenv + + +def get_board_id(): + return getenv("BOARD_ID") + + +def get_organization_id(): + return getenv("ORGANIZATION_ID") + + +def get_default_params(): + return { + 'key': getenv("TRELLO_KEY"), + 'token': getenv("TRELLO_TOKEN") + } \ No newline at end of file diff --git a/todo_app/data/trello_tasks.py b/todo_app/data/trello_tasks.py index 2b57b43..7a13fba 100644 --- a/todo_app/data/trello_tasks.py +++ b/todo_app/data/trello_tasks.py @@ -1,50 +1,21 @@ from json import JSONDecodeError -from os import getenv from typing import List import requests +from todo_app.data.env import get_default_params, get_board_id from todo_app.data.task import Task BASE_URL = "https://api.trello.com/1/" -BOARD_ID = None -ORGANIZATION_ID = None - -BOARD_URL = None -BOARDS_URL = None -BOARD_LISTS_URL = None -LISTS_URL = None -LIST_CARDS_URL = None -CARD_URL = None -CARDS_URL = None - -DEFAULT_PARAMS = { - 'key': getenv("TRELLO_KEY"), - 'token': getenv("TRELLO_TOKEN") -} - - -def init_env(): - global BOARD_ID, ORGANIZATION_ID, BASE_URL, BOARD_URL, BOARDS_URL, BOARD_LISTS_URL, \ - LISTS_URL, LIST_CARDS_URL, CARD_URL, CARDS_URL, DEFAULT_PARAMS - - BOARD_ID = getenv("BOARD_ID") - ORGANIZATION_ID = getenv("ORGANIZATION_ID") - - BOARD_URL = "%sboards/{id}" % BASE_URL - BOARDS_URL = "%sboards" % BASE_URL - BOARD_LISTS_URL = "%sboards/{id}/lists" % BASE_URL - LISTS_URL = "%slists" % BASE_URL - LIST_CARDS_URL = "%slists/{id}/cards" % BASE_URL - CARD_URL = "%scards/{id}" % BASE_URL - CARDS_URL = "%scards" % BASE_URL - - DEFAULT_PARAMS = { - 'key': getenv("TRELLO_KEY"), - 'token': getenv("TRELLO_TOKEN") - } +BOARD_URL = "%sboards/{id}" % BASE_URL +BOARDS_URL = "%sboards" % BASE_URL +BOARD_LISTS_URL = "%sboards/{id}/lists" % BASE_URL +LISTS_URL = "%slists" % BASE_URL +LIST_CARDS_URL = "%slists/{id}/cards" % BASE_URL +CARD_URL = "%scards/{id}" % BASE_URL +CARDS_URL = "%scards" % BASE_URL def toggle(status): @@ -56,14 +27,14 @@ def toggle(status): def get_or_create_list(name): - response = requests.get(BOARD_LISTS_URL.format(id=BOARD_ID), params=DEFAULT_PARAMS).json() + response = requests.get(BOARD_LISTS_URL.format(id=get_board_id()), params=get_default_params()).json() id = next((list['id'] for list in response if list['name'] == name), None) # if this is ie a new board without the list made yet if id is None: create_list_params = { - **DEFAULT_PARAMS, + **get_default_params(), 'name': name, 'idBoard': get_long_board_id() } @@ -89,14 +60,14 @@ def get_long_board_id(): global _long_board_id if _long_board_id is None: - response = requests.get(BOARD_URL.format(id=BOARD_ID), params=DEFAULT_PARAMS).json() + response = requests.get(BOARD_URL.format(id=get_board_id()), params=get_default_params()).json() _long_board_id = response["id"] return _long_board_id def get_tasks_from_list(id, status): - response = requests.get(LIST_CARDS_URL.format(id=id), params=DEFAULT_PARAMS).json() + response = requests.get(LIST_CARDS_URL.format(id=id), params=get_default_params()).json() return [Task.from_card(card, status) for card in response] @@ -126,7 +97,7 @@ def get_task(id) -> Task | None: """ try: - response = requests.get(CARD_URL.format(id=id), params=DEFAULT_PARAMS).json() + response = requests.get(CARD_URL.format(id=id), params=get_default_params()).json() except JSONDecodeError: return None @@ -154,7 +125,7 @@ def add_task(title): todo_list_id, _ = get_list_ids() add_params = { - **DEFAULT_PARAMS, + **get_default_params(), 'idList': todo_list_id, 'name': title } @@ -172,7 +143,7 @@ def delete_task(id): id: The ID of the task. """ - requests.delete(CARD_URL.format(id=id), params=DEFAULT_PARAMS) + requests.delete(CARD_URL.format(id=id), params=get_default_params()) def save_task(task: Task): @@ -188,7 +159,7 @@ def save_task(task: Task): list_id = done_list_id if task.status == "complete" else todo_list_id update_params = { - **DEFAULT_PARAMS, + **get_default_params(), 'name': task.title, 'idList': list_id, 'desc': task.description, From aaecd8335a369d16d8c2a616f782208b2eab1601 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 09:56:55 +0100 Subject: [PATCH 06/15] rework app_test.py to use this new structure now it uses temporary credentials as expected --- selenium_tests/app_test.py | 40 ++++++++++++++------------------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 085ddac..059263a 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -12,14 +12,14 @@ import pytest from todo_app.data import session_tasks +from todo_app.data.env import get_default_params, get_organization_id, get_board_id +from todo_app.data.session_tasks import BOARD_URL, BOARDS_URL def create_test_board(): - from todo_app.data.session_tasks import ORGANIZATION_ID, BOARDS_URL, DEFAULT_PARAMS - create_params = { - **DEFAULT_PARAMS, - 'idOrganization': ORGANIZATION_ID, + **get_default_params(), + 'idOrganization': get_organization_id(), 'defaultLists': "false", 'name': "TEST" } @@ -30,39 +30,29 @@ def create_test_board(): def delete_test_board(id): - from todo_app.data.session_tasks import BOARD_URL, DEFAULT_PARAMS - - delete(BOARD_URL.format(id=id), params=DEFAULT_PARAMS) + delete(BOARD_URL.format(id=id), params=get_default_params()) @pytest.fixture(scope='module') def app_with_temp_board(): - with MonkeyPatch().context() as monkeypatch: - session_tasks.init_env() - - board_id = create_test_board() - - monkeypatch.setattr(session_tasks, "BOARD_ID", board_id) - monkeypatch.setattr(session_tasks, "init_env", lambda: None) - environ["BOARD_ID"] = board_id + board_id = create_test_board() - thread = Thread(target=lambda: todo_app.run(use_reloader=False)) + environ["BOARD_ID"] = board_id - todo_app = app.create_app() + thread = Thread(target=lambda: todo_app.run(use_reloader=False)) - print(session_tasks.BOARD_ID) - print(board_id) + todo_app = app.create_app() - thread.daemon = True - thread.start() + thread.daemon = True + thread.start() - sleep(1) + sleep(1) - yield todo_app + yield todo_app - thread.join(1) + thread.join(1) - delete_test_board(board_id) + delete_test_board(board_id) @pytest.fixture(scope='module') From 56242aab79f5772626a7fb2941a5ed5389f78829 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 10:19:15 +0100 Subject: [PATCH 07/15] add a selenium test to check for adding items --- selenium_tests/app_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 059263a..a91efe7 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -5,6 +5,7 @@ from _pytest.monkeypatch import MonkeyPatch from requests import post, delete from selenium import webdriver +from selenium.webdriver.common.by import By from webdriver_manager.microsoft import EdgeChromiumDriverManager from todo_app import app @@ -65,3 +66,15 @@ def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") assert driver.title == 'To-Do App' + + +def test_add_element(driver, app_with_temp_board): + driver.get("http://localhost:5000") + + add_name = driver.find_element(By.ID, "itemtitle") + add_name.send_keys("This is a hard task") + + add_button = driver.find_element(By.ID, "btn-add") + add_button.click() + + assert "This is a hard task" in driver.page_source From 8e9e2a9baa3cfaa6c93000ffb170d97be5fe4bdc Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 10:52:44 +0100 Subject: [PATCH 08/15] add a selenium tests to check for deleting items --- selenium_tests/app_test.py | 56 ++++++++++++++++++++++++++++++++---- todo_app/templates/item.html | 4 +-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index a91efe7..b5f82e9 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -6,6 +6,7 @@ from requests import post, delete from selenium import webdriver from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement from webdriver_manager.microsoft import EdgeChromiumDriverManager from todo_app import app @@ -62,6 +63,13 @@ def driver(): yield driver +def add_task(name, driver): + add_name = driver.find_element(By.ID, "itemtitle") + add_name.send_keys(name) + add_button = driver.find_element(By.ID, "btn-add") + add_button.click() + + def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") @@ -71,10 +79,48 @@ def test_app_loads(driver, app_with_temp_board): def test_add_element(driver, app_with_temp_board): driver.get("http://localhost:5000") - add_name = driver.find_element(By.ID, "itemtitle") - add_name.send_keys("This is a hard task") - - add_button = driver.find_element(By.ID, "btn-add") - add_button.click() + add_task("This is a hard task", driver) assert "This is a hard task" in driver.page_source + + +def test_delete_element(driver, app_with_temp_board): + delete_me = "Please delete this task" + + driver.get("http://localhost:5000") + + add_task(delete_me, driver) + + assert delete_me in driver.page_source + + renamed_label = driver.find_element(By.NAME, delete_me) + parent: WebElement = renamed_label.find_element(By.XPATH, "./..") + delete_button = parent.find_element(By.NAME, "delete") + delete_button.click() + + assert delete_me not in driver.page_source + + +def test_delete_element_with_others_on_page(driver, app_with_temp_board): + dont_delete_me = "Please DO NOT delete this task" + delete_me = "Please delete this task" + dont_delete_me2 = "Also please DO NOT delete this task either" + + driver.get("http://localhost:5000") + + add_task(delete_me, driver) + add_task(dont_delete_me, driver) + add_task(dont_delete_me2, driver) + + assert delete_me in driver.page_source + assert dont_delete_me in driver.page_source + assert dont_delete_me2 in driver.page_source + + renamed_label = driver.find_element(By.NAME, delete_me) + parent: WebElement = renamed_label.find_element(By.XPATH, "./..") + delete_button = parent.find_element(By.NAME, "delete") + delete_button.click() + + assert delete_me not in driver.page_source + assert dont_delete_me in driver.page_source + assert dont_delete_me2 in driver.page_source diff --git a/todo_app/templates/item.html b/todo_app/templates/item.html index 6ceaf73..76a0aed 100644 --- a/todo_app/templates/item.html +++ b/todo_app/templates/item.html @@ -4,7 +4,7 @@ -

{{ item.title }}

+

{{ item.title }}

{{ item.title }} {% endif %} - + From 993170c9aaae8aebbd9fe638356f768d827d6562 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:14:30 +0100 Subject: [PATCH 09/15] add a test for checking if new task goes to correct list required adding an id to each list so it may be found --- selenium_tests/app_test.py | 14 ++++++++++++++ todo_app/templates/index.html | 4 ++-- todo_app/templates/item_list.html | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index b5f82e9..947f04d 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -124,3 +124,17 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): assert delete_me not in driver.page_source assert dont_delete_me in driver.page_source assert dont_delete_me2 in driver.page_source + + +def test_new_task_goes_to_incomplete(driver, app_with_temp_board): + this_is_a_new_incomplete_task = "This is a new incomplete task" + + driver.get("http://localhost:5000") + + add_task(this_is_a_new_incomplete_task, driver) + + incomplete_list = driver.find_element(By.ID, "incomplete") + assert incomplete_list.find_elements(By.NAME, this_is_a_new_incomplete_task) + + complete_list = driver.find_element(By.ID, "complete") + assert len(complete_list.find_elements(By.NAME, this_is_a_new_incomplete_task)) == 0 diff --git a/todo_app/templates/index.html b/todo_app/templates/index.html index f94acb1..0a6f6d1 100644 --- a/todo_app/templates/index.html +++ b/todo_app/templates/index.html @@ -12,9 +12,9 @@

To-Do App

Items

Incomplete

- {{ item_list(view_model.incomplete_items()) }} + {{ item_list(view_model.incomplete_items(), "incomplete") }}

Complete

- {{ item_list(view_model.complete_items()) }} + {{ item_list(view_model.complete_items(), "complete") }}

Add a new one

diff --git a/todo_app/templates/item_list.html b/todo_app/templates/item_list.html index 73120ab..83da944 100644 --- a/todo_app/templates/item_list.html +++ b/todo_app/templates/item_list.html @@ -1,7 +1,7 @@ {% from "item.html" import item as show_item %} -{% macro item_list(items) -%} -
    +{% macro item_list(items, id) -%} +
      {% for item in items %}
    • {{ show_item(item) }} From 6112f18464352601fd464bd10677abdb70a7eea3 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:18:32 +0100 Subject: [PATCH 10/15] move finding sibling button of task to a function --- selenium_tests/app_test.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 947f04d..176ed44 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -93,10 +93,8 @@ def test_delete_element(driver, app_with_temp_board): assert delete_me in driver.page_source - renamed_label = driver.find_element(By.NAME, delete_me) - parent: WebElement = renamed_label.find_element(By.XPATH, "./..") - delete_button = parent.find_element(By.NAME, "delete") - delete_button.click() + button = find_action_button(delete_me, driver, "delete") + button.click() assert delete_me not in driver.page_source @@ -116,10 +114,8 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): assert dont_delete_me in driver.page_source assert dont_delete_me2 in driver.page_source - renamed_label = driver.find_element(By.NAME, delete_me) - parent: WebElement = renamed_label.find_element(By.XPATH, "./..") - delete_button = parent.find_element(By.NAME, "delete") - delete_button.click() + button = find_action_button(delete_me, driver, "delete") + button.click() assert delete_me not in driver.page_source assert dont_delete_me in driver.page_source From e4ceeb1fa9a86ba3c5907dc7a200ba1845bcfcc6 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:35:50 +0100 Subject: [PATCH 11/15] add test for switching between tasks also move status checking to its own function --- selenium_tests/app_test.py | 35 ++++++++++++++++++++++++++++++----- todo_app/templates/item.html | 6 ++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 176ed44..ee53ba5 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -15,7 +15,7 @@ from todo_app.data import session_tasks from todo_app.data.env import get_default_params, get_organization_id, get_board_id -from todo_app.data.session_tasks import BOARD_URL, BOARDS_URL +from todo_app.data.session_tasks import BOARD_URL, BOARDS_URL, toggle def create_test_board(): @@ -70,6 +70,20 @@ def add_task(name, driver): add_button.click() +def find_action_button(name, driver, button_name): + label = driver.find_element(By.NAME, name) + parent: WebElement = label.find_element(By.XPATH, "./..") + button = parent.find_element(By.NAME, button_name) + return button + + +def task_is(status, name, driver): + wanted_list = driver.find_element(By.ID, status) + unwanted_list = driver.find_element(By.ID, toggle(status)) + assert wanted_list.find_elements(By.NAME, name) + assert not unwanted_list.find_elements(By.NAME, name) + + def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") @@ -129,8 +143,19 @@ def test_new_task_goes_to_incomplete(driver, app_with_temp_board): add_task(this_is_a_new_incomplete_task, driver) - incomplete_list = driver.find_element(By.ID, "incomplete") - assert incomplete_list.find_elements(By.NAME, this_is_a_new_incomplete_task) + task_is("incomplete", this_is_a_new_incomplete_task, driver) + + +def test_switching_between_tasks(driver, app_with_temp_board): + this_is_an_incomplete_task = "This is an incomplete task" + + driver.get("http://localhost:5000") + + add_task(this_is_an_incomplete_task, driver) + + task_is("incomplete", this_is_an_incomplete_task, driver) + + change_status_button = find_action_button(this_is_an_incomplete_task, driver, "changestatus") + change_status_button.click() - complete_list = driver.find_element(By.ID, "complete") - assert len(complete_list.find_elements(By.NAME, this_is_a_new_incomplete_task)) == 0 + task_is("complete", this_is_an_incomplete_task, driver) diff --git a/todo_app/templates/item.html b/todo_app/templates/item.html index 76a0aed..1fcdeb9 100644 --- a/todo_app/templates/item.html +++ b/todo_app/templates/item.html @@ -24,13 +24,15 @@

      {{ item.title }}

      + value="Mark as Completed" + name="changestatus"> {% endif %} {% if item.status == "complete" %} + value="Mark as Incomplete" + name="changestatus"> {% endif %} From 23081f3a25c94f2dd320d3bdbfadf042e9f699a9 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:46:13 +0100 Subject: [PATCH 12/15] add test for adding a task description --- selenium_tests/app_test.py | 26 ++++++++++++++++++++++++++ todo_app/templates/item.html | 4 ++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index ee53ba5..f704cdb 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -84,12 +84,14 @@ def task_is(status, name, driver): assert not unwanted_list.find_elements(By.NAME, name) +@pytest.mark.skip def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") assert driver.title == 'To-Do App' +@pytest.mark.skip def test_add_element(driver, app_with_temp_board): driver.get("http://localhost:5000") @@ -98,6 +100,7 @@ def test_add_element(driver, app_with_temp_board): assert "This is a hard task" in driver.page_source +@pytest.mark.skip def test_delete_element(driver, app_with_temp_board): delete_me = "Please delete this task" @@ -113,6 +116,7 @@ def test_delete_element(driver, app_with_temp_board): assert delete_me not in driver.page_source +@pytest.mark.skip def test_delete_element_with_others_on_page(driver, app_with_temp_board): dont_delete_me = "Please DO NOT delete this task" delete_me = "Please delete this task" @@ -136,6 +140,7 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): assert dont_delete_me2 in driver.page_source +@pytest.mark.skip def test_new_task_goes_to_incomplete(driver, app_with_temp_board): this_is_a_new_incomplete_task = "This is a new incomplete task" @@ -146,6 +151,7 @@ def test_new_task_goes_to_incomplete(driver, app_with_temp_board): task_is("incomplete", this_is_a_new_incomplete_task, driver) +@pytest.mark.skip def test_switching_between_tasks(driver, app_with_temp_board): this_is_an_incomplete_task = "This is an incomplete task" @@ -159,3 +165,23 @@ def test_switching_between_tasks(driver, app_with_temp_board): change_status_button.click() task_is("complete", this_is_an_incomplete_task, driver) + + +def test_adding_description(driver, app_with_temp_board): + rename_me = "Rename me" + new_description = "New description" + + driver.get("http://localhost:5000") + + add_task(rename_me, driver) + + edit_button = find_action_button(rename_me, driver, "edit") + edit_button.click() + + description_box = find_action_button(rename_me, driver, "description") + description_box.send_keys(new_description) + + submit_button = find_action_button(rename_me, driver, "submit") + submit_button.click() + + assert new_description in driver.page_source diff --git a/todo_app/templates/item.html b/todo_app/templates/item.html index 1fcdeb9..7743e97 100644 --- a/todo_app/templates/item.html +++ b/todo_app/templates/item.html @@ -34,8 +34,8 @@

      {{ item.title }}

      value="Mark as Incomplete" name="changestatus"> {% endif %} - - + +
From 1e89e0a9cba742e1e1bda6f24b7c058c6514b8c3 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:47:02 +0100 Subject: [PATCH 13/15] rename find_action_button to find_task_element as it can be used to find the description box too --- selenium_tests/app_test.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index f704cdb..0fe960e 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -70,7 +70,7 @@ def add_task(name, driver): add_button.click() -def find_action_button(name, driver, button_name): +def find_task_element(name, driver, button_name): label = driver.find_element(By.NAME, name) parent: WebElement = label.find_element(By.XPATH, "./..") button = parent.find_element(By.NAME, button_name) @@ -84,14 +84,12 @@ def task_is(status, name, driver): assert not unwanted_list.find_elements(By.NAME, name) -@pytest.mark.skip def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") assert driver.title == 'To-Do App' -@pytest.mark.skip def test_add_element(driver, app_with_temp_board): driver.get("http://localhost:5000") @@ -100,7 +98,6 @@ def test_add_element(driver, app_with_temp_board): assert "This is a hard task" in driver.page_source -@pytest.mark.skip def test_delete_element(driver, app_with_temp_board): delete_me = "Please delete this task" @@ -110,13 +107,12 @@ def test_delete_element(driver, app_with_temp_board): assert delete_me in driver.page_source - button = find_action_button(delete_me, driver, "delete") + button = find_task_element(delete_me, driver, "delete") button.click() assert delete_me not in driver.page_source -@pytest.mark.skip def test_delete_element_with_others_on_page(driver, app_with_temp_board): dont_delete_me = "Please DO NOT delete this task" delete_me = "Please delete this task" @@ -132,7 +128,7 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): assert dont_delete_me in driver.page_source assert dont_delete_me2 in driver.page_source - button = find_action_button(delete_me, driver, "delete") + button = find_task_element(delete_me, driver, "delete") button.click() assert delete_me not in driver.page_source @@ -140,7 +136,6 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): assert dont_delete_me2 in driver.page_source -@pytest.mark.skip def test_new_task_goes_to_incomplete(driver, app_with_temp_board): this_is_a_new_incomplete_task = "This is a new incomplete task" @@ -151,7 +146,6 @@ def test_new_task_goes_to_incomplete(driver, app_with_temp_board): task_is("incomplete", this_is_a_new_incomplete_task, driver) -@pytest.mark.skip def test_switching_between_tasks(driver, app_with_temp_board): this_is_an_incomplete_task = "This is an incomplete task" @@ -161,7 +155,7 @@ def test_switching_between_tasks(driver, app_with_temp_board): task_is("incomplete", this_is_an_incomplete_task, driver) - change_status_button = find_action_button(this_is_an_incomplete_task, driver, "changestatus") + change_status_button = find_task_element(this_is_an_incomplete_task, driver, "changestatus") change_status_button.click() task_is("complete", this_is_an_incomplete_task, driver) @@ -175,13 +169,13 @@ def test_adding_description(driver, app_with_temp_board): add_task(rename_me, driver) - edit_button = find_action_button(rename_me, driver, "edit") + edit_button = find_task_element(rename_me, driver, "edit") edit_button.click() - description_box = find_action_button(rename_me, driver, "description") + description_box = find_task_element(rename_me, driver, "description") description_box.send_keys(new_description) - submit_button = find_action_button(rename_me, driver, "submit") + submit_button = find_task_element(rename_me, driver, "submit") submit_button.click() assert new_description in driver.page_source From db509694fb9f38d77cdf0526356c84db64b26ad8 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:53:24 +0100 Subject: [PATCH 14/15] create functions to check for task existence --- selenium_tests/app_test.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 0fe960e..065d10b 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -84,6 +84,14 @@ def task_is(status, name, driver): assert not unwanted_list.find_elements(By.NAME, name) +def task_exists(name, driver): + assert driver.find_elements(By.NAME, name) + + +def task_not_exists(name, driver): + assert not driver.find_elements(By.NAME, name) + + def test_app_loads(driver, app_with_temp_board): driver.get("http://localhost:5000") @@ -95,7 +103,7 @@ def test_add_element(driver, app_with_temp_board): add_task("This is a hard task", driver) - assert "This is a hard task" in driver.page_source + task_exists("This is a hard task", driver) def test_delete_element(driver, app_with_temp_board): @@ -110,7 +118,7 @@ def test_delete_element(driver, app_with_temp_board): button = find_task_element(delete_me, driver, "delete") button.click() - assert delete_me not in driver.page_source + task_not_exists(delete_me, driver) def test_delete_element_with_others_on_page(driver, app_with_temp_board): @@ -120,20 +128,20 @@ def test_delete_element_with_others_on_page(driver, app_with_temp_board): driver.get("http://localhost:5000") - add_task(delete_me, driver) add_task(dont_delete_me, driver) + add_task(delete_me, driver) add_task(dont_delete_me2, driver) - assert delete_me in driver.page_source - assert dont_delete_me in driver.page_source - assert dont_delete_me2 in driver.page_source + task_exists(delete_me, driver) + task_exists(dont_delete_me, driver) + task_exists(dont_delete_me2, driver) button = find_task_element(delete_me, driver, "delete") button.click() - assert delete_me not in driver.page_source - assert dont_delete_me in driver.page_source - assert dont_delete_me2 in driver.page_source + task_not_exists(delete_me, driver) + task_exists(dont_delete_me, driver) + task_exists(dont_delete_me2, driver) def test_new_task_goes_to_incomplete(driver, app_with_temp_board): From 729222d734331cbf76ccdc6bc7f2d8970ab8f770 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Fri, 15 Jul 2022 11:56:11 +0100 Subject: [PATCH 15/15] add test for editing an existing description --- selenium_tests/app_test.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/selenium_tests/app_test.py b/selenium_tests/app_test.py index 065d10b..c56d51b 100644 --- a/selenium_tests/app_test.py +++ b/selenium_tests/app_test.py @@ -187,3 +187,34 @@ def test_adding_description(driver, app_with_temp_board): submit_button.click() assert new_description in driver.page_source + + +def test_editing_description(driver, app_with_temp_board): + please_change_my_description = "Please change my description" + temp_description = "Temporary description" + replacement_description = "Replacement description" + + driver.get("http://localhost:5000") + + add_task(please_change_my_description, driver) + + edit_button = find_task_element(please_change_my_description, driver, "edit") + edit_button.click() + + description_box = find_task_element(please_change_my_description, driver, "description") + description_box.send_keys(temp_description) + + submit_button = find_task_element(please_change_my_description, driver, "submit") + submit_button.click() + + edit_button = find_task_element(please_change_my_description, driver, "edit") + edit_button.click() + + description_box = find_task_element(please_change_my_description, driver, "description") + description_box.clear() + description_box.send_keys(replacement_description) + + submit_button = find_task_element(please_change_my_description, driver, "submit") + submit_button.click() + + assert replacement_description in driver.page_source