From 00b5a6db40078a77dd2b94f37154757d9209c61d Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Sat, 18 Jan 2020 00:16:44 +0100 Subject: [PATCH 1/6] Add E2E Tests for Wok --- tests/ui/README.md | 52 +++++++++++++++++++ tests/ui/pages/login.py | 72 ++++++++++++++++++++++++++ tests/ui/pytest.ini | 4 ++ tests/ui/requirements.txt | 28 ++++++++++ tests/ui/run_tests.sh | 13 +++++ tests/ui/test_login.py | 15 ++++++ tests/ui/utils.py | 105 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 289 insertions(+) create mode 100644 tests/ui/README.md create mode 100644 tests/ui/pages/login.py create mode 100644 tests/ui/pytest.ini create mode 100644 tests/ui/requirements.txt create mode 100755 tests/ui/run_tests.sh create mode 100644 tests/ui/test_login.py create mode 100644 tests/ui/utils.py diff --git a/tests/ui/README.md b/tests/ui/README.md new file mode 100644 index 00000000..e1fe9f4a --- /dev/null +++ b/tests/ui/README.md @@ -0,0 +1,52 @@ +# Wok E2E Tests + +The tests are located in `tests/ui`. You should go to the directory to start them +``` +$ cd tests/ui +``` + +## How to run + +First you need to install all dependencies to run the tests + +### Optional: install a virtual environment + +``` +$ python3 -m venv .env +$ source .env/bin/activate +``` + +### Install deps +``` +$ pip install -r requirements.txt +``` + +### Run in headless mode +The script expect some environment variables to run kimchi-project tests, which are: + +``` +Expect environment variables: +USERNAME: username for the host default: root +PASSWORD: password for the host +HOST: host for wok default: localhost +PORT: port for wok default: 8001 +``` + +So, if you are running against a remote host: + +``` +$ HOST= ./run_tests.sh +Type password for host USER@HOST + +``` + +### Run in debug mode +If you use the command above, the browser will no be visible for you. + +To see the browser action, add the variable `DEBUG` + +``` +$ HOST= DEBUG=true ./run_tests.sh +Type password for host USER@HOST + +``` diff --git a/tests/ui/pages/login.py b/tests/ui/pages/login.py new file mode 100644 index 00000000..8c23a53f --- /dev/null +++ b/tests/ui/pages/login.py @@ -0,0 +1,72 @@ +import logging as log +import os +import utils +import pytest +from selenium.common.exceptions import TimeoutException + +logging = log.getLogger(__name__) + +# locators by ID +USERNAME = "username" +PASSWORD = "password" +LOGIN_BUTTON = "btn-login" +LOGIN_BAR = "user-login" + +# environment variables +ENV_USER = "USERNAME" +ENV_PASS = "PASSWORD" +ENV_PORT = "PORT" +ENV_HOST = "HOST" + + +class KimchiLoginPage(): + """ + Page object to Login + + Expect environment variables: + KIMCHI_USERNAME: username for the host + KIMCHI_PASSWORD: password for the host + KIMCHI_HOST: host for kimchi + KIMCHI_PORT: port for kimchi + """ + + def __init__(self, browser): + self.browser = browser + + # assert envs + for var in [ENV_USER, ENV_PASS, ENV_PORT, ENV_HOST]: + assert var in os.environ, f"{var} is a required environment var" + + # get values + self.host = os.environ.get(ENV_HOST) + self.port = os.environ.get(ENV_PORT) + self.user = os.environ.get(ENV_USER) + self.password = os.environ.get(ENV_PASS) + + def login(self): + try: + url = f"https://{self.host}:{self.port}/login.html" + self.browser.get(url) + except TimeoutException as e: + logging.error(f"Cannot reach kimchi at {url}") + return False + + # fill user and password + logging.info(f"Loging in {url}") + utils.fillTextIfElementIsVisibleById(self.browser, + USERNAME, + self.user) + utils.fillTextIfElementIsVisibleById(self.browser, + PASSWORD, + self.password) + + # press login + utils.clickIfElementIsVisibleById(self.browser, LOGIN_BUTTON) + + # login bar not found: return error + if utils.waitElementIsVisibleById(self.browser, LOGIN_BAR) == False: + logging.error(f"Invalid credentials") + return False + + logging.info(f"Logged in {url}") + return True diff --git a/tests/ui/pytest.ini b/tests/ui/pytest.ini new file mode 100644 index 00000000..1ab51d69 --- /dev/null +++ b/tests/ui/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +verbose = True +log_cli = True +log_cli_level = INFO diff --git a/tests/ui/requirements.txt b/tests/ui/requirements.txt new file mode 100644 index 00000000..8fac610b --- /dev/null +++ b/tests/ui/requirements.txt @@ -0,0 +1,28 @@ +appnope==0.1.0 +attrs==19.3.0 +backcall==0.1.0 +chromedriver-binary==78.0.3904.105.0 +decorator==4.4.1 +importlib-metadata==1.3.0 +ipdb==0.12.3 +ipython==7.11.0 +ipython-genutils==0.2.0 +jedi==0.15.2 +more-itertools==8.0.2 +packaging==19.2 +parso==0.5.2 +pexpect==4.7.0 +pickleshare==0.7.5 +pluggy==0.13.1 +prompt-toolkit==3.0.2 +ptyprocess==0.6.0 +py==1.8.1 +Pygments==2.5.2 +pyparsing==2.4.6 +pytest==5.3.2 +selenium==3.141.0 +six==1.13.0 +traitlets==4.3.3 +urllib3==1.25.7 +wcwidth==0.1.7 +zipp==0.6.0 diff --git a/tests/ui/run_tests.sh b/tests/ui/run_tests.sh new file mode 100755 index 00000000..776e94db --- /dev/null +++ b/tests/ui/run_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +HOST=${HOST:-localhost} +PORT=${PORT:-8001} +USERNAME=${USERNAME:-root} + +# ask for password if not passed +if [ -z $PASSWORD ]; then + echo "Type password for host ${USERNAME}@${HOST}" + read -s PASSWORD +fi + +HOST=${HOST} PASSWORD=${PASSWORD} USERNAME=${USERNAME} PORT=${PORT} python3 -m pytest diff --git a/tests/ui/test_login.py b/tests/ui/test_login.py new file mode 100644 index 00000000..95d6c947 --- /dev/null +++ b/tests/ui/test_login.py @@ -0,0 +1,15 @@ +import utils +from pages.login import KimchiLoginPage + +import logging as log + +class TestWokLogin(): + + def setup(self): + self.browser = utils.getBrowser() + + def test_login(self): + assert KimchiLoginPage(self.browser).login(), "Cannot login to Kimchi" + + def tearDown(self): + self.browser.close() diff --git a/tests/ui/utils.py b/tests/ui/utils.py new file mode 100644 index 00000000..15b643df --- /dev/null +++ b/tests/ui/utils.py @@ -0,0 +1,105 @@ +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support.wait import TimeoutException +from selenium.webdriver.support import expected_conditions as EC + +import chromedriver_binary +import logging as log +import os + +logging = log.getLogger(__name__) + +WAIT = 10 + +def getBrowser(headless=True): + if os.environ.get("DEBUG") is not None: + logging.info("Headless mode deactivated") + headless = False + + options = Options() + if headless is True: + options.add_argument('--headless') + options.add_argument('--no-sandbox') + options.add_argument('--disable-gpu') + + driver = webdriver.Chrome(options=options) + driver.set_page_load_timeout(WAIT * 2) + return driver + +def waitElementByCondition(browser, condition, searchMethod, searchString, errorMessage, time=WAIT): + try: + element = WebDriverWait(browser, time).until( + condition((searchMethod, searchString)) + ) + except TimeoutException as e: + logging.error(f"Element {searchString} {errorMessage}") + return False + return True + + +def waitElementIsVisibleById(browser, elementId, time=WAIT): + return waitElementByCondition(browser, + EC.visibility_of_element_located, + By.ID, + elementId, + "is not visibile", + time) + +def waitElementIsVisibleByXpath(browser, xpath): + return waitElementByCondition(browser, + EC.visibility_of_element_located, + By.XPATH, + xpath, + "is not visibile") + +def waitElementIsClickableById(browser, elementId): + return waitElementByCondition(browser, + EC.element_to_be_clickable, + By.ID, + elementId, + "is not clickable") + +def waitElementIsClickableByXpath(browser, xpath): + return waitElementByCondition(browser, + EC.element_to_be_clickable, + By.XPATH, + xpath, + "is not clickable") + +def clickIfElementIsVisibleByXpath(browser, xpath): + try: + assert(waitElementIsVisibleByXpath(browser, xpath)) + assert(waitElementIsClickableByXpath(browser, xpath)) + browser.find_element_by_xpath(xpath).click() + + except Exception as e: + logging.error(f"Cannot click on element {xpath}: {e}") + return False + + return True + +def clickIfElementIsVisibleById(browser, elementId): + try: + assert(waitElementIsVisibleById(browser, elementId)) + assert(waitElementIsClickableById(browser, elementId)) + browser.find_element_by_id(elementId).click() + + except Exception as e: + logging.error(f"Cannot click on element {elementId}: {e}") + return False + + return True + +def fillTextIfElementIsVisibleById(browser, elementId, text): + try: + assert(waitElementIsVisibleById(browser, elementId)) + browser.find_element_by_id(elementId).send_keys(text) + + except Exception as e: + logging.error(f"Cannot type {text} on element {elementId}: {e}") + return False + + return True + From 3654b8e6b051f9f5a2734850bcf8fdb2f2d50f59 Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Sun, 19 Jan 2020 15:18:10 +0100 Subject: [PATCH 2/6] Only run tests locally --- tests/run_tests.sh.in | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/run_tests.sh.in b/tests/run_tests.sh.in index 336bfff7..92326be4 100644 --- a/tests/run_tests.sh.in +++ b/tests/run_tests.sh.in @@ -22,18 +22,14 @@ HAVE_UNITTEST=@HAVE_PYMOD_UNITTEST@ PYTHON_VER=@PYTHON_VERSION@ if [ "$1" = "-v" ]; then - OPTS="-v" + # p ./test*.py means the pattern for + # discover is local file starting with test + OPTS="-v -p ./test*.py" shift else OPTS="" fi -if [ $# -ne 0 ]; then - ARGS="$@" -else - ARGS=`find -name "test_*.py" | xargs -I @ basename @ .py` -fi - CMD="python3 -m unittest" -PYTHONPATH=../src:../ $CMD $OPTS $ARGS +PYTHONPATH=../src:../ $CMD $OPTS From 180610217db3020ceecbc3872fa221c35ee99377 Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Mon, 20 Jan 2020 10:40:44 +0100 Subject: [PATCH 3/6] Move to pytest fixture --- tests/ui/test_login.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/ui/test_login.py b/tests/ui/test_login.py index 95d6c947..1b029447 100644 --- a/tests/ui/test_login.py +++ b/tests/ui/test_login.py @@ -1,15 +1,13 @@ -import utils -from pages.login import KimchiLoginPage - -import logging as log +import pytest -class TestWokLogin(): - - def setup(self): - self.browser = utils.getBrowser() +from pages.login import KimchiLoginPage +from utils import getBrowser - def test_login(self): - assert KimchiLoginPage(self.browser).login(), "Cannot login to Kimchi" +@pytest.fixture +def browser(): + browser = getBrowser() + yield browser + browser.quit() - def tearDown(self): - self.browser.close() +def test_login(browser): + assert KimchiLoginPage(browser).login(), "Cannot login to Kimchi" From 78379d61079bdcf2854f155011aff13570778d39 Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Sun, 26 Jan 2020 13:43:53 +0100 Subject: [PATCH 4/6] Remove kimchi references at uiTests --- tests/ui/README.md | 2 +- tests/ui/pages/login.py | 12 ++++++------ tests/ui/test_login.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ui/README.md b/tests/ui/README.md index e1fe9f4a..0947b155 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -22,7 +22,7 @@ $ pip install -r requirements.txt ``` ### Run in headless mode -The script expect some environment variables to run kimchi-project tests, which are: +The script expect some environment variables to run wok tests, which are: ``` Expect environment variables: diff --git a/tests/ui/pages/login.py b/tests/ui/pages/login.py index 8c23a53f..cd14e1fb 100644 --- a/tests/ui/pages/login.py +++ b/tests/ui/pages/login.py @@ -19,15 +19,15 @@ ENV_HOST = "HOST" -class KimchiLoginPage(): +class WokLoginPage(): """ Page object to Login Expect environment variables: - KIMCHI_USERNAME: username for the host - KIMCHI_PASSWORD: password for the host - KIMCHI_HOST: host for kimchi - KIMCHI_PORT: port for kimchi + USERNAME: username for the host + PASSWORD: password for the host + HOST: host for Wok + PORT: port for Wok """ def __init__(self, browser): @@ -48,7 +48,7 @@ def login(self): url = f"https://{self.host}:{self.port}/login.html" self.browser.get(url) except TimeoutException as e: - logging.error(f"Cannot reach kimchi at {url}") + logging.error(f"Cannot reach wok server at {url}") return False # fill user and password diff --git a/tests/ui/test_login.py b/tests/ui/test_login.py index 1b029447..6ab6a335 100644 --- a/tests/ui/test_login.py +++ b/tests/ui/test_login.py @@ -1,6 +1,6 @@ import pytest -from pages.login import KimchiLoginPage +from pages.login import WokLoginPage from utils import getBrowser @pytest.fixture @@ -10,4 +10,4 @@ def browser(): browser.quit() def test_login(browser): - assert KimchiLoginPage(browser).login(), "Cannot login to Kimchi" + assert WokLoginPage(browser).login(), "Cannot login to Wok" From 2e12487a56360b2aca68d12692cb18cb498042d8 Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Sun, 26 Jan 2020 23:46:36 +0100 Subject: [PATCH 5/6] Improve logging and info about Google Chrome dep --- tests/ui/README.md | 12 +++++++++++- tests/ui/utils.py | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/ui/README.md b/tests/ui/README.md index 0947b155..a4918efe 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -7,7 +7,7 @@ $ cd tests/ui ## How to run -First you need to install all dependencies to run the tests +First you need to install all dependencies, start Wok server and run the tests ### Optional: install a virtual environment @@ -21,6 +21,16 @@ $ source .env/bin/activate $ pip install -r requirements.txt ``` +### Install Browser + +This tests expect Google Chrome installed. Visit https://www.google.com/chrome/ for info + +### Start wok server + +``` +$ python src/wokd +``` + ### Run in headless mode The script expect some environment variables to run wok tests, which are: diff --git a/tests/ui/utils.py b/tests/ui/utils.py index 15b643df..f3710955 100644 --- a/tests/ui/utils.py +++ b/tests/ui/utils.py @@ -24,7 +24,12 @@ def getBrowser(headless=True): options.add_argument('--no-sandbox') options.add_argument('--disable-gpu') - driver = webdriver.Chrome(options=options) + # error: googlechrome not found + try: + driver = webdriver.Chrome(options=options) + except Exception as e: + logging.info(f"Google Chrome not found: {e}") + driver.set_page_load_timeout(WAIT * 2) return driver From 95a118e589a7114b1521193201b44d58fb0b4230 Mon Sep 17 00:00:00 2001 From: Ramon Medeiros Date: Fri, 31 Jan 2020 00:37:14 +0100 Subject: [PATCH 6/6] Support Firefox --- tests/ui/README.md | 3 ++- tests/ui/requirements.txt | 1 + tests/ui/run_tests.sh | 4 +++- tests/ui/utils.py | 37 +++++++++++++++++++++++++------------ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/ui/README.md b/tests/ui/README.md index a4918efe..20ee05bc 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -31,7 +31,7 @@ This tests expect Google Chrome installed. Visit https://www.google.com/chrome/ $ python src/wokd ``` -### Run in headless mode +### Parameters for run The script expect some environment variables to run wok tests, which are: ``` @@ -40,6 +40,7 @@ USERNAME: username for the host default: root PASSWORD: password for the host HOST: host for wok default: localhost PORT: port for wok default: 8001 +BROWSER: browser to run default: CHROME possible: [CHROME, FIREFOX] ``` So, if you are running against a remote host: diff --git a/tests/ui/requirements.txt b/tests/ui/requirements.txt index 8fac610b..0199bf6b 100644 --- a/tests/ui/requirements.txt +++ b/tests/ui/requirements.txt @@ -17,6 +17,7 @@ pluggy==0.13.1 prompt-toolkit==3.0.2 ptyprocess==0.6.0 py==1.8.1 +pygeckodriver==0.26.0 Pygments==2.5.2 pyparsing==2.4.6 pytest==5.3.2 diff --git a/tests/ui/run_tests.sh b/tests/ui/run_tests.sh index 776e94db..d744a346 100755 --- a/tests/ui/run_tests.sh +++ b/tests/ui/run_tests.sh @@ -3,6 +3,7 @@ HOST=${HOST:-localhost} PORT=${PORT:-8001} USERNAME=${USERNAME:-root} +BROWSER=${BROWSER:-CHROME} # ask for password if not passed if [ -z $PASSWORD ]; then @@ -10,4 +11,5 @@ if [ -z $PASSWORD ]; then read -s PASSWORD fi -HOST=${HOST} PASSWORD=${PASSWORD} USERNAME=${USERNAME} PORT=${PORT} python3 -m pytest +echo "Running on browser ${BROWSER}" +HOST=${HOST} PASSWORD=${PASSWORD} USERNAME=${USERNAME} PORT=${PORT} BROWSER=${BROWSER} python3 -m pytest diff --git a/tests/ui/utils.py b/tests/ui/utils.py index f3710955..a04021ff 100644 --- a/tests/ui/utils.py +++ b/tests/ui/utils.py @@ -1,11 +1,13 @@ from selenium import webdriver -from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.firefox.options import Options as FirefoxOptions from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.wait import TimeoutException from selenium.webdriver.support import expected_conditions as EC +from pygeckodriver import geckodriver_path +from chromedriver_binary import chromedriver_filename -import chromedriver_binary import logging as log import os @@ -14,21 +16,32 @@ WAIT = 10 def getBrowser(headless=True): - if os.environ.get("DEBUG") is not None: - logging.info("Headless mode deactivated") - headless = False - - options = Options() - if headless is True: - options.add_argument('--headless') + # chrome: set browser class and options + if os.environ.get("BROWSER").upper() == "CHROME": + options = ChromeOptions() options.add_argument('--no-sandbox') options.add_argument('--disable-gpu') + browser = webdriver.Chrome + path = chromedriver_filename + + # firefox: set browser class and options + elif os.environ.get("BROWSER").upper() == "FIREFOX": + options = FirefoxOptions() + browser = webdriver.Firefox + path = geckodriver_path + + # headless + if "DEBUG" not in os.environ: + options.add_argument('--headless') + #options.headless = True + else: + logging.info("Headless mode deactivated") - # error: googlechrome not found + # error: browser not found try: - driver = webdriver.Chrome(options=options) + driver = browser(options=options, executable_path=path) except Exception as e: - logging.info(f"Google Chrome not found: {e}") + logging.info(f"Browser or driver not found: {e}") driver.set_page_load_timeout(WAIT * 2) return driver