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 diff --git a/tests/ui/README.md b/tests/ui/README.md new file mode 100644 index 00000000..20ee05bc --- /dev/null +++ b/tests/ui/README.md @@ -0,0 +1,63 @@ +# 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, start Wok server and run the tests + +### Optional: install a virtual environment + +``` +$ python3 -m venv .env +$ source .env/bin/activate +``` + +### Install deps +``` +$ 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 +``` + +### Parameters for run +The script expect some environment variables to run wok 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 +BROWSER: browser to run default: CHROME possible: [CHROME, FIREFOX] +``` + +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..cd14e1fb --- /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 WokLoginPage(): + """ + Page object to Login + + Expect environment variables: + USERNAME: username for the host + PASSWORD: password for the host + HOST: host for Wok + PORT: port for Wok + """ + + 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 wok server 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..0199bf6b --- /dev/null +++ b/tests/ui/requirements.txt @@ -0,0 +1,29 @@ +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 +pygeckodriver==0.26.0 +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..d744a346 --- /dev/null +++ b/tests/ui/run_tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +HOST=${HOST:-localhost} +PORT=${PORT:-8001} +USERNAME=${USERNAME:-root} +BROWSER=${BROWSER:-CHROME} + +# ask for password if not passed +if [ -z $PASSWORD ]; then + echo "Type password for host ${USERNAME}@${HOST}" + read -s PASSWORD +fi + +echo "Running on browser ${BROWSER}" +HOST=${HOST} PASSWORD=${PASSWORD} USERNAME=${USERNAME} PORT=${PORT} BROWSER=${BROWSER} python3 -m pytest diff --git a/tests/ui/test_login.py b/tests/ui/test_login.py new file mode 100644 index 00000000..6ab6a335 --- /dev/null +++ b/tests/ui/test_login.py @@ -0,0 +1,13 @@ +import pytest + +from pages.login import WokLoginPage +from utils import getBrowser + +@pytest.fixture +def browser(): + browser = getBrowser() + yield browser + browser.quit() + +def test_login(browser): + assert WokLoginPage(browser).login(), "Cannot login to Wok" diff --git a/tests/ui/utils.py b/tests/ui/utils.py new file mode 100644 index 00000000..a04021ff --- /dev/null +++ b/tests/ui/utils.py @@ -0,0 +1,123 @@ +from selenium import webdriver +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 logging as log +import os + +logging = log.getLogger(__name__) + +WAIT = 10 + +def getBrowser(headless=True): + # 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: browser not found + try: + driver = browser(options=options, executable_path=path) + except Exception as e: + logging.info(f"Browser or driver not found: {e}") + + 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 +