Commit 5cafac00 authored by Marc Egger's avatar Marc Egger
Browse files
parent b3b2d9b3
Pipeline #2837 failed with stages
in 3 minutes and 17 seconds
......@@ -53,6 +53,7 @@ Assumes you have run qfq in docker as explained above.
Assumes you have run qfq in docker as explained above.
0. Install selenium for python: ```python3 -m pip install --user selenium```
1. again from the docker directory run ```./run_selenium_tests_local.sh```
## Run a single selenium test file on local machine
......
This diff is collapsed.
This diff is collapsed.
......@@ -73,7 +73,7 @@ until mysql -u root --password=${MYSQL_ROOT_PASSWORD} --port=${DB_PORT} -h 127.0
-e "CREATE DATABASE ${T3_DATABASE}; CREATE DATABASE ${QFQ_DATABASE}"
do
tries=$((tries+1))
if [[ "${tries}" -gt 30 ]]; then
if [[ "${tries}" -gt 60 ]]; then
echo "Timeout: could not connect to database."
exit 1;
fi
......
#!/bin/bash -ex
#!/bin/bash
source run_qfq_docker.output
# download chromedriver
if [ ! -f chromedriver ]; then
wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`/chromedriver_linux64.zip
unzip /tmp/chromedriver.zip chromedriver
chmod +x chromedriver
# this function prints a separator
function print_separator {
# prints the separator
echo -e "----------------------------------------------------------------------"
}
# prints the starting separator
print_separator
# checks that a file named geckodriver doesn't already exist
if [ ! -f "geckodriver" ]; then
# stores the current version of the driver
gecko_version=$(curl --silent "https://api.github.com/repos/mozilla/geckodriver/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")') &> /dev/null
# downloads the geckodriver from github
wget -O /tmp/geckodriver.tar.gz https://github.com/mozilla/geckodriver/releases/download/${gecko_version}/geckodriver-${gecko_version}-linux64.tar.gz &> /dev/null
# unzips the downloaded geckodriver
tar xzf /tmp/geckodriver.tar.gz geckodriver &> /dev/null
# makes the geckodriver executable
chmod +x geckodriver &> /dev/null
# prints a success output
echo -e "Successfully downloaded geckodriver"
fi
# checks that a file named chromedriver doesn't already exist
if [ ! -f "chromedriver" ]; then
# downloads the newest version of the chromedriver from google
wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`/chromedriver_linux64.zip &> /dev/null
# unzips the downloaded chromedriver
unzip /tmp/chromedriver.zip chromedriver &> /dev/null
# makes the chromedriver executable
chmod +x chromedriver &> /dev/null
# prints a success output
echo -e "Successfully downloaded chromedriver"
fi
# reads the url to test from the input variable
SELENIUM_URL=$1
# checks if the selenium url is not given
if [ -z $SELENIUM_URL ]; then
SELENIUM_URL="http://127.0.0.1:${T3_PORT}"
fi
# defines the url to be used during testing
export SELENIUM_URL=$SELENIUM_URL
# stores the default engine
DEFAULT_ENGINE="chrome"
# reads the engine from the 2nd input variable
ENGINE=$2
# checks if an engine is not specified
if [ -z $ENGINE ]; then
# defines the default engine to use during tests
export BROWSER=$DEFAULT_ENGINE
# defines the path to the drivers of the engine
export DRIVER_PATH="${PWD}/${DEFAULT_ENGINE}driver"
else
# defines the engine to use during tests
export BROWSER=$ENGINE
# defines the path to the drivers of the engine
export DRIVER_PATH="${PWD}/${ENGINE}driver"
fi
# stores the default headless option
DEFAULT_HEADLESS="no"
# reads the headless option from the 3rd input variable
HEADLESS=$3
# checks if the headless parameter is not specified
if [ -z $HEADLESS ]; then
# defines if the browser gui should open
export SELENIUM_HEADLESS=$DEFAULT_HEADLESS
else
# defines if the browser gui should open
export SELENIUM_HEADLESS=$HEADLESS
fi
# stores the default slowdown
DEFAULT_SLOWDOWN="0"
# reads the slowdown from the 4th input variable
SLOWDOWN=$4
# checks if the slowdown parameter is not specified
if [ -z $SLOWDOWN ]; then
# defines what slowdown should be applied
export SELENIUM_SLOWDOWN=$DEFAULT_SLOWDOWN
else
# defines what slowdown should be applied
export SELENIUM_SLOWDOWN=$SLOWDOWN
fi
export CHROMEDRIVER_PATH="${PWD}/chromedriver"
# run tests
cd ../extension/Tests/selenium
export SELENIUM_URL="http://127.0.0.1:${T3_PORT}"
export SELENIUM_HEADLESS="no"
python -m unittest discover
\ No newline at end of file
# prints running tests
echo -e -n "Running tests: "
# runs the python tests
python3 -W ignore -m unittest discover
# checks if the tests were successful
if [ $? -eq 0 ]; then
print_separator
# prints a success message
echo -e "Successfully tested ${SELENIUM_URL}"
else
print_separator
# prints a success message
echo -e "Failed while testing ${SELENIUM_URL}"
fi
# prints the trailing separator
print_separator
# exits the program
exit 0
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.firefox.options import Options as FFOptions
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException, WebDriverException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import unittest
import os
import sys
import glob
import time
import codecs
import re
import socket
import random
import string
def urlJoin(url_list):
return '/'.join(s.strip('/') for s in url_list)
def urlFromHostname(hostname, port=80):
try:
return 'http://' + socket.gethostbyname(hostname) + ':' + str(port)
except socket.gaierror as e:
return 'unknown'
"""
this function returns a url from a given hostname and a given port
"""
try:
return 'http://' + socket.gethostbyname(hostname) + ':' + str(port)
except socket.gaierror as e:
return 'unknown'
class QfqSeleniumTestCase(unittest.TestCase):
"""initialize selenium and add custom commands and asserts"""
"""
this class initializes selenium and adds custom commands and asserts.
"""
max_number_of_log_files_to_keep = 100
gecko_browser_name = "gecko"
gecko_driver_path = "geckodriver"
chrome_browser_name = "chrome"
chrome_driver_path = "chromedriver"
# These variables can be overwritten by environment variables of the same name
CHROMEDRIVER_PATH = "chromedriver"
# these variables can be overwritten by environment variables of the same name
BROWSER = chrome_browser_name
DRIVER_PATH = ""
if BROWSER == chrome_browser_name:
DRIVER_PATH = chrome_driver_path
elif BROWSER == gecko_browser_name:
DRIVER_PATH = gecko_driver_path
SELENIUM_LOGS_PATH = os.getcwd()
SELENIUM_HEADLESS = 'yes' # set environment variable to 'no' to turn off
SELENIUM_HEADLESS = 'yes' # set environment variable to 'no' to turn off
SELENIUM_URL = urlFromHostname('typo3container')
SELENIUM_SLOWDOWN = 0
@classmethod
def setUpClass(cls):
"""executed by unittest once before any tests are executed"""
"""
executed by unittest once before any tests are executed.
"""
# read environment variables
cls.CHROMEDRIVER_PATH = os.environ.get('CHROMEDRIVER_PATH', cls.CHROMEDRIVER_PATH)
# reads the environment variables
cls.BROWSER = os.environ.get('BROWSER', cls.BROWSER)
cls.DRIVER_PATH = os.environ.get('DRIVER_PATH', cls.DRIVER_PATH)
cls.SELENIUM_LOGS_PATH = os.environ.get('SELENIUM_LOGS_PATH', cls.SELENIUM_LOGS_PATH)
cls.SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', cls.SELENIUM_HEADLESS)
cls.SELENIUM_URL = os.environ.get('SELENIUM_URL', cls.SELENIUM_URL)
cls.SELENIUM_SLOWDOWN = float(os.environ.get('SELENIUM_SLOWDOWN', cls.SELENIUM_SLOWDOWN))
# setup log directory, delete very old log files
cls.selenium_logs_dir = 'selenium_logs'
......@@ -52,42 +79,58 @@ class QfqSeleniumTestCase(unittest.TestCase):
log_file_pattern = re.compile('[0-9]{8}-{1}[0-9]{6}.*')
for filename in log_files_to_delete:
if not log_file_pattern.match(filename):
print 'non log file in log directory found: ', filename
print('non log file in log directory found: ', filename)
continue
file_path = os.path.join(cls.selenium_logs_dir_path, filename)
os.remove(file_path)
# initialize webdriver
chrome_options = Options()
if cls.SELENIUM_HEADLESS != 'no':
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
desired_capabilities = {'UNEXPECTED_ALERT_BEHAVIOUR': 'ignore'}
cls.driver = webdriver.Chrome(chrome_options=chrome_options,
executable_path=cls.CHROMEDRIVER_PATH,
desired_capabilities=desired_capabilities)
# initializes webdriver
if cls.BROWSER == cls.chrome_browser_name:
chrome_options = Options()
if cls.SELENIUM_HEADLESS != 'no':
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
desired_capabilities = {'UNEXPECTED_ALERT_BEHAVIOUR': 'ignore'}
cls.driver = webdriver.Chrome(chrome_options=chrome_options,
executable_path=cls.DRIVER_PATH,
desired_capabilities=desired_capabilities)
elif cls.BROWSER == cls.gecko_browser_name:
firefox_options = FFOptions()
if cls.SELENIUM_HEADLESS != 'no':
firefox_options.headless = True
desired_capabilities = webdriver.DesiredCapabilities().FIREFOX
cls.driver = webdriver.Firefox(firefox_options=firefox_options,
executable_path=cls.DRIVER_PATH,
capabilities=desired_capabilities)
cls.driver.set_window_size(1920, 1080)
cls.driver.implicitly_wait(30)
@classmethod
def tearDownClass(cls):
"""executed by unittest after all test cases were executed"""
"""
executed by unittest after all test cases were executed
"""
cls.driver.quit()
def tearDown(self):
"""executed by unittest after every single test case"""
"""
executed by unittest after every single test case
"""
# save website state on failure
if sys.exc_info()[0]:
filename = time.strftime("%Y%m%d-%H%M%S") + '_' + self._testMethodName
screenshot_file_path = self.qfq_save_screenshot(filename)
html_file_path = self.qfq_save_html(filename)
print 'Test failed.'
print 'Webpage screenshot saved to ' + screenshot_file_path
print 'Webpage Html saved to ' + html_file_path
print '!!! ATTENTION !!!: If you get the error "unexpected alert open",' \
+ 'there must be another error above which actually triggers the test failure.'
print('Test failed.')
print('Webpage screenshot saved to ' + screenshot_file_path)
print('Webpage Html saved to ' + html_file_path)
print('!!! ATTENTION !!!: If you get the error "unexpected alert open",' \
+ 'there must be another error above which actually triggers the test failure.')
def _prepare_log_file_path(self, filename, suffix=''):
file_path = os.path.join(self.selenium_logs_dir_path, filename + suffix)
......@@ -101,49 +144,300 @@ class QfqSeleniumTestCase(unittest.TestCase):
except (StaleElementReferenceException, NoSuchElementException, WebDriverException):
retries -= 1
def _slow_available(func):
"""
adds a globally specified slowdown to a function
"""
def f(*args, **kwargs):
self = args[0]
self.qfq_wait(self.SELENIUM_SLOWDOWN)
func(*args, **kwargs)
return f
# ----- DATA SAVING ----- #
def qfq_save_screenshot(self, filename):
"""
saves a screenshot of the page to a logfile with a given name
"""
screenshot_file_path = self._prepare_log_file_path(filename, '.png')
self.driver.save_screenshot(screenshot_file_path)
return screenshot_file_path
def qfq_save_html(self, filename):
"""
saves the html source to a logfile with a given name
"""
html_file_path = self._prepare_log_file_path(filename, '.html')
with codecs.open(html_file_path, "w", "utf-8") as f:
f.write(self.driver.page_source)
return html_file_path
def qfq_goto(self, web_path):
url = urlJoin([self.SELENIUM_URL, web_path])
self.driver.get(url)
def qfq_wait(self, seconds):
time.sleep(seconds)
# ----- ASSERTIONS ----- #
def qfq_assert_text_exists(self, text):
self.assertTrue(text in self.driver.page_source)
"""
asserts if a given text exists on the webpage
"""
self.assertTrue(str(text) in self.driver.page_source)
def qfq_assert_text_absent(self, text):
self.assertTrue(text not in self.driver.page_source)
"""
asserts if a given text is absent on the webpage
"""
self.assertTrue(str(text) not in self.driver.page_source)
def qfq_assert_true(self, bool):
"""
asserts if a given bool is true
"""
self.assertTrue(bool)
def qfq_assert_false(self, bool):
"""
asserts if a given bool is false
"""
self.assertFalse(bool)
# ----- SELECTORS ----- #
def qfq_get_element_by_css_selector(self, selector, select_invisibles=True):
"""
this funtion returns the first element to match a given css selector
"""
element = WebDriverWait(self.driver, 1).until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
if self.qfq_element_is_visible(element) or select_invisibles:
return element
else:
raise ValueError("The element with the given selector is not visible on screen")
def qfq_find_by_data_ref(self, data_reference):
return self.driver.find_element_by_css_selector("[data-reference='{}']".format(data_reference))
def qfq_get_element_by_id(self, id, select_invisibles=True):
"""
returns an element which is identified by a given id
"""
id_occurences = self.driver.page_source.count(" id=\"" + id + "\"")
if id_occurences == 0 or id_occurences > 1:
raise ValueError(
"No element could be identified with the given id. The page source contains more than one or no element with the given id."
)
return self.qfq_get_element_by_css_selector(
"#" + str(id),
select_invisibles
)
def qfq_get_element_by_data_ref(self, data_reference, select_invisibles=True):
"""
returns an element which is identified by a given data reference.
If there is no such data reference it throws an exception.
"""
return self.qfq_get_element_by_css_selector(
"[data-reference='" + data_reference + "']",
select_invisibles
)
def qfq_get_element_by_xpath(self, xpath, select_invisibles=True):
"""
this function returns an element identified by a given xpath
"""
element = WebDriverWait(self.driver, 1).until(EC.presence_of_element_located((By.XPATH, xpath)))
if self.qfq_element_is_visible(element) or select_invisibles:
return element
else:
raise ValueError("The element with the given selector is not visible on screen")
# ----- ACTIONS ----- #
@_slow_available
def qfq_goto_page(self, web_path):
"""
goes to a sub page of the given path
"""
url = urlJoin([self.SELENIUM_URL, str(web_path)])
self.driver.get(url)
@_slow_available
def qfq_fill_textfield(self, data_reference, text):
self.qfq_find_by_data_ref(data_reference).send_keys(text)
"""
fills a given string into a text field which
is identified by the given data reference
"""
self.qfq_get_element_by_data_ref(data_reference).send_keys(str(text))
@_slow_available
def qfq_clear_textfield(self, data_reference):
"""
clears a textfield with a given data reference
"""
self.qfq_get_element_by_data_ref(data_reference).clear()
@_slow_available
def qfq_dropdown_select(self, data_reference, option):
Select(self.qfq_find_by_data_ref(data_reference)).select_by_visible_text(option)
"""
selects an option (with the option's text)
from a dropdown list by data reference
"""
Select(
self.qfq_get_element_by_data_ref(data_reference)
).select_by_visible_text(str(option))
@_slow_available
def qfq_radio_select(self, text):
"""
selects a radio button in a radio button set
"""
self.qfq_click_element_with_xpath(
"//label[text()='" + str(text) + "']"
)
@_slow_available
def qfq_checkbox_select(self, text):
"""
selects a checkbox in a checkbox set
"""
# TODO doesn't work currently because qfq uses div instead of label
# self.qfq_click_element_with_xpath(
# "//label[text()='" + str(text) + "']"
# )
@_slow_available
def qfq_open_pill(self, name):
"""
this function opens a pill by name
"""
self.qfq_click_element_with_xpath(
"//*[@id='qfqTabs']//a[text()='" + str(name) + "']"
)
@_slow_available
def qfq_upload_file(self, data_reference, file_name, suffix, size):
"""
this function uploads a generated file from the given
arguments size and suffix. This file is uploaded to the
input with the given data reference.
"""
# stores the tmp dir name
tmp_dir = "tmp"
# checks if the tmp dir doesn't already exist
if not os.path.exists(tmp_dir):
# creates tmp dir if it doesn't exist
os.makedirs(tmp_dir)
# loops through all files in the tmp directory
for f in glob.glob(tmp_dir + "/*." + suffix):
# removes each file (old test files)
os.remove(f)
# stores the path for the file
file_path = os.getcwd() + "/" + tmp_dir + "/" + file_name + "." + suffix
# opens the file from the file path (creates the file)
with open(file_path, "w") as f:
# writes some random text to the file with the given size
f.write(self.qfq_generate_random_string(size))
# uploads the created file
self.qfq_get_element_by_data_ref(data_reference).send_keys(file_path)
# ----- BUTTONS ----- #
@_slow_available
def qfq_click_new_form_button(self):
self.qfq_find_by_data_ref('newForm').click()
"""
clicks the button to create a new form
"""
self.qfq_click_element_with_data_ref('newForm')
@_slow_available
def qfq_click_close_form_button(self):
def f(): self.driver.find_element_by_id('close-button').click()
"""
clicks the close button when adding a new data entry into a form
"""
def f():
self.qfq_click_element_with_id('close-button')
self._retry_on_certain_exceptions(f)
@_slow_available
def qfq_click_save_form_button(self):
self.driver.find_element_by_id('save-button').click()
"""
clicks the save button when adding a new data entry into a form
"""
self.qfq_click_element_with_id('save-button')
@_slow_available
def qfq_click_element_with_text(self, element_tag, text):
"""
clicks a given element name in the dom that contains
the given text
"""
def f():
self.qfq_get_element_by_xpath(
"//" + element_tag + "[text()='" + text + "']"
).click()
self._retry_on_certain_exceptions(f)
@_slow_available
def qfq_click_element_with_data_ref(self, data_reference):
"""
clicks an element with a given data reference
"""
self.qfq_get_element_by_data_ref(data_reference).click()
@_slow_available
def qfq_click_element_with_id(self, id):
"""
clicks a button with a given id
"""
self.qfq_get_element_by_id(id).click()
@_slow_available
def qfq_click_element_with_xpath(self, xpath):
"""
clicks an element with a given xpath
"""
def f():
self.qfq_get_element_by_xpath(xpath).click()
self._retry_on_certain_exceptions(f)
@_slow_available
def qfq_click_element_with_css_selector(self, selector):
"""
this function clicks an element with a given css selector
"""
self.qfq_get_element_by_css_selector(selector).click()
# ----- HELPER FUNCTIONS ----- #
def qfq_generate_random_string(self, length=32):
"""
generates and returns a random string with
a given length (default is 32)
"""
return ''.join([
random.choice(string.ascii_letters + string.digits)\
for n in range(int(length))
])
def qfq_generate_random_number(self, start = 0, to = 2**31):
"""
generates and returns a random number with