refactor: file structure change to multiple files
This commit is contained in:
parent
9bca740133
commit
abdcce6042
0
civicrm_tester/__init__.py
Normal file
0
civicrm_tester/__init__.py
Normal file
67
civicrm_tester/activities_tab.py
Normal file
67
civicrm_tester/activities_tab.py
Normal file
@ -0,0 +1,67 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from .base import BaseTester
|
||||
|
||||
|
||||
class ActivitiesTab(BaseTester):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Activities Tab")
|
||||
self.desc("Testing if a contacts activities tab displays all activies")
|
||||
self.contact_page = self.base_url + "/civicrm/contact/view/?reset=1&cid={}"
|
||||
|
||||
def _test(self, cid: str):
|
||||
self.debug("loading contact page for CID %s" % cid)
|
||||
self.browser.get(self.contact_page.format(cid))
|
||||
self.wait_until_visible((By.ID, "ui-id-10"))
|
||||
# Contact page as loaded
|
||||
activities_tab_button = self.find_element_by_id("ui-id-10")
|
||||
num_element = activities_tab_button.find_element_by_tag_name("em")
|
||||
num_of_activ = int(num_element.text)
|
||||
activities_tab_button.click()
|
||||
table_row_selector = (
|
||||
By.CSS_SELECTOR, "#DataTables_Table_0 > tbody tr"
|
||||
)
|
||||
self.debug(
|
||||
"clicked activities button and waiting for activities page to load"
|
||||
)
|
||||
self.wait_until_visible(table_row_selector)
|
||||
# Activities page as now loaded
|
||||
table_length_dropdown = Select(
|
||||
self.browser.find_element_by_xpath(
|
||||
"//select[@name='DataTables_Table_0_length']"
|
||||
)
|
||||
)
|
||||
table_length_dropdown.select_by_visible_text("100")
|
||||
self.wait_until_clickable(table_row_selector)
|
||||
num_of_rows = len(
|
||||
self.browser.find_elements_by_css_selector("tr.crm-entity")
|
||||
)
|
||||
if num_of_activ == num_of_rows:
|
||||
self.passed(
|
||||
"expected number of activities found in activity table for CID %s. Expected: %d, Actual %d"
|
||||
% (cid, num_of_activ, num_of_rows)
|
||||
)
|
||||
elif num_of_activ > 100 and num_of_rows == 100:
|
||||
self.issue(
|
||||
"Number of activities for CID %s is above 100. This is the max amount the table can display. The table is displaying 100 entries. Pagination is not supported by the script."
|
||||
% cid
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"Number of activities is lower than expected for CID %s. Most likely didn't load properly Expected: %d, Actual %d"
|
||||
% (cid, num_of_activ, num_of_rows)
|
||||
)
|
||||
|
||||
def test_all_hardcoded_contacts(self):
|
||||
"""Loops self.test with all hardcoded contact ID's
|
||||
Using MP's as MP's are public knowledge and often have activities within the crm system
|
||||
names provided in comments here for debugging purposes
|
||||
"""
|
||||
cid_da = "42219" # Debbie Abrahams
|
||||
cid_kh = "82163" # Kate Hollern
|
||||
cid_na = "42269" # Nigel Addams
|
||||
#cid_db = "43193" Use to test 100 max limit
|
||||
cid_tuple = (cid_da, cid_kh, cid_na)
|
||||
self._test_all(cid_tuple)
|
184
civicrm_tester/base.py
Normal file
184
civicrm_tester/base.py
Normal file
@ -0,0 +1,184 @@
|
||||
import logging
|
||||
import re
|
||||
import urllib.parse as urlparse
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
|
||||
class BaseTester:
|
||||
"""
|
||||
Base class for all test suites
|
||||
|
||||
One caveat: This class defines a pseudo function "_test" which is called by "_test_all"
|
||||
Overloading classes are to define their own tests to be run by selenium after logging in
|
||||
Then they are to call "_test_all" which will run these tests in a loop with given variables
|
||||
This allows the test to be run multiple times with different terms to make sure the test covers all bases.
|
||||
"""
|
||||
def __init__(self, user: str, passwd: str, dev: bool, show_browser: bool):
|
||||
self.console = Console()
|
||||
# Disable pdfminers logger cause its annoying as fuck and provides no value
|
||||
logging.getLogger("pdfminer").setLevel(100)
|
||||
logging.basicConfig(
|
||||
level="INFO",
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler()]
|
||||
)
|
||||
self.log = logging.getLogger("civiCRM-tester")
|
||||
self.user = user
|
||||
self.passwd = passwd
|
||||
if dev:
|
||||
self.base_url = "https://crm-dev.caat.org.uk"
|
||||
else:
|
||||
self.base_url = "https://crm.caat.org.uk"
|
||||
|
||||
firefox_options = webdriver.FirefoxOptions()
|
||||
firefox_options.headless = not show_browser
|
||||
self.browser = webdriver.Firefox(options=firefox_options)
|
||||
self.wait = WebDriverWait(self.browser, 20)
|
||||
|
||||
def debug(self, msg: str):
|
||||
"""Wrapper for logging debug levels for rich output"""
|
||||
return self.log.debug(msg, extra={"markup": True})
|
||||
|
||||
def info(self, msg: str):
|
||||
"""Wrapper for logging info levels for rich output"""
|
||||
return self.log.info(msg, extra={"markup": True})
|
||||
|
||||
def warn(self, msg: str):
|
||||
"""Wrapper for logging warn levels for rich output"""
|
||||
return self.log.warning(msg, extra={"markup": True})
|
||||
|
||||
def error(self, msg: str):
|
||||
"""Wrapper for logging error levels for rich output"""
|
||||
return self.log.error(msg, extra={"markup": True})
|
||||
|
||||
def critical(self, msg: str):
|
||||
"""Wrapper for logging critical levels for rich output"""
|
||||
return self.log.critical(msg, extra={"markup": True})
|
||||
|
||||
def passed(self, msg: str):
|
||||
"""Wrapper for logging passed tests"""
|
||||
return self.info("[reverse green]PASSED[/reverse green] %s" % msg)
|
||||
|
||||
def issue(self, msg: str):
|
||||
"""
|
||||
Wrapper for logging tests with issues
|
||||
This is used for tests where the result is inconclusive due to some issue that is neither a fail nor pass.
|
||||
Usually requires human interaction
|
||||
"""
|
||||
return self.warn("[reverse yellow]ISSUE[/reverse yellow] %s" % msg)
|
||||
|
||||
def failed(self, msg: str):
|
||||
"""Wrapper for logging failed tests"""
|
||||
return self.info("[reverse red]FAILED[/reverse red] %s" % msg)
|
||||
|
||||
def title(self, test_name: str):
|
||||
"""Create title header for new suite of tests"""
|
||||
return self.info("[cyan]%s[/cyan]" % test_name)
|
||||
|
||||
def desc(self, test_desc: str):
|
||||
"""Log test description of upcoming number of tests"""
|
||||
return self.info("%s" % test_desc)
|
||||
|
||||
def login(self):
|
||||
""" Login to civicrm so we can continue with the proper cookies """
|
||||
self.browser.get(self.base_url)
|
||||
self.debug("Logging in as %s @ %s" % (self.user, self.base_url))
|
||||
username = self.browser.find_element_by_id("edit-name")
|
||||
password = self.browser.find_element_by_id("edit-pass")
|
||||
submit = self.browser.find_element_by_id("edit-submit")
|
||||
username.send_keys(self.user)
|
||||
password.send_keys(self.passwd)
|
||||
submit.click()
|
||||
|
||||
# Wait for the js elements load so we know the cookies are good.
|
||||
# Waits for "Recent Items" part of sidebar which is unique when logged in
|
||||
self.wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "block-civicrm-2"))
|
||||
)
|
||||
self.debug("Successfully logged in as %s" % self.user)
|
||||
|
||||
def logout(self):
|
||||
"""Log browser out of civicrm to reset our test environment"""
|
||||
self.browser.get(self.base_url + "/user/logout")
|
||||
# Wait for the next page to load to finish logging out
|
||||
self.wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "tabs-wrapper"))
|
||||
)
|
||||
|
||||
def _test(self, *args):
|
||||
"""Placeholder to be overwritten by overloading classes"""
|
||||
|
||||
def _test_all(self, test_strings: tuple[str]):
|
||||
"""Loops testing over the given terms"""
|
||||
try:
|
||||
self.login()
|
||||
for term in test_strings:
|
||||
self._test(term)
|
||||
finally:
|
||||
self.logout()
|
||||
self.browser.close()
|
||||
|
||||
def find_element_by_id(self, *args, **kwargs):
|
||||
"""Alias for browser.find_element_by_id"""
|
||||
return self.browser.find_element_by_id(*args, **kwargs)
|
||||
|
||||
def find_element_by_css_selector(self, *args, **kwargs):
|
||||
"""Alias for browser.find_element_by_css_selector"""
|
||||
return self.browser.find_element_by_css_selector(*args, **kwargs)
|
||||
|
||||
def wait_until_visible(self, locator):
|
||||
"""Alias for using inbuilt wait object for wait.until(EC.visibility_of_element_located)"""
|
||||
return self.wait.until(EC.visibility_of_element_located(locator))
|
||||
|
||||
def wait_until_clickable(self, locator):
|
||||
"""Alias for using inbuilt wait object for wait.until(EC.element_to_be_clickable)"""
|
||||
return self.wait.until(EC.element_to_be_clickable(locator))
|
||||
|
||||
|
||||
class SearchExportTester(BaseTester):
|
||||
def _get_export_id(self) -> str:
|
||||
"""Parses url to get the param used to ID what search we are currently doing"""
|
||||
export_page_url = self.browser.current_url
|
||||
parsed = urlparse.urlparse(export_page_url)
|
||||
qf_key = parse_qs(parsed.query)['qfKey']
|
||||
self.debug("got qf_key '%s' from url" % qf_key)
|
||||
return qf_key
|
||||
|
||||
def download(self, data: dict) -> requests.Response:
|
||||
"""
|
||||
Download exports from searches manually using requests
|
||||
This is because managing downloads within Selenium is a nightmare
|
||||
Function tries to replicate the download within the browser as much as possible
|
||||
"""
|
||||
session_cookie = {}
|
||||
for cookie in self.browser.get_cookies():
|
||||
if re.findall(r"^SSESS.*", cookie.get("name")):
|
||||
session_cookie = cookie
|
||||
self.debug("session cookie: %s" % session_cookie)
|
||||
if not session_cookie:
|
||||
self.critical("NO SESSION COOKIE FOUND. Are you logged in?")
|
||||
raise RuntimeError("No session cookie found.")
|
||||
|
||||
session = requests.Session()
|
||||
session.cookies.update(
|
||||
{session_cookie["name"]: session_cookie["value"]}
|
||||
)
|
||||
# Most of this data is replicating the export settings so that the response is the csv data
|
||||
data = {
|
||||
"qfKey": self._get_export_id(),
|
||||
"entryURL": self.base_url + "/civicrm/contact/search",
|
||||
**data # Add provided export data required for specific export requests
|
||||
}
|
||||
res = session.request(
|
||||
"POST", self.base_url + "/civicrm/contact/search", data=data
|
||||
)
|
||||
return res
|
103
civicrm_tester/contact_export.py
Normal file
103
civicrm_tester/contact_export.py
Normal file
@ -0,0 +1,103 @@
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from .base import SearchExportTester
|
||||
|
||||
|
||||
class ContactExport(SearchExportTester):
|
||||
"""Tests if exporting contacts from a search returns a CSV file with all contacts."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Contact Export")
|
||||
self.desc(
|
||||
"Testing if exporting contacts from a search returns a CSV file with all expected contacts."
|
||||
)
|
||||
self.search_url = self.base_url + "/civicrm/contact/search"
|
||||
|
||||
self.contact_selectall_id = "CIVICRM_QFID_ts_all_4"
|
||||
self.contact_dropdown_id = "select2-chosen-4"
|
||||
self.contact_exportoption_id = "select2-result-label-15"
|
||||
|
||||
def download_csv(self):
|
||||
# Data of the request that is specific for this export and not genertic like qr_key
|
||||
data = {
|
||||
"_qf_Select_next": "Continue",
|
||||
"exportOption": 1,
|
||||
"mergeOption": 0,
|
||||
"postal_greeting": 1,
|
||||
"addressee": 1,
|
||||
}
|
||||
return self.download(data)
|
||||
|
||||
def calculate_exported_contacts_number(self) -> int:
|
||||
"""
|
||||
Downloads csv, opens it in a string buffer using StringIO and passes it to the csv lib
|
||||
Counts number of rows (excl. header) and returns that value
|
||||
"""
|
||||
res = self.download_csv()
|
||||
csv_file = io.StringIO(res.text)
|
||||
|
||||
# Dict reader should remove headers
|
||||
exported_csv = csv.DictReader(csv_file)
|
||||
exported_number_exports = sum(1 for row in exported_csv)
|
||||
self.debug(
|
||||
"found %d rows in exported csv excluding header row" %
|
||||
exported_number_exports
|
||||
)
|
||||
|
||||
return exported_number_exports
|
||||
|
||||
def _test(self, search_term: str):
|
||||
"""
|
||||
Test Description:
|
||||
Go to the contact search url
|
||||
Search for the search term
|
||||
Select all contacts and set them to export
|
||||
Get the login cookie and search ID and download the export manually with requests
|
||||
Save the file in tmp
|
||||
Read it to check the number of exported contacts is the same as reported in the UI
|
||||
"""
|
||||
self.browser.get(self.search_url)
|
||||
search_box = self.find_element_by_id("sort_name")
|
||||
search_box.send_keys(search_term)
|
||||
search_box.send_keys(Keys.ENTER)
|
||||
self.debug("searching for contacts with term '%s'" % search_term)
|
||||
self.wait_until_visible(
|
||||
(By.ID, "alpha-filter")
|
||||
) #wait for table to load
|
||||
self.debug("table of results has loaded")
|
||||
|
||||
results_text = self.find_element_by_css_selector(
|
||||
".form-layout-compressed > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2) > label:nth-child(2)"
|
||||
).text
|
||||
matches = re.findall(
|
||||
r"(\d+)", results_text
|
||||
) # Should just be one match in normal cases
|
||||
result_no = int(matches[0])
|
||||
|
||||
self.debug("exporting results using the magic dropdown")
|
||||
self.find_element_by_id(self.contact_selectall_id).click()
|
||||
self.find_element_by_id(self.contact_dropdown_id).click()
|
||||
self.find_element_by_id(self.contact_exportoption_id).click()
|
||||
self.wait_until_visible((By.CSS_SELECTOR, ".crm-block"))
|
||||
|
||||
exported_number_exports = self.calculate_exported_contacts_number()
|
||||
if exported_number_exports == (result_no):
|
||||
self.passed(
|
||||
"Number of expected contact exports for '%s' matches actual number of exports - Expected: %d, Actual: %d"
|
||||
% (search_term, result_no, exported_number_exports)
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"Number of expected contact exports for '%s' WAS NOT EQUAL to actual number of exports - Expected: %d, Actual: %d"
|
||||
% (search_term, result_no, exported_number_exports)
|
||||
)
|
||||
|
||||
def test_hardcoded_search_terms(self):
|
||||
"""Loops over the test with three hardcoded search terms"""
|
||||
search_terms = ("John", "e", "Smith")
|
||||
self._test_all(search_terms)
|
81
civicrm_tester/steeringcommittee_print_labels.py
Normal file
81
civicrm_tester/steeringcommittee_print_labels.py
Normal file
@ -0,0 +1,81 @@
|
||||
import io
|
||||
import re
|
||||
|
||||
from pdfminer.high_level import extract_text
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from .base import SearchExportTester
|
||||
|
||||
|
||||
class SteeringCommitteePrintLabels(SearchExportTester):
|
||||
"""Tests the pdf labels for the SteeringCommitee show all contacts names"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Steering Committee Print Labels")
|
||||
self.desc(
|
||||
"Testing the pdf labels for the SteeringCommittee show all contacts names"
|
||||
)
|
||||
self.search_url = self.base_url + "/civicrm/contact/search"
|
||||
self.group_dropdown = "s2id_autogen2"
|
||||
self.search_button = "_qf_Basic_refresh"
|
||||
self.mail_label_option = "select2-result-label-19"
|
||||
self.contact_selectall_id = "CIVICRM_QFID_ts_all_4"
|
||||
self.contact_dropdown_id = "select2-chosen-4"
|
||||
# Using this to count amount of people in the exported pdf
|
||||
# This will fail if someone is added that doesn't have a UK adddress
|
||||
self.pdf_search_string = "UNITED KINGDOM"
|
||||
|
||||
def _test(self):
|
||||
self.browser.get(self.search_url)
|
||||
group_dropdown = self.find_element_by_id(self.group_dropdown)
|
||||
group_dropdown.click()
|
||||
group_dropdown.send_keys("Steering Committee" + Keys.ENTER)
|
||||
self.find_element_by_id(self.search_button).click()
|
||||
self.wait_until_visible(
|
||||
(By.ID, "alpha-filter")
|
||||
) #wait for table to load
|
||||
self.debug("table of results has loaded")
|
||||
# TODO: Refactor this in base class as a helper function because we do this in another test
|
||||
results_text = self.find_element_by_css_selector(
|
||||
".form-layout-compressed > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2) > label:nth-child(2)"
|
||||
).text
|
||||
matches = re.findall(
|
||||
r"(\d+)", results_text
|
||||
) # Should just be one match in normal cases
|
||||
result_no = int(matches[0])
|
||||
self.debug("exporting results using the magic dropdown")
|
||||
self.find_element_by_id(self.contact_selectall_id).click()
|
||||
self.find_element_by_id(self.contact_dropdown_id).click()
|
||||
self.find_element_by_id(self.mail_label_option).click()
|
||||
|
||||
self.wait_until_visible((By.CSS_SELECTOR, ".crm-block"))
|
||||
# By omitting the field, we are effectively disabling the do not mail filter
|
||||
data = {
|
||||
"_qf_default": "Label:submit",
|
||||
# Smallest labels, will show 24 contacts on one page
|
||||
"label_name": "3475",
|
||||
"location_type_id": "",
|
||||
"_qf_Label_submit": "Make+Mailing+Labels"
|
||||
}
|
||||
res = self.download(data)
|
||||
pdf_text = extract_text(io.BytesIO(res.content))
|
||||
label_count = pdf_text.count(self.pdf_search_string)
|
||||
if result_no == label_count:
|
||||
self.passed(
|
||||
"no missing mailing labels from pdf export. Expected: %d, Actual %d"
|
||||
% (result_no, label_count)
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"missing mailing labels from the pdf export. Expected: %d, Actual %d"
|
||||
% (result_no, label_count)
|
||||
)
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
self.login()
|
||||
self._test()
|
||||
finally:
|
||||
self.logout()
|
||||
self.browser.close()
|
430
main.py
430
main.py
@ -1,429 +1,9 @@
|
||||
import argparse
|
||||
import csv
|
||||
import logging
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import urllib.parse as urlparse
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from pdfminer.high_level import extract_text
|
||||
import requests
|
||||
from rich.logging import RichHandler
|
||||
from rich.console import Console
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
|
||||
class BaseTester:
|
||||
"""
|
||||
Base class for all test suites
|
||||
|
||||
One caveat: This class defines a pseudo function "_test" which is called by "_test_all"
|
||||
Overloading classes are to define their own tests to be run by selenium after logging in
|
||||
Then they are to call "_test_all" which will run these tests in a loop with given variables
|
||||
This allows the test to be run multiple times with different terms to make sure the test covers all bases.
|
||||
"""
|
||||
def __init__(self, user: str, passwd: str, dev: bool, show_browser: bool):
|
||||
self.console = Console()
|
||||
# Disable pdfminers logger cause its annoying as fuck and provides no value
|
||||
logging.getLogger("pdfminer").setLevel(100)
|
||||
logging.basicConfig(
|
||||
level="INFO",
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler()]
|
||||
)
|
||||
self.log = logging.getLogger("civiCRM-tester")
|
||||
self.user = user
|
||||
self.passwd = passwd
|
||||
if dev:
|
||||
self.base_url = "https://crm-dev.caat.org.uk"
|
||||
else:
|
||||
self.base_url = "https://crm.caat.org.uk"
|
||||
|
||||
firefox_options = webdriver.FirefoxOptions()
|
||||
firefox_options.headless = not show_browser
|
||||
self.browser = webdriver.Firefox(options=firefox_options)
|
||||
self.wait = WebDriverWait(self.browser, 20)
|
||||
|
||||
def debug(self, msg: str):
|
||||
"""Wrapper for logging debug levels for rich output"""
|
||||
return self.log.debug(msg, extra={"markup": True})
|
||||
|
||||
def info(self, msg: str):
|
||||
"""Wrapper for logging info levels for rich output"""
|
||||
return self.log.info(msg, extra={"markup": True})
|
||||
|
||||
def warn(self, msg: str):
|
||||
"""Wrapper for logging warn levels for rich output"""
|
||||
return self.log.warning(msg, extra={"markup": True})
|
||||
|
||||
def error(self, msg: str):
|
||||
"""Wrapper for logging error levels for rich output"""
|
||||
return self.log.error(msg, extra={"markup": True})
|
||||
|
||||
def critical(self, msg: str):
|
||||
"""Wrapper for logging critical levels for rich output"""
|
||||
return self.log.critical(msg, extra={"markup": True})
|
||||
|
||||
def passed(self, msg: str):
|
||||
"""Wrapper for logging passed tests"""
|
||||
return self.info("[reverse green]PASSED[/reverse green] %s" % msg)
|
||||
|
||||
def issue(self, msg: str):
|
||||
"""
|
||||
Wrapper for logging tests with issues
|
||||
This is used for tests where the result is inconclusive due to some issue that is neither a fail nor pass.
|
||||
Usually requires human interaction
|
||||
"""
|
||||
return self.warn("[reverse yellow]ISSUE[/reverse yellow] %s" % msg)
|
||||
|
||||
def failed(self, msg: str):
|
||||
"""Wrapper for logging failed tests"""
|
||||
return self.info("[reverse red]FAILED[/reverse red] %s" % msg)
|
||||
|
||||
def title(self, test_name: str):
|
||||
"""Create title header for new suite of tests"""
|
||||
return self.info("[cyan]%s[/cyan]" % test_name)
|
||||
|
||||
def desc(self, test_desc: str):
|
||||
"""Log test description of upcoming number of tests"""
|
||||
return self.info("%s" % test_desc)
|
||||
|
||||
def login(self):
|
||||
""" Login to civicrm so we can continue with the proper cookies """
|
||||
self.browser.get(self.base_url)
|
||||
self.debug("Logging in as %s @ %s" % (self.user, self.base_url))
|
||||
username = self.browser.find_element_by_id("edit-name")
|
||||
password = self.browser.find_element_by_id("edit-pass")
|
||||
submit = self.browser.find_element_by_id("edit-submit")
|
||||
username.send_keys(self.user)
|
||||
password.send_keys(self.passwd)
|
||||
submit.click()
|
||||
|
||||
# Wait for the js elements load so we know the cookies are good.
|
||||
# Waits for "Recent Items" part of sidebar which is unique when logged in
|
||||
self.wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "block-civicrm-2"))
|
||||
)
|
||||
self.debug("Successfully logged in as %s" % self.user)
|
||||
|
||||
def logout(self):
|
||||
"""Log browser out of civicrm to reset our test environment"""
|
||||
self.browser.get(self.base_url + "/user/logout")
|
||||
# Wait for the next page to load to finish logging out
|
||||
self.wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "tabs-wrapper"))
|
||||
)
|
||||
|
||||
def _test(self, *args):
|
||||
"""Placeholder to be overwritten by overloading classes"""
|
||||
|
||||
def _test_all(self, test_strings: tuple[str]):
|
||||
"""Loops testing over the given terms"""
|
||||
try:
|
||||
self.login()
|
||||
for term in test_strings:
|
||||
self._test(term)
|
||||
finally:
|
||||
self.logout()
|
||||
self.browser.close()
|
||||
|
||||
def find_element_by_id(self, *args, **kwargs):
|
||||
"""Alias for browser.find_element_by_id"""
|
||||
return self.browser.find_element_by_id(*args, **kwargs)
|
||||
|
||||
def find_element_by_css_selector(self, *args, **kwargs):
|
||||
"""Alias for browser.find_element_by_css_selector"""
|
||||
return self.browser.find_element_by_css_selector(*args, **kwargs)
|
||||
|
||||
def wait_until_visible(self, locator):
|
||||
"""Alias for using inbuilt wait object for wait.until(EC.visibility_of_element_located)"""
|
||||
return self.wait.until(EC.visibility_of_element_located(locator))
|
||||
|
||||
def wait_until_clickable(self, locator):
|
||||
"""Alias for using inbuilt wait object for wait.until(EC.element_to_be_clickable)"""
|
||||
return self.wait.until(EC.element_to_be_clickable(locator))
|
||||
|
||||
|
||||
class SearchExportTester(BaseTester):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _get_export_id(self) -> str:
|
||||
"""Parses url to get the param used to ID what search we are currently doing"""
|
||||
export_page_url = self.browser.current_url
|
||||
parsed = urlparse.urlparse(export_page_url)
|
||||
qf_key = parse_qs(parsed.query)['qfKey']
|
||||
self.debug("got qf_key '%s' from url" % qf_key)
|
||||
return qf_key
|
||||
|
||||
def download(self, data: dict) -> requests.Response:
|
||||
"""
|
||||
Download exports from searches manually using requests
|
||||
This is because managing downloads within Selenium is a nightmare
|
||||
Function tries to replicate the download within the browser as much as possible
|
||||
"""
|
||||
session_cookie = {}
|
||||
for cookie in self.browser.get_cookies():
|
||||
if re.findall(r"^SSESS.*", cookie.get("name")):
|
||||
session_cookie = cookie
|
||||
self.debug("session cookie: %s" % session_cookie)
|
||||
if not session_cookie:
|
||||
self.critical("NO SESSION COOKIE FOUND. Are you logged in?")
|
||||
raise RuntimeError("No session cookie found.")
|
||||
|
||||
session = requests.Session()
|
||||
session.cookies.update(
|
||||
{session_cookie["name"]: session_cookie["value"]}
|
||||
)
|
||||
# Most of this data is replicating the export settings so that the response is the csv data
|
||||
data = {
|
||||
"qfKey": self._get_export_id(),
|
||||
"entryURL": self.base_url + "/civicrm/contact/search",
|
||||
**data # Add provided export data required for specific export requests
|
||||
}
|
||||
res = session.request(
|
||||
"POST", self.base_url + "/civicrm/contact/search", data=data
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class ContactExport(SearchExportTester):
|
||||
"""Tests if exporting contacts from a search returns a CSV file with all contacts."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Contact Export")
|
||||
self.desc(
|
||||
"Testing if exporting contacts from a search returns a CSV file with all expected contacts."
|
||||
)
|
||||
self.search_url = self.base_url + "/civicrm/contact/search"
|
||||
|
||||
self.contact_selectall_id = "CIVICRM_QFID_ts_all_4"
|
||||
self.contact_dropdown_id = "select2-chosen-4"
|
||||
self.contact_exportoption_id = "select2-result-label-15"
|
||||
|
||||
def download_csv(self):
|
||||
# Data of the request that is specific for this export and not genertic like qr_key
|
||||
data = {
|
||||
"_qf_Select_next": "Continue",
|
||||
"exportOption": 1,
|
||||
"mergeOption": 0,
|
||||
"postal_greeting": 1,
|
||||
"addressee": 1,
|
||||
}
|
||||
return self.download(data)
|
||||
|
||||
def calculate_exported_contacts_number(self) -> int:
|
||||
"""
|
||||
Downloads csv, opens it in a string buffer using StringIO and passes it to the csv lib
|
||||
Counts number of rows (excl. header) and returns that value
|
||||
"""
|
||||
res = self.download_csv()
|
||||
csv_file = io.StringIO(res.text)
|
||||
|
||||
# Dict reader should remove headers
|
||||
exported_csv = csv.DictReader(csv_file)
|
||||
exported_number_exports = sum(1 for row in exported_csv)
|
||||
self.debug(
|
||||
"found %d rows in exported csv excluding header row" %
|
||||
exported_number_exports
|
||||
)
|
||||
|
||||
return exported_number_exports
|
||||
|
||||
def _test(self, search_term: str):
|
||||
"""
|
||||
Test Description:
|
||||
Go to the contact search url
|
||||
Search for the search term
|
||||
Select all contacts and set them to export
|
||||
Get the login cookie and search ID and download the export manually with requests
|
||||
Save the file in tmp
|
||||
Read it to check the number of exported contacts is the same as reported in the UI
|
||||
"""
|
||||
self.browser.get(self.search_url)
|
||||
search_box = self.find_element_by_id("sort_name")
|
||||
search_box.send_keys(search_term)
|
||||
search_box.send_keys(Keys.ENTER)
|
||||
self.debug("searching for contacts with term '%s'" % search_term)
|
||||
self.wait_until_visible(
|
||||
(By.ID, "alpha-filter")
|
||||
) #wait for table to load
|
||||
self.debug("table of results has loaded")
|
||||
|
||||
results_text = self.find_element_by_css_selector(
|
||||
".form-layout-compressed > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2) > label:nth-child(2)"
|
||||
).text
|
||||
matches = re.findall(
|
||||
r"(\d+)", results_text
|
||||
) # Should just be one match in normal cases
|
||||
result_no = int(matches[0])
|
||||
|
||||
self.debug("exporting results using the magic dropdown")
|
||||
self.find_element_by_id(self.contact_selectall_id).click()
|
||||
self.find_element_by_id(self.contact_dropdown_id).click()
|
||||
self.find_element_by_id(self.contact_exportoption_id).click()
|
||||
self.wait_until_visible((By.CSS_SELECTOR, ".crm-block"))
|
||||
|
||||
exported_number_exports = self.calculate_exported_contacts_number()
|
||||
if exported_number_exports == (result_no):
|
||||
self.passed(
|
||||
"Number of expected contact exports for '%s' matches actual number of exports - Expected: %d, Actual: %d"
|
||||
% (search_term, result_no, exported_number_exports)
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"Number of expected contact exports for '%s' WAS NOT EQUAL to actual number of exports - Expected: %d, Actual: %d"
|
||||
% (search_term, result_no, exported_number_exports)
|
||||
)
|
||||
|
||||
def test_hardcoded_search_terms(self):
|
||||
"""Loops over the test with three hardcoded search terms"""
|
||||
search_terms = ("John", "e", "Smith")
|
||||
self._test_all(search_terms)
|
||||
|
||||
|
||||
class ActivitiesTab(BaseTester):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Activities Tab")
|
||||
self.desc("Testing if a contacts activities tab displays all activies")
|
||||
self.contact_page = self.base_url + "/civicrm/contact/view/?reset=1&cid={}"
|
||||
|
||||
def _test(self, cid: str):
|
||||
self.debug("loading contact page for CID %s" % cid)
|
||||
self.browser.get(self.contact_page.format(cid))
|
||||
self.wait_until_visible((By.ID, "ui-id-10"))
|
||||
# Contact page as loaded
|
||||
activities_tab_button = self.find_element_by_id("ui-id-10")
|
||||
num_element = activities_tab_button.find_element_by_tag_name("em")
|
||||
num_of_activ = int(num_element.text)
|
||||
activities_tab_button.click()
|
||||
table_row_selector = (
|
||||
By.CSS_SELECTOR, "#DataTables_Table_0 > tbody tr"
|
||||
)
|
||||
self.debug(
|
||||
"clicked activities button and waiting for activities page to load"
|
||||
)
|
||||
self.wait_until_visible(table_row_selector)
|
||||
# Activities page as now loaded
|
||||
table_length_dropdown = Select(
|
||||
self.browser.find_element_by_xpath(
|
||||
"//select[@name='DataTables_Table_0_length']"
|
||||
)
|
||||
)
|
||||
table_length_dropdown.select_by_visible_text("100")
|
||||
self.wait_until_clickable(table_row_selector)
|
||||
num_of_rows = len(
|
||||
self.browser.find_elements_by_css_selector("tr.crm-entity")
|
||||
)
|
||||
if num_of_activ == num_of_rows:
|
||||
self.passed(
|
||||
"expected number of activities found in activity table for CID %s. Expected: %d, Actual %d"
|
||||
% (cid, num_of_activ, num_of_rows)
|
||||
)
|
||||
elif num_of_activ > 100 and num_of_rows == 100:
|
||||
self.issue(
|
||||
"Number of activities for CID %s is above 100. This is the max amount the table can display. The table is displaying 100 entries. Pagination is not supported by the script."
|
||||
% cid
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"Number of activities is lower than expected for CID %s. Most likely didn't load properly Expected: %d, Actual %d"
|
||||
% (cid, num_of_activ, num_of_rows)
|
||||
)
|
||||
|
||||
def test_all_hardcoded_contacts(self):
|
||||
"""Loops self.test with all hardcoded contact ID's
|
||||
Using MP's as MP's are public knowledge and often have activities within the crm system
|
||||
names provided in comments here for debugging purposes
|
||||
"""
|
||||
cid_da = "42219" # Debbie Abrahams
|
||||
cid_kh = "82163" # Kate Hollern
|
||||
cid_na = "42269" # Nigel Addams
|
||||
#cid_db = "43193" Use to test 100 max limit
|
||||
cid_tuple = (cid_da, cid_kh, cid_na)
|
||||
self._test_all(cid_tuple)
|
||||
|
||||
|
||||
class SteeringCommitteePrintLabels(SearchExportTester):
|
||||
"""Tests the pdf labels for the SteeringCommitee show all contacts names"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.title("Steering Committee Print Labels")
|
||||
self.desc(
|
||||
"Testing the pdf labels for the SteeringCommittee show all contacts names"
|
||||
)
|
||||
self.search_url = self.base_url + "/civicrm/contact/search"
|
||||
self.group_dropdown = "s2id_autogen2"
|
||||
self.search_button = "_qf_Basic_refresh"
|
||||
self.mail_label_option = "select2-result-label-19"
|
||||
self.contact_selectall_id = "CIVICRM_QFID_ts_all_4"
|
||||
self.contact_dropdown_id = "select2-chosen-4"
|
||||
# Using this to count amount of people in the exported pdf
|
||||
# This will fail if someone is added that doesn't have a UK adddress
|
||||
self.pdf_search_string = "UNITED KINGDOM"
|
||||
|
||||
def _test(self):
|
||||
self.browser.get(self.search_url)
|
||||
group_dropdown = self.find_element_by_id(self.group_dropdown)
|
||||
group_dropdown.click()
|
||||
group_dropdown.send_keys("Steering Committee" + Keys.ENTER)
|
||||
self.find_element_by_id(self.search_button).click()
|
||||
self.wait_until_visible(
|
||||
(By.ID, "alpha-filter")
|
||||
) #wait for table to load
|
||||
self.debug("table of results has loaded")
|
||||
# TODO: Refactor this in base class as a helper function because we do this in another test
|
||||
results_text = self.find_element_by_css_selector(
|
||||
".form-layout-compressed > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2) > label:nth-child(2)"
|
||||
).text
|
||||
matches = re.findall(
|
||||
r"(\d+)", results_text
|
||||
) # Should just be one match in normal cases
|
||||
result_no = int(matches[0])
|
||||
self.debug("exporting results using the magic dropdown")
|
||||
self.find_element_by_id(self.contact_selectall_id).click()
|
||||
self.find_element_by_id(self.contact_dropdown_id).click()
|
||||
self.find_element_by_id(self.mail_label_option).click()
|
||||
|
||||
self.wait_until_visible((By.CSS_SELECTOR, ".crm-block"))
|
||||
# By omitting the field, we are effectively disabling the do not mail filter
|
||||
data = {
|
||||
"_qf_default": "Label:submit",
|
||||
# Smallest labels, will show 24 contacts on one page
|
||||
"label_name": "3475",
|
||||
"location_type_id": "",
|
||||
"_qf_Label_submit": "Make+Mailing+Labels"
|
||||
}
|
||||
res = self.download(data)
|
||||
pdf_text = extract_text(io.BytesIO(res.content))
|
||||
label_count = pdf_text.count(self.pdf_search_string)
|
||||
if result_no == label_count:
|
||||
self.passed(
|
||||
"no missing mailing labels from pdf export. Expected: %d, Actual %d"
|
||||
% (result_no, label_count)
|
||||
)
|
||||
else:
|
||||
self.failed(
|
||||
"missing mailing labels from the pdf export. Expected: %d, Actual %d"
|
||||
% (result_no, label_count)
|
||||
)
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
self.login()
|
||||
self._test()
|
||||
finally:
|
||||
self.logout()
|
||||
self.browser.close()
|
||||
|
||||
from civicrm_tester.activities_tab import ActivitiesTab
|
||||
from civicrm_tester.contact_export import ContactExport
|
||||
from civicrm_tester.steeringcommittee_print_labels import \
|
||||
SteeringCommitteePrintLabels
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="")
|
||||
@ -452,9 +32,9 @@ if __name__ == "__main__":
|
||||
cl_arg = (
|
||||
arguments.user, arguments.passwd, arguments.dev, arguments.show_browser
|
||||
)
|
||||
SteeringCommitteePrintLabels(*cl_arg).test()
|
||||
ActivitiesTab(*cl_arg).test_all_hardcoded_contacts()
|
||||
ContactExport(*cl_arg).test_hardcoded_search_terms()
|
||||
SteeringCommitteePrintLabels(*cl_arg).test()
|
||||
|
||||
# Mailing list
|
||||
# Load mailing list test and enter test data
|
||||
|
Loading…
Reference in New Issue
Block a user