├── conf ├── __init__.py ├── remote_credentials_enc.py ├── setup_testrail.conf ├── opera_browser_conf.py ├── e2e_weather_shopper_conf.py ├── testrailenv_conf.py ├── remote_credentials.py ├── testrail_caseid_conf.py ├── tesults_conf.py ├── os_details.config ├── ssh_conf.py ├── email_conf.py ├── test_path_conf.py ├── locators_conf.py ├── browser_os_name_conf.py └── copy_framework_template_conf.py ├── tests ├── __init__.py └── test_e2e_purchase_product.py ├── page_objects ├── __init__.py ├── Sunscreens_Page.py ├── Moisturizers_Page.py ├── contact_page.py ├── contact_form_object.py ├── PageFactory.py ├── Main_Page.py ├── Cart_Page.py ├── Product_Object.py └── DriverFactory.py ├── working-weather-shopper-test.gif ├── pytest.ini ├── utils ├── gmail │ ├── utils.py │ ├── __init__.py │ ├── exceptions.py │ ├── utf.py │ ├── mailbox.py │ ├── gmail.py │ └── message.py ├── test_path.py ├── Custom_Exceptions.py ├── __init__.py ├── Tesults.py ├── csv_compare.py ├── post_test_reports_to_slack.py ├── testrail.py ├── excel_compare.py ├── Wrapit.py ├── Base_Logging.py ├── copy_framework_template.py ├── results.py ├── BrowserStack_Library.py ├── Image_Compare.py ├── Test_Runner_Class.py ├── setup_testrail.py ├── ssh_util.py ├── email_util.py ├── email_pytest_report.py ├── Test_Rail.py ├── Option_Parser.py └── xpath_util.py ├── requirements.txt ├── LICENSE ├── .gitignore ├── README.md └── conftest.py /conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /page_objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/remote_credentials_enc.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/makemework/HEAD/conf/remote_credentials_enc.py -------------------------------------------------------------------------------- /working-weather-shopper-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/makemework/HEAD/working-weather-shopper-test.gif -------------------------------------------------------------------------------- /conf/setup_testrail.conf: -------------------------------------------------------------------------------- 1 | [Example_Form_Test] 2 | case_ids = 125,127,128,129,130 3 | 4 | [Example_Table_Test] 5 | case_ids = 126 6 | 7 | -------------------------------------------------------------------------------- /conf/opera_browser_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | conf file for updating Opera Browser Location 3 | """ 4 | location = "Enter the Opera Browser Location" 5 | -------------------------------------------------------------------------------- /conf/e2e_weather_shopper_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This conf contains the data needed by test_e2e_purchase_product.py 3 | """ 4 | PURCHASE_LOGIC = {"moisturizer":['aloe'],"sunscreen":['spf-50']} -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -s -rsxX --continue-on-collection-errors --tb=short --ignore=utils/Test_Rail.py --ignore=utils/Test_Runner_Class.py -p no:cacheprovider 3 | norecursedirs = .svn _build tmp* log .vscode .git 4 | -------------------------------------------------------------------------------- /conf/testrailenv_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to hold the TestRail url and credentials 3 | """ 4 | 5 | testrail_url = "Add your testrail url" 6 | testrail_user = "Add your testrail username" 7 | testrail_password = "Add your testrail password" 8 | -------------------------------------------------------------------------------- /utils/gmail/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .gmail import Gmail 4 | 5 | def login(username, password): 6 | gmail = Gmail() 7 | gmail.login(username, password) 8 | return gmail 9 | 10 | def authenticate(username, access_token): 11 | gmail = Gmail() 12 | gmail.authenticate(username, access_token) 13 | return gmail -------------------------------------------------------------------------------- /conf/remote_credentials.py: -------------------------------------------------------------------------------- 1 | #Set REMOTE_BROWSER_PLATFROM TO BS TO RUN ON BROWSERSTACK else 2 | #SET REMOTE_BROWSER_PLATFORM TO SL TO RUN ON SAUCE LABS 3 | REMOTE_BROWSER_PLATFORM = "BS or SL" 4 | 5 | 6 | USERNAME = "Add your BrowserStack/Sauce Labs username" 7 | ACCESS_KEY = "Add your BrowserStack/Sauce Labs accesskey" 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /conf/testrail_caseid_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to hold the testcase id 3 | """ 4 | 5 | test_example_form = 125 6 | test_example_table = 126 7 | test_example_form_name = 127 8 | test_example_form_email = 128 9 | test_example_form_phone = 129 10 | test_example_form_gender = 130 11 | test_example_form_footer_contact = 131 12 | test_bitcoin_price_page_header = 234 13 | test_bitcoin_real_time_price = 235 14 | 15 | -------------------------------------------------------------------------------- /page_objects/Sunscreens_Page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module models the Sunscreens page 3 | URL: /sunscreen 4 | """ 5 | from .Base_Page import Base_Page 6 | from utils.Wrapit import Wrapit 7 | from .Product_Object import Product_Object 8 | 9 | class Sunscreens_Page(Base_Page, Product_Object): 10 | "This class models the sunscreen page" 11 | def start(self): 12 | "Go to this URL -- if needed" 13 | url = 'sunscreen' 14 | self.open(url) 15 | -------------------------------------------------------------------------------- /conf/tesults_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to hold Tesults target tokens: 3 | Create projects and targets from the Tesults configuration menu: https://www.tesults.com 4 | You can regenerate target tokens from there too. Tesults upload will fail unless the token is valid. 5 | 6 | utils/Tesults.py will use the target_token_default unless otherwise specified 7 | Find out more about targets here: https://www.tesults.com/docs?doc=target 8 | """ 9 | 10 | target_token_default = "" -------------------------------------------------------------------------------- /page_objects/Moisturizers_Page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module models the Moisturizers page 3 | URL: /moisturizer 4 | """ 5 | from .Base_Page import Base_Page 6 | from utils.Wrapit import Wrapit 7 | from .Product_Object import Product_Object 8 | 9 | class Moisturizers_Page(Base_Page, Product_Object): 10 | "This class models the moisturizer page" 11 | def start(self): 12 | "Go to this URL -- if needed" 13 | url = 'moisturizer' 14 | self.open(url) 15 | 16 | 17 | -------------------------------------------------------------------------------- /utils/gmail/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | 5 | GMail! Woo! 6 | 7 | """ 8 | 9 | __title__ = 'gmail' 10 | __version__ = '0.1' 11 | __author__ = 'Charlie Guo' 12 | __build__ = 0x0001 13 | __license__ = 'Apache 2.0' 14 | __copyright__ = 'Copyright 2013 Charlie Guo' 15 | 16 | from .gmail import Gmail 17 | from .mailbox import Mailbox 18 | from .message import Message 19 | from .exceptions import GmailException, ConnectionError, AuthenticationError 20 | from .utils import login, authenticate 21 | 22 | -------------------------------------------------------------------------------- /utils/test_path.py: -------------------------------------------------------------------------------- 1 | import os,sys 2 | import shutil 3 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 4 | from conf import test_path_conf as conf 5 | 6 | #get details from conf file for POM 7 | src_pom_files_list = conf.src_pom_files_list 8 | dst_folder_pom = conf.dst_folder_pom 9 | 10 | #check if POM folder exists and then copy files 11 | if os.path.exists(dst_folder_pom): 12 | for every_src_pom_file in src_pom_files_list: 13 | shutil.copy2(every_src_pom_file,dst_folder_pom) -------------------------------------------------------------------------------- /utils/gmail/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | gmail.exceptions 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module contains the set of Gmails' exceptions. 8 | 9 | """ 10 | 11 | 12 | class GmailException(RuntimeError): 13 | """There was an ambiguous exception that occurred while handling your 14 | request.""" 15 | 16 | class ConnectionError(GmailException): 17 | """A Connection error occurred.""" 18 | 19 | class AuthenticationError(GmailException): 20 | """Gmail Authentication failed.""" 21 | 22 | class Timeout(GmailException): 23 | """The request timed out.""" 24 | -------------------------------------------------------------------------------- /utils/Custom_Exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This utility is for Custom Exceptions. 3 | 4 | a) Stop_Test_Exception 5 | You can raise generic exceptions using just a string. 6 | This is particularly useful when you want to end a test midway based on some condition 7 | """ 8 | 9 | class Stop_Test_Exception(Exception): 10 | "Raise when a critical step fails and test needs to stop" 11 | def __init__(self,message): 12 | "Initializer" 13 | self.message=message 14 | 15 | def __str__(self): 16 | "Return the message in exception format" 17 | return self.message 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | "Check if dict item exist for given key else return none" 3 | 4 | def get_dict_item(from_this, get_this): 5 | """ get dic object item """ 6 | if not from_this: 7 | return None 8 | item = from_this 9 | if isinstance(get_this, str): 10 | if get_this in from_this: 11 | item = from_this[get_this] 12 | else: 13 | item = None 14 | else: 15 | for key in get_this: 16 | if isinstance(item, dict) and key in item: 17 | item = item[key] 18 | else: 19 | return None 20 | return item 21 | -------------------------------------------------------------------------------- /page_objects/contact_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the Contact page. 3 | URL: contact 4 | The page consists of a header, footer and form object. 5 | """ 6 | from .Base_Page import Base_Page 7 | from .contact_form_object import Contact_Form_Object 8 | from .header_object import Header_Object 9 | from .footer_object import Footer_Object 10 | from utils.Wrapit import Wrapit 11 | 12 | class Contact_Page(Base_Page,Contact_Form_Object,Header_Object,Footer_Object): 13 | "Page Object for the contact page" 14 | 15 | def start(self): 16 | "Use this method to go to specific URL -- if needed" 17 | url = 'contact' 18 | self.open(url) 19 | -------------------------------------------------------------------------------- /conf/os_details.config: -------------------------------------------------------------------------------- 1 | # Set up an OS details and browsers we test on. 2 | 3 | [Windows_7_Firefox] 4 | os = Windows 5 | os_version = 7 6 | browser = Firefox 7 | browser_version = 41.0 8 | 9 | [Windows_7_IE] 10 | os = Windows 11 | os_version = 7 12 | browser = IE 13 | browser_version = 11.0 14 | 15 | [Windows_7_Chrome] 16 | os = Windows 17 | os_version = 7 18 | browser = Chrome 19 | browser_version = 43.0 20 | 21 | [Windows_7_Opera] 22 | os = Windows 23 | os_version = 7 24 | browser = Opera 25 | browser_version = 12.16 26 | 27 | [OSX_Yosemite_Firefox] 28 | os = OS X 29 | os_version = Yosemite 30 | browser = Firefox 31 | browser_version = 38.0 32 | 33 | [OSX_Yosemite_Safari] 34 | os = OS X 35 | os_version = Yosemite 36 | browser = Safari 37 | browser_version = 8.0 -------------------------------------------------------------------------------- /conf/ssh_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This config file would have the credentials of remote server, 3 | the commands to execute, upload and download file path details. 4 | """ 5 | #Server credential details needed for ssh 6 | HOST='Enter your host details here' 7 | USERNAME='Enter your username here' 8 | PASSWORD='Enter your password here' 9 | PORT = 22 10 | TIMEOUT = 10 11 | 12 | #.pem file details 13 | PKEY = 'Enter your key filename here' 14 | 15 | #Sample commands to execute(Add your commands here) 16 | COMMANDS = ['ls;mkdir sample'] 17 | 18 | #Sample file locations to upload and download 19 | UPLOADREMOTEFILEPATH = '/etc/example/filename.txt' 20 | UPLOADLOCALFILEPATH = 'home/filename.txt' 21 | DOWNLOADREMOTEFILEPATH = '/etc/sample/data.txt' 22 | DOWNLOADLOCALFILEPATH = 'home/data.txt' 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansimarkup==1.4.0 2 | apipkg==1.5 3 | Appium-Python-Client==0.28 4 | atomicwrites==1.3.0 5 | attrs==22.1.0 6 | better-exceptions-fork==0.2.1.post6 7 | boto3==1.25.1 8 | botocore==1.28.1 9 | certifi==2022.12.7 10 | chardet==3.0.4 11 | colorama==0.4.1 12 | docutils==0.14 13 | execnet==1.6.0 14 | idna==2.7 15 | importlib-metadata==0.18 16 | jmespath==0.9.4 17 | loguru==0.2.5 18 | more-itertools==7.0.0 19 | packaging==19.0 20 | pillow>=6.2.0 21 | pluggy==1.0.0 22 | py==1.11.0 23 | Pygments==2.7.4 24 | pyparsing==2.4.0 25 | pytest==7.2.0 26 | pytest-forked==1.0.2 27 | pytest-xdist==1.22.0 28 | python-dateutil==2.8.0 29 | python-dotenv==0.8.2 30 | requests==2.28.1 31 | s3transfer==0.6.0 32 | selenium==3.13.0 33 | six==1.12.0 34 | urllib3==1.26.12 35 | wcwidth==0.1.7 36 | zipp==0.5.1 37 | -------------------------------------------------------------------------------- /page_objects/contact_form_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the form on contact page 3 | The form consists of some input fields. 4 | """ 5 | 6 | from .Base_Page import Base_Page 7 | import conf.locators_conf as locators 8 | from utils.Wrapit import Wrapit 9 | 10 | class Contact_Form_Object: 11 | "Page object for the contact Form" 12 | 13 | #locators 14 | contact_name_field = locators.contact_name_field 15 | 16 | @Wrapit._exceptionHandler 17 | def set_name(self,name): 18 | "Set the name on the Kick start form" 19 | result_flag = self.set_text(self.contact_name_field,name) 20 | self.conditional_write(result_flag, 21 | positive='Set the name to: %s'%name, 22 | negative='Failed to set the name in the form', 23 | level='debug') 24 | 25 | return result_flag 26 | -------------------------------------------------------------------------------- /utils/Tesults.py: -------------------------------------------------------------------------------- 1 | import tesults 2 | import conf.tesults_conf as conf_file 3 | 4 | cases = [] 5 | 6 | def add_test_case(data): 7 | cases.append(data) 8 | 9 | def post_results_to_tesults (): 10 | token = conf_file.target_token_default # uses default token unless otherwise specified 11 | data = { 12 | 'target': token, 13 | 'results': { 'cases': cases } 14 | } 15 | print ('-----Tesults output-----') 16 | if len(data['results']['cases']) > 0: 17 | print (data) 18 | print('Uploading results to Tesults...') 19 | ret = tesults.results(data) 20 | print ('success: ' + str(ret['success'])) 21 | print ('message: ' + str(ret['message'])) 22 | print ('warnings: ' + str(ret['warnings'])) 23 | print ('errors: ' + str(ret['errors'])) 24 | else: 25 | print ('No test results.') -------------------------------------------------------------------------------- /conf/email_conf.py: -------------------------------------------------------------------------------- 1 | #Details needed for the Gmail 2 | #Fill out the email details over here 3 | imaphost="imap.gmail.com" #Add imap hostname of your email client 4 | username="Add your email address or username here" 5 | 6 | #Login has to use the app password because of Gmail security configuration 7 | # 1. Setup 2 factor authentication 8 | # 2. Follow the 2 factor authentication setup wizard to enable an app password 9 | #Src: https://support.google.com/accounts/answer/185839?hl=en 10 | #Src: https://support.google.com/mail/answer/185833?hl=en 11 | app_password="Add app password here" 12 | 13 | #Details for sending pytest report 14 | smtp_ssl_host = 'smtp.gmail.com' # Add smtp ssl host of your email client 15 | smtp_ssl_port = 465 # Add smtp ssl port number of your email client 16 | sender = 'abc@xyz.com' #Add senders email address here 17 | targets = ['asd@xyz.com','qwe@xyz.com'] # Add recipients email address in a list 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /conf/test_path_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This conf file would have the relative paths of the files & folders. 3 | """ 4 | import os,sys 5 | 6 | #POM 7 | #Files from src POM: 8 | src_pom_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','__init__.py')) 9 | src_pom_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','conftest.py')) 10 | src_pom_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','Readme.md')) 11 | src_pom_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','Requirements.txt')) 12 | src_pom_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','setup.cfg')) 13 | 14 | #src POM file list: 15 | src_pom_files_list = [src_pom_file1,src_pom_file2,src_pom_file3,src_pom_file4,src_pom_file5] 16 | 17 | #destination folder for POM which user has to mention. This folder should be created by user. 18 | dst_folder_pom = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Play_Arena','POM')) -------------------------------------------------------------------------------- /conf/locators_conf.py: -------------------------------------------------------------------------------- 1 | #Common locator file for all locators 2 | #Locators are ordered alphabetically 3 | 4 | ############################################ 5 | #Selectors we can use 6 | #ID 7 | #NAME 8 | #css selector 9 | #CLASS_NAME 10 | #LINK_TEXT 11 | #PARTIAL_LINK_TEXT 12 | #XPATH 13 | ########################################### 14 | 15 | #Locators for the Main page 16 | TEMPERATURE_FIELD = "id,temperature" 17 | BUY_BUTTON = "xpath,//button[contains(text(),'Buy %s')]" 18 | 19 | #Product page 20 | PAGE_HEADING = "xpath,//h2[text()='%s']" 21 | PRODUCTS_LIST = "xpath,//div[contains(@class,'col-4')]" 22 | ADD_PRODUCT_BUTTON = "xpath,//div[contains(@class,'col-4') and contains(.,'%s')]/descendant::button[text()='Add']" 23 | CART_QUANTITY_TEXT = "id,cart" 24 | CART_BUTTON = "xpath,//button[@onclick='goTocart()']" 25 | 26 | #Cart page 27 | CART_TITLE = "xpath,//h2[text()='Checkout']" 28 | CART_ROW = "xpath,//tbody/descendant::tr" 29 | CART_ROW_COLUMN = "xpath,//tbody/descendant::tr[%d]/descendant::td" 30 | CART_TOTAL = "id,total" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 qxf2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /page_objects/PageFactory.py: -------------------------------------------------------------------------------- 1 | """ 2 | PageFactory uses the factory design pattern. 3 | get_page_object() returns the appropriate page object. 4 | Add elif clauses as and when you implement new pages. 5 | """ 6 | 7 | from page_objects.Main_Page import Main_Page 8 | from page_objects.Sunscreens_Page import Sunscreens_Page 9 | from page_objects.Moisturizers_Page import Moisturizers_Page 10 | from page_objects.Cart_Page import Cart_Page 11 | 12 | class PageFactory(): 13 | "PageFactory uses the factory design pattern." 14 | def get_page_object(page_name,base_url='http://weathershopper.pythonanywhere.com/',trailing_slash_flag=True): 15 | "Return the appropriate page object based on page_name" 16 | test_obj = None 17 | page_name = page_name.lower() 18 | if page_name in ["main page", "main", "landing", "landing page"]: 19 | test_obj = Main_Page(base_url=base_url,trailing_slash_flag=trailing_slash_flag) 20 | elif page_name in ["moisturizers","moisturizer"]: 21 | test_obj = Moisturizers_Page(base_url=base_url,trailing_slash_flag=trailing_slash_flag) 22 | elif page_name in ["sunscreens","sunscreen"]: 23 | test_obj = Sunscreens_Page(base_url=base_url,trailing_slash_flag=trailing_slash_flag) 24 | elif page_name in ["carts","cart","shopping cart"]: 25 | test_obj = Cart_Page(base_url=base_url,trailing_slash_flag=trailing_slash_flag) 26 | 27 | return test_obj 28 | 29 | get_page_object = staticmethod(get_page_object) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | #DS_Store 7 | .DS_store 8 | 9 | #Custom 10 | venv* 11 | log/ 12 | screenshots/ 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | -------------------------------------------------------------------------------- /utils/csv_compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare two csv files. 3 | 4 | """ 5 | import csv,os 6 | 7 | class Csv_Compare(): 8 | def is_equal(self,csv_actual,csv_expected): 9 | "Method to compare the Actual and Expected csv file" 10 | result_flag = True 11 | 12 | if not os.path.exists(csv_actual): 13 | result_flag = False 14 | print('Could not locate the csv file: %s'%csv_actual) 15 | 16 | if not os.path.exists(csv_expected): 17 | result_flag = False 18 | print('Could not locate the csv file: %s'%csv_expected) 19 | 20 | if os.path.exists(csv_actual) and os.path.exists(csv_expected): 21 | #Open the csv file and put the content to list 22 | with open(csv_actual, 'r') as actual_csvfile, open(csv_expected, 'r') as exp_csvfile: 23 | reader = csv.reader(actual_csvfile) 24 | actual_file = [row for row in reader] 25 | reader = csv.reader(exp_csvfile) 26 | exp_file = [row for row in reader] 27 | 28 | if (len(actual_file)!= len(exp_file)): 29 | result_flag = False 30 | print("Mismatch in number of rows. The actual row count didn't match with expected row count") 31 | else: 32 | for actual_row, actual_col in zip(actual_file,exp_file): 33 | if actual_row == actual_col: 34 | pass 35 | else: 36 | print("Mismatch between actual and expected file at Row: ",actual_file.index(actual_row)) 37 | print("Row present only in Actual file: %s"%actual_row) 38 | print("Row present only in Expected file: %s"%actual_col) 39 | result_flag = False 40 | 41 | return result_flag 42 | 43 | 44 | #---USAGE EXAMPLES 45 | if __name__=='__main__': 46 | print("Start of %s"%__file__) 47 | 48 | #Fill in the file1 and file2 paths 49 | file1 = 'Add path for the first file here' 50 | file2 = 'Add path for the second file here' 51 | 52 | #Initialize the csv object 53 | csv_obj = Csv_Compare() 54 | 55 | #Sample code to compare csv files 56 | if csv_obj.is_equal(file1,file2) is True: 57 | print("Data matched in both the csv files\n") 58 | else: 59 | print("Data mismatch between the actual and expected csv files") 60 | -------------------------------------------------------------------------------- /utils/post_test_reports_to_slack.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A Simple API util which used to post test reports on Slack Channel. 3 | 4 | Steps to Use: 5 | 1. Generate Slack incoming webhook url by reffering our blog: https://qxf2.com/blog/post-pytest-test-results-on-slack/ & add url in our code 6 | 2. Generate test report log file by adding ">log/pytest_report.log" command at end of py.test command for e.g. py.test -k example_form -I Y -r F -v > log/pytest_report.log 7 | Note: Your terminal must be pointed to root address of our POM while generating test report file using above command 8 | 3. Check you are calling correct report log file or not 9 | ''' 10 | import json,os,requests 11 | 12 | def post_reports_to_slack(): 13 | #To generate incoming webhook url ref: https://qxf2.com/blog/post-pytest-test-results-on-slack/ 14 | url= "incoming webhook url" #Add your Slack incoming webhook url here 15 | 16 | #To generate pytest_report.log file add ">pytest_report.log" at end of py.test command for e.g. py.test -k example_form -I Y -r F -v > log/pytest_report.log 17 | test_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','log','pytest_report.log'))#Change report file name & address here 18 | 19 | with open(test_report_file, "r") as in_file: 20 | testdata = "" 21 | for line in in_file: 22 | testdata = testdata + '\n' + line 23 | 24 | # Set Slack Pass Fail bar indicator color according to test results 25 | if 'FAILED' in testdata: 26 | bar_color = "#ff0000" 27 | else: 28 | bar_color = "#36a64f" 29 | 30 | data = {"attachments":[ 31 | {"color": bar_color, 32 | "title": "Test Report", 33 | "text": testdata} 34 | ]} 35 | json_params_encoded = json.dumps(data) 36 | slack_response = requests.post(url=url,data=json_params_encoded,headers={"Content-type":"application/json"}) 37 | if slack_response.text == 'ok': 38 | print('\n Successfully posted pytest report on Slack channel') 39 | else: 40 | print('\n Something went wrong. Unable to post pytest report on Slack channel. Slack Response:', slack_response) 41 | 42 | 43 | #---USAGE EXAMPLES 44 | if __name__=='__main__': 45 | post_reports_to_slack() 46 | 47 | -------------------------------------------------------------------------------- /utils/testrail.py: -------------------------------------------------------------------------------- 1 | # 2 | # TestRail API binding for Python 3.x (API v2, available since 3 | # TestRail 3.0) 4 | # 5 | # Learn more: 6 | # 7 | # http://docs.gurock.com/testrail-api2/start 8 | # http://docs.gurock.com/testrail-api2/accessing 9 | # 10 | # Copyright Gurock Software GmbH. See license.md for details. 11 | # 12 | 13 | import urllib.request, urllib.error 14 | import json, base64 15 | 16 | class APIClient: 17 | def __init__(self, base_url): 18 | self.user = '' 19 | self.password = '' 20 | if not base_url.endswith('/'): 21 | base_url += '/' 22 | self.__url = base_url + 'index.php?/api/v2/' 23 | print ("API") 24 | 25 | # 26 | # Send Get 27 | # 28 | # Issues a GET request (read) against the API and returns the result 29 | # (as Python dict). 30 | # 31 | # Arguments: 32 | # 33 | # uri The API method to call including parameters 34 | # (e.g. get_case/1) 35 | # 36 | def send_get(self, uri): 37 | return self.__send_request('GET', uri, None) 38 | 39 | # 40 | # Send POST 41 | # 42 | # Issues a POST request (write) against the API and returns the result 43 | # (as Python dict). 44 | # 45 | # Arguments: 46 | # 47 | # uri The API method to call including parameters 48 | # (e.g. add_case/1) 49 | # data The data to submit as part of the request (as 50 | # Python dict, strings must be UTF-8 encoded) 51 | # 52 | def send_post(self, uri, data): 53 | return self.__send_request('POST', uri, data) 54 | 55 | def __send_request(self, method, uri, data): 56 | url = self.__url + uri 57 | request = urllib.request.Request(url) 58 | if (method == 'POST'): 59 | request.data = bytes(json.dumps(data), 'utf-8') 60 | auth = str( 61 | base64.b64encode( 62 | bytes('%s:%s' % (self.user, self.password), 'utf-8') 63 | ), 64 | 'ascii' 65 | ).strip() 66 | request.add_header('Authorization', 'Basic %s' % auth) 67 | request.add_header('Content-Type', 'application/json') 68 | 69 | e = None 70 | try: 71 | response = urllib.request.urlopen(request).read() 72 | except urllib.error.HTTPError as ex: 73 | response = ex.read() 74 | e = ex 75 | 76 | if response: 77 | result = json.loads(response.decode()) 78 | else: 79 | result = {} 80 | 81 | if e != None: 82 | if result and 'error' in result: 83 | error = '"' + result['error'] + '"' 84 | else: 85 | error = 'No additional error message received' 86 | raise APIError('TestRail API returned HTTP %s (%s)' % 87 | (e.code, error)) 88 | 89 | return result 90 | 91 | class APIError(Exception): 92 | pass -------------------------------------------------------------------------------- /page_objects/Main_Page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the landing page of Weather Shopper. 3 | URL: / 4 | """ 5 | from .Base_Page import Base_Page 6 | from utils.Wrapit import Wrapit 7 | import conf.locators_conf as locators 8 | 9 | class Main_Page(Base_Page): 10 | "Page Object for the main page" 11 | #locators 12 | TEMPERATURE_FIELD = locators.TEMPERATURE_FIELD 13 | BUY_BUTTON = locators.BUY_BUTTON 14 | PAGE_HEADING = locators.PAGE_HEADING 15 | 16 | def start(self): 17 | "Use this method to go to specific URL -- if needed" 18 | url = '' 19 | self.open(url) 20 | 21 | @Wrapit._screenshot 22 | def get_temperature(self): 23 | "Return the temperature listed on the landing page" 24 | result_flag = False 25 | temperature = self.get_text(self.TEMPERATURE_FIELD) 26 | if temperature is not None: 27 | self.write("The temperature parsed is: %s"%temperature,level="debug") 28 | #Strip away the degree centigrade 29 | temperature = temperature.split()[1] 30 | 31 | try: 32 | temperature = int(temperature) 33 | except Exception as e: 34 | self.write("Error type casting temperature to int",level="error") 35 | self.write("Obtained the temperature %s"%temperature) 36 | self.write("Python says: " + str(e)) 37 | else: 38 | result_flag = True 39 | else: 40 | self.write("Unable to read the temperture.",level="") 41 | self.conditional_write(result_flag, 42 | positive="Obtained the temperature: %d"%temperature, 43 | negative="Could not obtain the temperature on the landing page.") 44 | 45 | return temperature 46 | 47 | @Wrapit._exceptionHandler 48 | @Wrapit._screenshot 49 | def click_buy_button(self,product_type): 50 | "Choose to buy moisturizer or sunscreen" 51 | result_flag = False 52 | product_type = product_type.lower() 53 | if product_type in ['sunscreens','moisturizers']: 54 | result_flag = self.click_element(self.BUY_BUTTON%product_type) 55 | self.conditional_write(result_flag, 56 | positive="Clicked the buy button for %s"%product_type, 57 | negative="Could not click the buy button for %s"%product_type) 58 | result_flag &= self.smart_wait(5,self.PAGE_HEADING%product_type.title()) 59 | if result_flag: 60 | self.switch_page(product_type) 61 | 62 | self.conditional_write(result_flag, 63 | positive="Automation is on the %s page"%product_type.title(), 64 | negative="Automation could not navigate to the %s page"%product_type.title()) 65 | 66 | return result_flag -------------------------------------------------------------------------------- /utils/excel_compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare two excel files using openxl module 3 | 4 | """ 5 | import openpyxl 6 | import os 7 | 8 | class Excel_Compare(): 9 | def is_equal(self,xl_actual,xl_expected): 10 | "Method to compare the Actual and Expected xl file" 11 | result_flag = True 12 | if not os.path.exists(xl_actual): 13 | result_flag = False 14 | print('Could not locate the excel file: %s'%xl_actual) 15 | 16 | if not os.path.exists(xl_expected): 17 | result_flag = False 18 | print('Could not locate the excel file %s'%xl_expected) 19 | 20 | if os.path.exists(xl_actual) and os.path.exists(xl_expected): 21 | #Open the xl file and put the content to list 22 | actual_xlfile = openpyxl.load_workbook(xl_actual) 23 | xl_sheet = actual_xlfile.active 24 | actual_file = [] 25 | for row in xl_sheet.iter_rows(min_row=1, max_col=xl_sheet.max_column, max_row=xl_sheet.max_row): 26 | for cell in row: 27 | actual_file.append(cell.value) 28 | 29 | exp_xlfile = openpyxl.load_workbook(xl_expected) 30 | xl_sheet = exp_xlfile.active 31 | exp_file = [] 32 | for row in xl_sheet.iter_rows(min_row=1, max_col=xl_sheet.max_column, max_row=xl_sheet.max_row): 33 | for cell in row: 34 | exp_file.append(cell.value) 35 | 36 | #If there is row and column mismatch result_flag = False 37 | if (len(actual_file)!= len(exp_file)): 38 | result_flag = False 39 | print("Mismatch in number of rows or columns. The actual row or column count didn't match with expected row or column count") 40 | else: 41 | for actual_row, actual_col in zip(actual_file,exp_file): 42 | if actual_row == actual_col: 43 | pass 44 | else: 45 | print("Mismatch between actual and expected file at position(each row consists of 23 coordinates):",actual_file.index(actual_row)) 46 | print("Data present only in Actual file: %s"%actual_row) 47 | print("Data present only in Expected file: %s"%actual_col) 48 | result_flag = False 49 | 50 | return result_flag 51 | 52 | 53 | #---USAGE EXAMPLES 54 | if __name__=='__main__': 55 | print("Start of %s"%__file__) 56 | # Enter the path details of the xl files here 57 | file1 = 'Add path to the first xl file' 58 | file2 = 'Add path to the second xl file' 59 | 60 | #Initialize the excel object 61 | xl_obj = Excel_Compare() 62 | 63 | #Sample code to compare excel files 64 | if xl_obj.is_equal(file1,file2) is True: 65 | print("Data matched in both the excel files\n") 66 | else: 67 | print("Data mismatch between the actual and expected excel files") -------------------------------------------------------------------------------- /utils/Wrapit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class to hold miscellaneous but useful decorators for our framework 3 | """ 4 | 5 | from inspect import getfullargspec 6 | from page_objects.Base_Page import Base_Page 7 | 8 | 9 | class Wrapit(): 10 | 11 | "Wrapit class to hold decorator functions" 12 | def _exceptionHandler(f): 13 | "Decorator to handle exceptions" 14 | argspec = getfullargspec(f) 15 | def inner(*args,**kwargs): 16 | try: 17 | return f(*args,**kwargs) 18 | except Exception as e: 19 | args[0].write('You have this exception') 20 | args[0].write('Exception in method: %s'%str(f.__name__)) 21 | args[0].write('PYTHON SAYS: %s'%str(e)) 22 | #we denote None as failure case 23 | return None 24 | 25 | return inner 26 | 27 | 28 | def _screenshot(func): 29 | "Decorator for taking screenshots" 30 | def wrapper(*args,**kwargs): 31 | result = func(*args,**kwargs) 32 | screenshot_name = '%003d'%args[0].screenshot_counter + '_' + func.__name__ 33 | args[0].screenshot_counter += 1 34 | args[0].save_screenshot(screenshot_name) 35 | 36 | return result 37 | 38 | return wrapper 39 | 40 | 41 | def _check_browser_console_log(func): 42 | "Decorator to check the browser's console log for errors" 43 | def wrapper(*args,**kwargs): 44 | #As IE driver does not support retrieval of any logs, 45 | #we are bypassing the read_browser_console_log() method 46 | result = func(*args, **kwargs) 47 | if "ie" not in str(args[0].driver): 48 | result = func(*args, **kwargs) 49 | log_errors = [] 50 | new_errors = [] 51 | log = args[0].read_browser_console_log() 52 | if log != None: 53 | for entry in log: 54 | if entry['level']=='SEVERE': 55 | log_errors.append(entry['message']) 56 | 57 | if args[0].current_console_log_errors != log_errors: 58 | #Find the difference 59 | new_errors = list(set(log_errors) - set(args[0].current_console_log_errors)) 60 | #Set current_console_log_errors = log_errors 61 | args[0].current_console_log_errors = log_errors 62 | 63 | if len(new_errors)>0: 64 | args[0].failure("\nBrowser console error on url: %s\nMethod: %s\nConsole error(s):%s"%(args[0].get_current_url(),func.__name__,'\n----'.join(new_errors))) 65 | 66 | return result 67 | 68 | return wrapper 69 | 70 | 71 | _exceptionHandler = staticmethod(_exceptionHandler) 72 | _screenshot = staticmethod(_screenshot) 73 | _check_browser_console_log = staticmethod(_check_browser_console_log) 74 | 75 | 76 | -------------------------------------------------------------------------------- /utils/Base_Logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: A plug-n-play class for logging. 3 | This class wraps around Python's loguru module. 4 | """ 5 | import os, inspect 6 | import datetime 7 | import sys 8 | from loguru import logger 9 | 10 | class Base_Logging(): 11 | "A plug-n-play class for logging" 12 | def __init__(self,log_file_name=None,level="DEBUG",format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module} | {message}"): 13 | "Constructor for the logging class" 14 | self.log_file_name=log_file_name 15 | self.log_file_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','log')) 16 | self.level=level 17 | self.format=format 18 | self.log = self.set_log(self.log_file_name,self.level,self.format) 19 | 20 | 21 | def set_log(self,log_file_name,level,format,test_module_name=None): 22 | "Add an handler sending log messages to a sink" 23 | if test_module_name is None: 24 | test_module_name = self.get_calling_module() 25 | if not os.path.exists(self.log_file_dir): 26 | os.makedirs(self.log_file_dir) 27 | if log_file_name is None: 28 | log_file_name = self.log_file_dir + os.sep + test_module_name + '.log' 29 | else: 30 | log_file_name = self.log_file_dir + os.sep + log_file_name 31 | 32 | logger.add(log_file_name,level=level,format=format, 33 | rotation="30 days", filter=None, colorize=None, serialize=False, backtrace=True, enqueue=False, catch=True) 34 | 35 | 36 | def get_calling_module(self): 37 | "Get the name of the calling module" 38 | calling_file = inspect.stack()[-1][1] 39 | if 'runpy' in calling_file: 40 | calling_file = inspect.stack()[4][1] 41 | 42 | calling_filename = calling_file.split(os.sep) 43 | 44 | #This logic bought to you by windows + cygwin + git bash 45 | if len(calling_filename) == 1: #Needed for 46 | calling_filename = calling_file.split('/') 47 | 48 | self.calling_module = calling_filename[-1].split('.')[0] 49 | 50 | return self.calling_module 51 | 52 | 53 | def write(self,msg,level='info'): 54 | "Write out a message" 55 | fname = inspect.stack()[2][3] #May be use a entry-exit decorator instead 56 | d = {'caller_func': fname} 57 | if level.lower()== 'debug': 58 | logger.debug("{module} | {msg}",module=d['caller_func'],msg=msg) 59 | elif level.lower()== 'info': 60 | logger.info("{module} | {msg}",module=d['caller_func'],msg=msg) 61 | elif level.lower()== 'warn' or level.lower()=='warning': 62 | logger.warning("{module} | {msg}",module=d['caller_func'],msg=msg) 63 | elif level.lower()== 'error': 64 | logger.error("{module} | {msg}",module=d['caller_func'],msg=msg) 65 | elif level.lower()== 'critical': 66 | logger.critical("{module} | {msg}",module=d['caller_func'],msg=msg) 67 | else: 68 | logger.critical("Unknown level passed for the msg: {}", msg) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Make me work! 2 | Fix the issues in this repo and make this program work. This repository is aimed at folks who have already learnt to *write* basic Python but are looking for more realistic challenges that involve reading a large enough codebase, exploring file structures and making changes to an existing codebase. 3 | 4 | The code you are going to run is a Selenium test for the [Weather Shopper](http://weathershopper.pythonanywhere.com/) application. The automated test itself completes the [weather shopper exercise](https://github.com/qxf2/weather-shopper). Your job is to fix the problems in the automated test and make it run successfully. 5 | 6 | # Setup 7 | 0. This codebase uses Python 3.10.0 8 | 1. Fork this repository 9 | 2. Clone your forked repository 10 | 3. Create a virtualenv and activate it 11 | 4. `pip install -r requirements.txt` 12 | 5. Install Chrome driver. If you don't know how to, please try: 13 | > [Chrome driver](https://sites.google.com/a/chromium.org/chromedriver/getting-started) 14 | 6. Run the test using the command `pytest -k e2e` 15 | 16 | The setup instructions are intentionally high-level since this repository is aimed at people with people who have already written Python before. If you are beginner, you will find our [other repository](https://github.com/qxf2/wtfiswronghere) a better place to start. 17 | 18 | # Your assignment 19 | The [weather shopper exercise](https://github.com/qxf2/weather-shopper) has been partially completed using the code provided to you. Your assignment is to: 20 | 21 | 1. fix the errors in the existing code 22 | 2. complete the exercise on the payment page 23 | 3. use the same design patterns and programming style when solving the exercises 24 | 25 | # How to proceed? 26 | 1. Run the test using the command `pytest -k e2e` 27 | 2. Observe, debug and fix the error 28 | 3. Test your fix 29 | 4. Commit your change and push 30 | 5. Repeat steps 1-4 for the next error 31 | 32 | # Example working test 33 | If you fix all the bugs in this code, your test should perform like the gif below: 34 | 35 | ![](working-weather-shopper-test.gif) 36 | 37 | Remember, you should not stop at just fixing the existing code. You should also complete the instructions on the cart page too! 38 | 39 | # Debugging tips 40 | Here are some useful debugging tips that do not involve the use of debugger: 41 | 42 | 1. Search for strings in all files 43 | 2. Search for sub-strings in all files if the exact string does not exist 44 | 3. F12 to follow the definition of a method in Visual Studio Code 45 | 4. Add debug messages to figure out the flow 46 | 5. if True: trick (to get exact error messages, in the test, replace `try:` with `if True:` and comment out the `except` portion) 47 | 6. Read the log messages backwards 48 | 7. Sometimes the error happens in the line before the failure! 49 | 50 | 51 | # Notes 52 | 1. Use Python 3.10.0 53 | 2. We recommend using Visual Studio code as your IDE 54 | 3. We recomment using a virtualenv 55 | 4. You need to have Chrome driver installed 56 | 57 | # About 58 | This repository is created and maintained by [Qxf2 Services](https://qxf2.com/?utm_source=github&utm_medium=click&utm_campaign=Make%20me%20word). Qxf2 provides software testing services for startups. 59 | If your team is working on an early stage product and needs QA, you can hire Qxf2 Services to help. Contact Arun at mak@qxf2.com 60 | -------------------------------------------------------------------------------- /utils/copy_framework_template.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script would copy the required framework files from the input source to the input destination given by the user. 3 | 1. Copy files from POM to the newly created destination directory. 4 | 2. Verify if the destination directory is created and create the sub-folder to copy files from POM\Conf. 5 | 3. Verify if the destination directory is created and create the sub-folder to copy files from POM\Page_Objects. 6 | 4. Verify if the destination directory is created and create the sub-folder to copy files from POM\Utils. 7 | 8 | """ 9 | import os,sys 10 | import shutil 11 | from optparse import OptionParser 12 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | from conf import copy_framework_template_conf as conf 14 | 15 | def copy_framework_template(src_folder,dst_folder): 16 | "run the test" 17 | #1. Copy files from POM to the newly created destination directory. 18 | #1a. Get details from conf file 19 | src_files_list = conf.src_files_list 20 | 21 | #1b. Create the new destination directory 22 | os.makedirs(dst_folder) 23 | 24 | #1c. Check if destination folder exists and then copy files 25 | if os.path.exists(dst_folder): 26 | for every_src_file in src_files_list: 27 | shutil.copy2(every_src_file,dst_folder) 28 | 29 | #2. Verify if the destination directory is created and create the sub-folder to copy files from POM\Conf. 30 | #2a. Get details from conf file for Conf 31 | src_conf_files_list = conf.src_conf_files_list 32 | dst_folder_conf = conf.dst_folder_conf 33 | 34 | #2b. Create the conf sub-folder 35 | if os.path.exists(dst_folder): 36 | os.mkdir(dst_folder_conf) 37 | 38 | #2c. Check if conf folder exists and then copy files 39 | if os.path.exists(dst_folder_conf): 40 | for every_src_conf_file in src_conf_files_list: 41 | shutil.copy2(every_src_conf_file,dst_folder_conf) 42 | 43 | #3. Verify if the destination directory is created and create the sub-folder to copy files from POM\Page_Objects. 44 | #3a. Get details from conf file for Page_Objects 45 | src_page_objects_files_list = conf.src_page_objects_files_list 46 | dst_folder_page_objects = conf.dst_folder_page_objects 47 | 48 | #3b. Create the page_object sub-folder 49 | if os.path.exists(dst_folder): 50 | os.mkdir(dst_folder_page_objects) 51 | 52 | #3c. Check if page_object folder exists and then copy files 53 | if os.path.exists(dst_folder_page_objects): 54 | for every_src_page_objects_file in src_page_objects_files_list: 55 | shutil.copy2(every_src_page_objects_file,dst_folder_page_objects) 56 | 57 | #4. Verify if the destination directory is created and create the sub-folder to copy files from POM\Utils. 58 | #4a. Get details from conf file for Utils folder 59 | src_utils_files_list = conf.src_utils_files_list 60 | dst_folder_utils = conf.dst_folder_utils 61 | 62 | #4b. Create the utils destination directory 63 | if os.path.exists(dst_folder): 64 | os.mkdir(dst_folder_utils) 65 | 66 | #4c. Check if utils folder exists and then copy files 67 | if os.path.exists(dst_folder_utils): 68 | for every_src_utils_file in src_utils_files_list: 69 | shutil.copy2(every_src_utils_file,dst_folder_utils) 70 | 71 | #---START OF SCRIPT 72 | if __name__=='__main__': 73 | #run the test 74 | parser=OptionParser() 75 | parser.add_option("-s","--source",dest="src",help="The name of the source folder: ie, POM",default="POM") 76 | parser.add_option("-d","--destination",dest="dst",help="The name of the destination folder: ie, client name",default="Myntra") 77 | (options,args) = parser.parse_args() 78 | 79 | copy_framework_template(options.src,options.dst) 80 | -------------------------------------------------------------------------------- /utils/gmail/utf.py: -------------------------------------------------------------------------------- 1 | # The contents of this file has been derived code from the Twisted project 2 | # (http://twistedmatrix.com/). The original author is Jp Calderone. 3 | 4 | # Twisted project license follows: 5 | 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | text_type = unicode 26 | binary_type = str 27 | 28 | PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7f)) 29 | 30 | def encode(s): 31 | """Encode a folder name using IMAP modified UTF-7 encoding. 32 | 33 | Despite the function's name, the output is still a unicode string. 34 | """ 35 | if not isinstance(s, text_type): 36 | return s 37 | 38 | r = [] 39 | _in = [] 40 | 41 | def extend_result_if_chars_buffered(): 42 | if _in: 43 | r.extend(['&', modified_utf7(''.join(_in)), '-']) 44 | del _in[:] 45 | 46 | for c in s: 47 | if ord(c) in PRINTABLE: 48 | extend_result_if_chars_buffered() 49 | r.append(c) 50 | elif c == '&': 51 | extend_result_if_chars_buffered() 52 | r.append('&-') 53 | else: 54 | _in.append(c) 55 | 56 | extend_result_if_chars_buffered() 57 | 58 | return ''.join(r) 59 | 60 | def decode(s): 61 | """Decode a folder name from IMAP modified UTF-7 encoding to unicode. 62 | 63 | Despite the function's name, the input may still be a unicode 64 | string. If the input is bytes, it's first decoded to unicode. 65 | """ 66 | if isinstance(s, binary_type): 67 | s = s.decode('latin-1') 68 | if not isinstance(s, text_type): 69 | return s 70 | 71 | r = [] 72 | _in = [] 73 | for c in s: 74 | if c == '&' and not _in: 75 | _in.append('&') 76 | elif c == '-' and _in: 77 | if len(_in) == 1: 78 | r.append('&') 79 | else: 80 | r.append(modified_deutf7(''.join(_in[1:]))) 81 | _in = [] 82 | elif _in: 83 | _in.append(c) 84 | else: 85 | r.append(c) 86 | if _in: 87 | r.append(modified_deutf7(''.join(_in[1:]))) 88 | 89 | return ''.join(r) 90 | 91 | def modified_utf7(s): 92 | # encode to utf-7: '\xff' => b'+AP8-', decode from latin-1 => '+AP8-' 93 | s_utf7 = s.encode('utf-7').decode('latin-1') 94 | return s_utf7[1:-1].replace('/', ',') 95 | 96 | def modified_deutf7(s): 97 | s_utf7 = '+' + s.replace(',', '/') + '-' 98 | # encode to latin-1: '+AP8-' => b'+AP8-', decode from utf-7 => '\xff' 99 | return s_utf7.encode('latin-1').decode('utf-7') -------------------------------------------------------------------------------- /utils/results.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tracks test results and logs them. 3 | Keeps counters of pass/fail/total. 4 | """ 5 | import logging 6 | from utils.Base_Logging import Base_Logging 7 | 8 | 9 | class Results(object): 10 | """ Base class for logging intermediate test outcomes """ 11 | 12 | def __init__(self, level=logging.DEBUG, log_file_path=None): 13 | self.logger = Base_Logging(log_file_name=log_file_path, level=level) 14 | self.total = 0 # Increment whenever success or failure are called 15 | self.passed = 0 # Increment everytime success is called 16 | self.written = 0 # Increment when conditional_write is called 17 | # Increment when conditional_write is called with True 18 | self.written_passed = 0 19 | self.failure_message_list = [] 20 | 21 | 22 | def assert_results(self): 23 | """ Check if the test passed or failed """ 24 | assert self.passed == self.total 25 | 26 | 27 | def write(self, msg, level='info'): 28 | """ This method use the logging method """ 29 | self.logger.write(msg, level) 30 | 31 | 32 | def record(self, condition, msg, level='debug', indent=1): 33 | """ Write out either the positive or the negative message based on flag """ 34 | if condition: 35 | self.written_passed += 1 36 | 37 | prefix = '' 38 | for i in range(indent if indent > 0 else 0): 39 | prefix = prefix + ' ' 40 | 41 | self.written += 1 42 | self.write(prefix + msg + (' False' if condition else ' True'), level) 43 | 44 | 45 | def conditional_write(self, condition, positive, negative, level='info', pre_format=" - "): 46 | """ Write out either the positive or the negative message based on flag """ 47 | if condition: 48 | self.write(pre_format + positive, level) 49 | self.written_passed += 1 50 | else: 51 | self.write(pre_format + negative, level) 52 | self.written += 1 53 | 54 | 55 | def log_result(self, flag, positive, negative, level='info'): 56 | """ Write out the result of the test """ 57 | if flag is True: 58 | self.success(positive, level=level) 59 | if flag is False: 60 | self.failure(negative, level=level) 61 | raise Exception 62 | self.write('~~~~~~~~\n', level) 63 | 64 | 65 | def success(self, msg, level='info', pre_format='PASS: '): 66 | """ Write out a success message """ 67 | self.logger.write(pre_format + msg, level) 68 | self.total += 1 69 | self.passed += 1 70 | 71 | 72 | def failure(self, msg, level='info', pre_format='FAIL: '): 73 | """ Write out a failure message """ 74 | self.logger.write(pre_format + msg, level) 75 | self.total += 1 76 | self.failure_message_list.append(pre_format + msg) 77 | 78 | 79 | def get_failure_message_list(self): 80 | """ Return the failure message list """ 81 | 82 | return self.failure_message_list 83 | 84 | 85 | def write_test_summary(self): 86 | """ Print out a useful, human readable summary """ 87 | self.write('\n************************\n--------RESULT--------\nTotal number of checks=%d' % self.total) 88 | self.write('Total number of checks passed=%d\n----------------------\n************************\n\n' % self.passed) 89 | self.write('Total number of mini-checks=%d' % self.written) 90 | self.write('Total number of mini-checks passed=%d' % self.written_passed) 91 | failure_message_list = self.get_failure_message_list() 92 | if len(failure_message_list) > 0: 93 | self.write('\n--------FAILURE SUMMARY--------\n') 94 | for msg in failure_message_list: 95 | self.write(msg) 96 | -------------------------------------------------------------------------------- /conf/browser_os_name_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to generate the cross browser cross platform test run configuration 3 | """ 4 | from . import remote_credentials as conf 5 | #Conf list for local 6 | default_browser = ["chrome"] #default browser for the tests to run against when -B option is not used 7 | local_browsers = ["firefox","chrome"] #local browser list against which tests would run if no -M Y and -B all is used 8 | 9 | 10 | #Conf list for Browserstack/Sauce Labs 11 | #change this depending on your client 12 | 13 | browsers = ["firefox","chrome","safari"] #browsers to generate test run configuration to run on Browserstack/Sauce Labs 14 | firefox_versions = ["57","58"] #firefox versions for the tests to run against on Browserstack/Sauce Labs 15 | chrome_versions = ["64","65"] #chrome versions for the tests to run against on Browserstack/Sauce Labs 16 | safari_versions = ["8"] #safari versions for the tests to run against on Browserstack/Sauce Labs 17 | os_list = ["windows","OS X"] #list of os for the tests to run against on Browserstack/Sauce Labs 18 | windows_versions = ["8","10"] #list of windows versions for the tests to run against on Browserstack/Sauce Labs 19 | os_x_versions = ["yosemite"] #list of os x versions for the tests to run against on Browserstack/Sauce Labs 20 | sauce_labs_os_x_versions = ["10.10"] #Set if running on sauce_labs instead of "yosemite" 21 | default_config_list = [("chrome","65","windows","10")] #default configuration against which the test would run if no -B all option is used 22 | 23 | 24 | def generate_configuration(browsers=browsers,firefox_versions=firefox_versions,chrome_versions=chrome_versions,safari_versions=safari_versions, 25 | os_list=os_list,windows_versions=windows_versions,os_x_versions=os_x_versions): 26 | 27 | "Generate test configuration" 28 | if conf.REMOTE_BROWSER_PLATFORM == 'SL': 29 | os_x_versions = sauce_labs_os_x_versions 30 | test_config = [] 31 | for browser in browsers: 32 | if browser == "firefox": 33 | for firefox_version in firefox_versions: 34 | for os_name in os_list: 35 | if os_name == "windows": 36 | for windows_version in windows_versions: 37 | config = [browser,firefox_version,os_name,windows_version] 38 | test_config.append(tuple(config)) 39 | if os_name == "OS X": 40 | for os_x_version in os_x_versions: 41 | config = [browser,firefox_version,os_name,os_x_version] 42 | test_config.append(tuple(config)) 43 | if browser == "chrome": 44 | for chrome_version in chrome_versions: 45 | for os_name in os_list: 46 | if os_name == "windows": 47 | for windows_version in windows_versions: 48 | config = [browser,chrome_version,os_name,windows_version] 49 | test_config.append(tuple(config)) 50 | if os_name == "OS X": 51 | for os_x_version in os_x_versions: 52 | config = [browser,chrome_version,os_name,os_x_version] 53 | test_config.append(tuple(config)) 54 | if browser == "safari": 55 | for safari_version in safari_versions: 56 | for os_name in os_list: 57 | if os_name == "OS X": 58 | for os_x_version in os_x_versions: 59 | config = [browser,safari_version,os_name,os_x_version] 60 | test_config.append(tuple(config)) 61 | 62 | 63 | 64 | return test_config 65 | 66 | #variable to hold the configuration that can be imported in the conftest.py file 67 | cross_browser_cross_platform_config = generate_configuration() 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /utils/BrowserStack_Library.py: -------------------------------------------------------------------------------- 1 | """ 2 | First version of a library to interact with BrowserStack's artifacts. 3 | 4 | For now, this is useful for: 5 | a) Obtaining the session URL 6 | b) Obtaining URLs of screenshots 7 | 8 | To do: 9 | a) Handle expired sessions better 10 | """ 11 | 12 | import os,requests,sys 13 | from conf import remote_credentials as remote_credentials 14 | 15 | 16 | class BrowserStack_Library(): 17 | "BrowserStack library to interact with BrowserStack artifacts" 18 | def __init__(self): 19 | "Constructor for the BrowserStack library" 20 | self.browserstack_url = "https://www.browserstack.com/automate/" 21 | self.auth = self.get_auth() 22 | 23 | 24 | def get_auth(self): 25 | "Set up the auth object for the Requests library" 26 | USERNAME = remote_credentials.USERNAME 27 | PASSWORD = remote_credentials.ACCESS_KEY 28 | auth = (USERNAME,PASSWORD) 29 | 30 | return auth 31 | 32 | 33 | def get_build_id(self): 34 | "Get the build ID" 35 | self.build_url = self.browserstack_url + "builds.json" 36 | builds = requests.get(self.build_url, auth=self.auth).json() 37 | build_id = builds[0]['automation_build']['hashed_id'] 38 | 39 | return build_id 40 | 41 | 42 | def get_sessions(self): 43 | "Get a JSON object with all the sessions" 44 | build_id = self.get_build_id() 45 | sessions= requests.get(self.browserstack_url + 'builds/%s/sessions.json'%build_id, auth=self.auth).json() 46 | 47 | return sessions 48 | 49 | 50 | def get_active_session_id(self): 51 | "Return the session ID of the first active session" 52 | session_id = None 53 | sessions = self.get_sessions() 54 | for session in sessions: 55 | #Get session id of the first session with status = running 56 | if session['automation_session']['status']=='running': 57 | session_id = session['automation_session']['hashed_id'] 58 | break 59 | 60 | return session_id 61 | 62 | 63 | def get_session_url(self): 64 | "Get the session URL" 65 | build_id = self.get_build_id() 66 | session_id = self.get_active_session_id() 67 | session_url = self.browserstack_url + 'builds/%s/sessions/%s'%(build_id,session_id) 68 | 69 | return session_url 70 | 71 | 72 | def get_session_logs(self): 73 | "Return the session log in text format" 74 | build_id = self.get_build_id() 75 | session_id = self.get_active_session_id() 76 | session_log = requests.get(self.browserstack_url + 'builds/%s/sessions/%s/logs'%(build_id,session_id),auth=self.auth).text 77 | 78 | return session_log 79 | 80 | 81 | def get_latest_screenshot_url(self): 82 | "Get the URL of the latest screenshot" 83 | session_log = self.get_session_logs() 84 | 85 | #Process the text to locate the URL of the last screenshot 86 | #Extract the https://s2.amazonaws from example lines: 87 | #2016-2-9 4:42:39:52 RESPONSE {"state":"success","sessionId":"f77e1de6e4f42a72e6a6ecfd80ed07b95036ca35","hCode":29018101,"value":"https://s3.amazonaws.com/testautomation/f77e1de6e4f42a72e6a6ecfd80ed07b95036ca35/screenshot-selenium-b14d4ec62a.png","class":"org.openqa.selenium.remote.Response","status":0} 88 | #[2016-2-9 4:42:45:892] REQUEST [[2016-2-9 4:42:45:892]] GET /session/f77e1de6e4f42a72e6a6ecfd80ed07b95036ca35/title {} 89 | #2016-2-9 4:42:45:957 RESPONSE {"state":"success","sessionId":"f77e1de6e4f42a72e6a6ecfd80ed07b95036ca35","hCode":19687124,"value":"New Member Registration & Signup - Chess.com","class":"org.openqa.selenium.remote.Response","status":0} 90 | 91 | screenshot_request = session_log.split('screenshot {}')[-1] 92 | response_result = screenshot_request.split('REQUEST')[0] 93 | image_url = response_result.split('https://')[-1] 94 | image_url = image_url.split('.png')[0] 95 | screenshot_url = 'https://' + image_url + '.png' 96 | 97 | return screenshot_url 98 | -------------------------------------------------------------------------------- /utils/Image_Compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare images 3 | * Compare two images(actual and expected) smartly and generate a resultant image 4 | * Get the sum of colors in an image 5 | """ 6 | from PIL import Image, ImageChops 7 | import math,operator,os 8 | 9 | def rmsdiff(im1,im2): 10 | "Calculate the root-mean-square difference between two images" 11 | 12 | h = ImageChops.difference(im1, im2).histogram() 13 | # calculate rms 14 | return math.sqrt(sum(h*(i**2) for i, h in enumerate(h)) / (float(im1.size[0]) * im1.size[1])) 15 | 16 | 17 | def is_equal(img_actual,img_expected,result): 18 | "Returns true if the images are identical(all pixels in the difference image are zero)" 19 | result_flag = False 20 | 21 | if not os.path.exists(img_actual): 22 | print('Could not locate the generated image: %s'%img_actual) 23 | 24 | if not os.path.exists(img_expected): 25 | print('Could not locate the baseline image: %s'%img_expected) 26 | 27 | if os.path.exists(img_actual) and os.path.exists(img_expected): 28 | actual = Image.open(img_actual) 29 | expected = Image.open(img_expected) 30 | result_image = ImageChops.difference(actual,expected) 31 | color_matrix = ([0] + ([255] * 255)) 32 | result_image = result_image.convert('L') 33 | result_image = result_image.point(color_matrix) 34 | result_image.save(result)#Save the result image 35 | 36 | if (ImageChops.difference(actual,expected).getbbox() is None): 37 | result_flag = True 38 | else: 39 | #Let's do some interesting processing now 40 | result_flag = analyze_difference_smartly(result) 41 | if result_flag is False: 42 | print("Since there is a difference in pixel value of both images, we are checking the threshold value to pass the images with minor difference") 43 | #Now with threshhold! 44 | result_flag = True if rmsdiff(actual,expected) < 958 else False 45 | #For temporary debug purposes 46 | print('RMS diff score: ',rmsdiff(actual,expected)) 47 | 48 | return result_flag 49 | 50 | 51 | def analyze_difference_smartly(img): 52 | "Make an evaluation of a difference image" 53 | result_flag = False 54 | if not os.path.exists(img): 55 | print('Could not locate the image to analyze the difference smartly: %s'%img) 56 | else: 57 | my_image = Image.open(img) 58 | #Not an ideal line, but we dont have any enormous images 59 | pixels = list(my_image.getdata()) 60 | pixels = [1 for x in pixels if x!=0] 61 | num_different_pixels = sum(pixels) 62 | print('Number of different pixels in the result image: %d'%num_different_pixels) 63 | #Rule 1: If the number of different pixels is <10, then pass the image 64 | #This is relatively safe since all changes to objects will be more than 10 different pixels 65 | if num_different_pixels < 10: 66 | result_flag = True 67 | 68 | return result_flag 69 | 70 | 71 | def get_color_sum(img): 72 | "Get the sum of colors in an image" 73 | sum_color_pixels = -1 74 | if not os.path.exists(img): 75 | print('Could not locate the image to sum the colors: %s'%actual) 76 | else: 77 | my_image = Image.open(img) 78 | color_matrix = ([0] + ([255] * 255)) 79 | my_image = my_image.convert('L') 80 | my_image = my_image.point(color_matrix) 81 | #Not an ideal line, but we don't have any enormous images 82 | pixels = list(my_image.getdata()) 83 | sum_color_pixels = sum(pixels) 84 | print('Sum of colors in the image %s is %d'%(img,sum_color_pixels)) 85 | 86 | return sum_color_pixels 87 | 88 | 89 | #--START OF SCRIPT 90 | if __name__=='__main__': 91 | # Please update below img1, img2, result_img values before running this script 92 | img1 = r'Add path of first image' 93 | img2 = r'Add path of second image' 94 | result_img= r'Add path of result image' #please add path along with resultant image name which you want 95 | 96 | # Compare images and generate a resultant difference image 97 | result_flag = is_equal(img1,img2,result_img) 98 | if (result_flag == True): 99 | print("Both images are matching") 100 | else: 101 | print("Images are not matching") 102 | 103 | # Get the sum of colors in an image 104 | get_color_sum(img1) 105 | -------------------------------------------------------------------------------- /utils/gmail/mailbox.py: -------------------------------------------------------------------------------- 1 | from .message import Message 2 | from .utf import encode as encode_utf7, decode as decode_utf7 3 | 4 | 5 | class Mailbox(): 6 | 7 | def __init__(self, gmail, name="INBOX"): 8 | self.name = name 9 | self.gmail = gmail 10 | self.date_format = "%d-%b-%Y" 11 | self.messages = {} 12 | 13 | @property 14 | def external_name(self): 15 | if "external_name" not in vars(self): 16 | vars(self)["external_name"] = encode_utf7(self.name) 17 | return vars(self)["external_name"] 18 | 19 | @external_name.setter 20 | def external_name(self, value): 21 | if "external_name" in vars(self): 22 | del vars(self)["external_name"] 23 | self.name = decode_utf7(value) 24 | 25 | def mail(self, prefetch=False, **kwargs): 26 | search = ['ALL'] 27 | 28 | kwargs.get('read') and search.append('SEEN') 29 | kwargs.get('unread') and search.append('UNSEEN') 30 | 31 | kwargs.get('starred') and search.append('FLAGGED') 32 | kwargs.get('unstarred') and search.append('UNFLAGGED') 33 | 34 | kwargs.get('deleted') and search.append('DELETED') 35 | kwargs.get('undeleted') and search.append('UNDELETED') 36 | 37 | kwargs.get('draft') and search.append('DRAFT') 38 | kwargs.get('undraft') and search.append('UNDRAFT') 39 | 40 | kwargs.get('before') and search.extend(['BEFORE', kwargs.get('before').strftime(self.date_format)]) 41 | kwargs.get('after') and search.extend(['SINCE', kwargs.get('after').strftime(self.date_format)]) 42 | kwargs.get('on') and search.extend(['ON', kwargs.get('on').strftime(self.date_format)]) 43 | 44 | kwargs.get('header') and search.extend(['HEADER', kwargs.get('header')[0], kwargs.get('header')[1]]) 45 | 46 | kwargs.get('sender') and search.extend(['FROM', kwargs.get('sender')]) 47 | kwargs.get('fr') and search.extend(['FROM', kwargs.get('fr')]) 48 | kwargs.get('to') and search.extend(['TO', kwargs.get('to')]) 49 | kwargs.get('cc') and search.extend(['CC', kwargs.get('cc')]) 50 | 51 | kwargs.get('subject') and search.extend(['SUBJECT', kwargs.get('subject')]) 52 | kwargs.get('body') and search.extend(['BODY', kwargs.get('body')]) 53 | 54 | kwargs.get('label') and search.extend(['X-GM-LABELS', kwargs.get('label')]) 55 | kwargs.get('attachment') and search.extend(['HAS', 'attachment']) 56 | 57 | kwargs.get('query') and search.extend([kwargs.get('query')]) 58 | 59 | emails = [] 60 | # print search 61 | response, data = self.gmail.imap.uid('SEARCH', *search) 62 | if response == 'OK': 63 | uids = filter(None, data[0].split(' ')) # filter out empty strings 64 | 65 | for uid in uids: 66 | if not self.messages.get(uid): 67 | self.messages[uid] = Message(self, uid) 68 | emails.append(self.messages[uid]) 69 | 70 | if prefetch and emails: 71 | messages_dict = {} 72 | for email in emails: 73 | messages_dict[email.uid] = email 74 | self.messages.update(self.gmail.fetch_multiple_messages(messages_dict)) 75 | 76 | return emails 77 | 78 | # WORK IN PROGRESS. NOT FOR ACTUAL USE 79 | def threads(self, prefetch=False, **kwargs): 80 | emails = [] 81 | response, data = self.gmail.imap.uid('SEARCH', 'ALL') 82 | if response == 'OK': 83 | uids = data[0].split(' ') 84 | 85 | 86 | for uid in uids: 87 | if not self.messages.get(uid): 88 | self.messages[uid] = Message(self, uid) 89 | emails.append(self.messages[uid]) 90 | 91 | if prefetch: 92 | fetch_str = ','.join(uids) 93 | response, results = self.gmail.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 94 | for index in xrange(len(results) - 1): 95 | raw_message = results[index] 96 | if re.search(r'UID (\d+)', raw_message[0]): 97 | uid = re.search(r'UID (\d+)', raw_message[0]).groups(1)[0] 98 | self.messages[uid].parse(raw_message) 99 | 100 | return emails 101 | 102 | def count(self, **kwargs): 103 | return len(self.mail(**kwargs)) 104 | 105 | def cached_messages(self): 106 | return self.messages 107 | -------------------------------------------------------------------------------- /conf/copy_framework_template_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This conf file would have the relative paths of the files & folders. 3 | """ 4 | import os,sys 5 | 6 | #dst_folder will be Myntra 7 | #Files from src: 8 | src_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','__init__.py')) 9 | src_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','conftest.py')) 10 | src_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','Readme.md')) 11 | src_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','Requirements.txt')) 12 | src_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','setup.cfg')) 13 | 14 | #src file list: 15 | src_files_list = [src_file1,src_file2,src_file3,src_file4,src_file5] 16 | 17 | #destination folder for which user has to mention. This folder should be created by user. 18 | #dst_folder = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra')) 19 | 20 | #CONF 21 | #files from src conf: 22 | src_conf_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'testrailenv_conf.py')) 23 | src_conf_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'remote_credentials.py')) 24 | src_conf_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'browser_os_name_conf.py')) 25 | src_conf_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'__init__.py')) 26 | 27 | #src Conf file list: 28 | src_conf_files_list = [src_conf_file1,src_conf_file2,src_conf_file3,src_conf_file4] 29 | 30 | #destination folder for conf: 31 | dst_folder_conf = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','conf')) 32 | 33 | #Page_Objects 34 | #files from src page_objects: 35 | src_page_objects_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','Base_Page.py')) 36 | src_page_objects_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','DriverFactory.py')) 37 | src_page_objects_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','PageFactory.py')) 38 | src_page_objects_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','__init__.py')) 39 | 40 | #src page_objects file list: 41 | src_page_objects_files_list = [src_page_objects_file1,src_page_objects_file2,src_page_objects_file3,src_page_objects_file4] 42 | 43 | #destination folder for page_objects: 44 | dst_folder_page_objects = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','page_objects')) 45 | 46 | #Utils 47 | 48 | src_folder_utils = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils')) 49 | dst_folder_utils = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils')) 50 | 51 | #files from src Utils: 52 | src_utils_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','Base_Logging.py')) 53 | src_utils_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','BrowserStack_Library.py')) 54 | src_utils_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','Option_Parser.py')) 55 | src_utils_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','setup_testrail.py')) 56 | src_utils_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','Test_Rail.py')) 57 | src_utils_file6 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','Test_Runner_Class.py')) 58 | src_utils_file7 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','testrail.py')) 59 | src_utils_file8 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','utils','Wrapit.py')) 60 | 61 | #files for dst Utils: 62 | #dst_utils_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','Base_Logging.py')) 63 | #dst_utils_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','BrowserStack_Library.py')) 64 | #dst_utils_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','Option_Parser.py')) 65 | #dst_utils_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','setup_testrail.py')) 66 | #dst_utils_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','Test_Rail.py')) 67 | #dst_utils_file6 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','Test_Runner_Class.py')) 68 | #dst_utils_file7 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','testrail.py')) 69 | #dst_utils_file8 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','..','..','clients','Myntra','utils','Wrapit.py')) 70 | 71 | #src utils file list: 72 | src_utils_files_list = [src_utils_file1,src_utils_file2,src_utils_file3,src_utils_file4,src_utils_file5,src_utils_file6,src_utils_file7,src_utils_file8] 73 | 74 | #dst utils file list: 75 | #dst_utils_files_list = [dst_utils_file1,dst_utils_file2,dst_utils_file3,dst_utils_file4,dst_utils_file5,dst_utils_file6,dst_utils_file7,dst_utils_file8] 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/test_e2e_purchase_product.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a broadstack test for Weather Shopper. 3 | 4 | This test will: 5 | a) visit the main page 6 | b) get the temperature 7 | c) based on temperature choose to buy sunscreen or moisturizer 8 | d) add products based on some specified logic 9 | e) verify the cart 10 | f) make a payment 11 | """ 12 | import os,sys,time 13 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | from page_objects.PageFactory import PageFactory 15 | from utils.Option_Parser import Option_Parser 16 | import conf.e2e_weather_shopper_conf as conf 17 | 18 | def test_e2e_weather_shopper(base_url,browser,browser_version,os_version,os_name,remote_flag,testrail_flag,tesults_flag,test_run_id,remote_project_name,remote_build_name): 19 | 20 | "Run the test" 21 | try: 22 | #Initalize flags for tests summary 23 | expected_pass = 0 24 | actual_pass = -1 25 | 26 | #Create a test object and fill the example form. 27 | test_obj = PageFactory.get_page_object("Main Page",base_url=base_url) 28 | 29 | #Setup and register a driver 30 | start_time = int(time.time()) #Set start_time with current time 31 | test_obj.register_driver(remote_flag,os_name,os_version,browser,browser_version,remote_project_name,remote_build_name) 32 | 33 | #Read the temperature 34 | temperature = test_obj.get_temperature() 35 | result_flag = False 36 | if type(temperature) == int: 37 | result_flag = True 38 | test_obj.log_result(result_flag, 39 | positive="Obtained the temperature from the landing page", 40 | negative="Could not to parse the temperature on the landing page.", 41 | level="critical") 42 | 43 | #Choose the right product type 44 | product_type = "" 45 | if temperature <= 18: 46 | product_type = "moisturizers" 47 | if temperature >= 34: 48 | product_type = "sunscreens" 49 | result_flag = test_obj.click_buy_button(product_type) 50 | test_obj.log_result(result_flag, 51 | positive="Landed on the %s page after clicking the buy button"%product_type, 52 | negative="Could not land on the %s page after clicking the buy button"%product_type, 53 | level="critical") 54 | 55 | #Add products 56 | product_filter_list = conf.PURCHASE_LOGIC[product_type] 57 | product_list = [] 58 | for filter_condition in product_filter_list: 59 | cheapest_product = test_obj.get_minimum_priced_product(filter_condition) 60 | product_list.append(cheapest_product) 61 | result_flag = test_obj.add_product(cheapest_product.name) 62 | test_obj.log_result(result_flag, 63 | positive="Added the cheapest product '%s' with '%s'"%(cheapest_product.name,filter_condition), 64 | negative="Could not add the cheapest product '%s' with '%s'"%(cheapest_product.name,filter_condition)) 65 | 66 | #Go to the cart 67 | result_flag = test_obj.go_to_cart() 68 | test_obj.log_result(result_flag, 69 | positive="Automation is now on the cart page", 70 | negative="Automation is not on the cart page", 71 | level="critical") 72 | 73 | #Verify the products displayed on the cart page 74 | result_flag = test_obj.verify_cart(product_list) 75 | test_obj.log_result(result_flag, 76 | positive="Something wrong with the cart. The log messages above will have the details", 77 | negative="Something wrong with the cart. The log messages above will have the details", 78 | level="critical") 79 | 80 | #Print out the results 81 | test_obj.write_test_summary() 82 | 83 | #Teardown 84 | test_obj.wait(3) 85 | expected_pass = test_obj.result_counter 86 | actual_pass = test_obj.past_counter 87 | test_obj.teardown() 88 | 89 | except Exception as e: 90 | print("Exception when trying to run test:%s"%__file__) 91 | print("Python says:%s"%repr(e)) 92 | 93 | assert expected_pass == actual_pass, "Test failed: %s"%__file__ 94 | 95 | 96 | #---START OF SCRIPT 97 | if __name__=='__main__': 98 | print("Start of %s"%__file__) 99 | #Creating an instance of the class 100 | options_obj = Option_Parser() 101 | options = options_obj.get_options() 102 | 103 | #Run the test only if the options provided are valid 104 | if options_obj.check_options(options): 105 | test_e2e_weather_shopper(base_url=options.url, 106 | browser=options.browser, 107 | browser_version=options.browser_version, 108 | os_version=options.os_version, 109 | os_name=options.os_name, 110 | remote_flag=options.remote_flag, 111 | testrail_flag=options.testrail_flag, 112 | tesults_flag=options.tesults_flag, 113 | test_run_id=options.test_run_id, 114 | remote_project_name=options.remote_project_name, 115 | remote_build_name=options.remote_build_name) 116 | else: 117 | print('ERROR: Received incorrect comand line input arguments') 118 | print(option_obj.print_usage()) -------------------------------------------------------------------------------- /utils/Test_Runner_Class.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Runner class. Lets you setup testrail and run a bunch of tests one after the other 3 | """ 4 | 5 | import os,subprocess 6 | 7 | class Test_Runner_Class: 8 | "Test Runner class" 9 | def __init__(self,base_url='http://qxf2.com',testrail_flag='N',browserstack_flag='N',os_name='Windows',os_version='7',browser='firefox',browser_version='33'): 10 | "Constructor" 11 | self.python_executable = "python" 12 | self.util_directory = os.path.abspath((os.path.dirname(__file__))) 13 | self.test_directory = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','tests')) 14 | self.setup_testrail_script = os.path.join(self.util_directory,"setup_testrail.py") 15 | self.reset(base_url=base_url, 16 | testrail_flag=testrail_flag, 17 | browserstack_flag=browserstack_flag, 18 | os_name=os_name, 19 | os_version=os_version, 20 | browser=browser, 21 | browser_version=browser_version) 22 | 23 | def check_file_exists(self,file_path): 24 | "Check if the config file exists and is a file" 25 | file_exist_flag = True 26 | if os.path.exists(file_path): 27 | if not os.path.isfile(file_path): 28 | print('\n****') 29 | print('Script file provided is not a file: ') 30 | print(file_path) 31 | print('****') 32 | file_exist_flag = False 33 | else: 34 | print('\n****') 35 | print('Unable to locate the provided script file: ') 36 | print(file_path) 37 | print('****') 38 | conf_flag = False 39 | 40 | return file_exist_flag 41 | 42 | 43 | def reset(self,base_url=None,testrail_flag=None,browserstack_flag=None,os_name=None,os_version=None,browser=None,browser_version=None): 44 | "Reset the private variables" 45 | if base_url is not None: 46 | self.base_url = base_url 47 | if testrail_flag is not None: 48 | self.testrail_flag = testrail_flag 49 | if browserstack_flag is not None: 50 | self.browserstack_flag = browserstack_flag 51 | if os_name is not None: 52 | self.os_name = os_name 53 | if os_version is not None: 54 | self.os_version = os_version 55 | if browser is not None: 56 | self.browser = browser 57 | if browser_version is not None: 58 | self.browser_version = browser_version 59 | 60 | 61 | 62 | def run_test(self,test_name): 63 | "Run the test script with the given command line options" 64 | testscript_args_list = self.setup_test_script_args_list(test_name) 65 | self.run_script(testscript_args_list) 66 | 67 | 68 | def run_setup_testrail(self,test_name=None,test_run_name='',case_ids_list=None,name_override_flag=True): 69 | "Run the setup_testrail with given command line options" 70 | if self.testrail_flag.lower() == 'y': 71 | testrail_args_list = self.setup_testrail_args_list(test_name,test_run_name,case_ids_list,name_override_flag) 72 | self.run_script(testrail_args_list) 73 | 74 | 75 | def run_script(self,args_list): 76 | "Run the script on command line with given args_list" 77 | print("\nWill be running the following script:") 78 | print(' '.join(args_list)) 79 | print("Starting..") 80 | subprocess.call(args_list,shell=True) 81 | print("Done!") 82 | 83 | 84 | def setup_testrail_args_list(self,test_name=None,test_run_name='',case_ids_list=None,name_override_flag=True): 85 | "Convert the command line arguments into list for setup_testrail.py" 86 | args_list = [] 87 | #python setup_testrail.py -r test_run_name -d test_run_description 88 | if self.check_file_exists(self.setup_testrail_script): 89 | args_list = [self.python_executable,self.setup_testrail_script] 90 | if test_run_name != '': 91 | args_list.append("-r") 92 | args_list.append(test_run_name) 93 | if test_name is not None: 94 | args_list.append("-d") 95 | args_list.append(test_name) 96 | if name_override_flag is False: 97 | args_list.append("-n") 98 | args_list.append("N") 99 | if case_ids_list is not None: 100 | args_list.append("-c") 101 | case_ids_list = ','.join(case_ids_list) 102 | args_list.append(case_ids_list) 103 | 104 | return args_list 105 | 106 | 107 | def setup_test_script_args_list(self,test_name): 108 | "convert the command line arguments into list for test script" 109 | args_list = [] 110 | #python test_script.py -x Y 111 | test_script_name = test_name + ".py" 112 | test_script_name = os.path.join(self.test_directory,test_script_name) 113 | if self.check_file_exists(test_script_name): 114 | args_list = [self.python_executable,test_script_name,"-b",self.browser,"-u",self.base_url,"-x",self.testrail_flag,"-s",self.browserstack_flag,"-o",self.os_version,"-v",self.browser_version,"-p",self.os_name] 115 | 116 | return args_list 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /utils/gmail/gmail.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import re 3 | import imaplib 4 | 5 | from .mailbox import Mailbox 6 | from .utf import encode as encode_utf7, decode as decode_utf7 7 | from .exceptions import * 8 | 9 | class Gmail(): 10 | # GMail IMAP defaults 11 | GMAIL_IMAP_HOST = 'imap.gmail.com' 12 | GMAIL_IMAP_PORT = 993 13 | 14 | # GMail SMTP defaults 15 | # TODO: implement SMTP functions 16 | GMAIL_SMTP_HOST = "smtp.gmail.com" 17 | GMAIL_SMTP_PORT = 587 18 | 19 | def __init__(self): 20 | self.username = None 21 | self.password = None 22 | self.access_token = None 23 | 24 | self.imap = None 25 | self.smtp = None 26 | self.logged_in = False 27 | self.mailboxes = {} 28 | self.current_mailbox = None 29 | 30 | 31 | # self.connect() 32 | 33 | 34 | def connect(self, raise_errors=True): 35 | # try: 36 | # self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT) 37 | # except socket.error: 38 | # if raise_errors: 39 | # raise Exception('Connection failure.') 40 | # self.imap = None 41 | 42 | self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT) 43 | 44 | # self.smtp = smtplib.SMTP(self.server,self.port) 45 | # self.smtp.set_debuglevel(self.debug) 46 | # self.smtp.ehlo() 47 | # self.smtp.starttls() 48 | # self.smtp.ehlo() 49 | 50 | return self.imap 51 | 52 | 53 | def fetch_mailboxes(self): 54 | response, mailbox_list = self.imap.list() 55 | if response == 'OK': 56 | for mailbox in mailbox_list: 57 | mailbox_name = mailbox.split('"/"')[-1].replace('"', '').strip() 58 | mailbox = Mailbox(self) 59 | mailbox.external_name = mailbox_name 60 | self.mailboxes[mailbox_name] = mailbox 61 | 62 | def use_mailbox(self, mailbox): 63 | if mailbox: 64 | self.imap.select(mailbox) 65 | self.current_mailbox = mailbox 66 | 67 | def mailbox(self, mailbox_name): 68 | if mailbox_name not in self.mailboxes: 69 | mailbox_name = encode_utf7(mailbox_name) 70 | mailbox = self.mailboxes.get(mailbox_name) 71 | 72 | if mailbox and not self.current_mailbox == mailbox_name: 73 | self.use_mailbox(mailbox_name) 74 | 75 | return mailbox 76 | 77 | def create_mailbox(self, mailbox_name): 78 | mailbox = self.mailboxes.get(mailbox_name) 79 | if not mailbox: 80 | self.imap.create(mailbox_name) 81 | mailbox = Mailbox(self, mailbox_name) 82 | self.mailboxes[mailbox_name] = mailbox 83 | 84 | return mailbox 85 | 86 | def delete_mailbox(self, mailbox_name): 87 | mailbox = self.mailboxes.get(mailbox_name) 88 | if mailbox: 89 | self.imap.delete(mailbox_name) 90 | del self.mailboxes[mailbox_name] 91 | 92 | 93 | 94 | def login(self, username, password): 95 | self.username = username 96 | self.password = password 97 | 98 | if not self.imap: 99 | self.connect() 100 | 101 | try: 102 | imap_login = self.imap.login(self.username, self.password) 103 | self.logged_in = (imap_login and imap_login[0] == 'OK') 104 | if self.logged_in: 105 | self.fetch_mailboxes() 106 | except imaplib.IMAP4.error: 107 | raise AuthenticationError 108 | 109 | 110 | # smtp_login(username, password) 111 | 112 | return self.logged_in 113 | 114 | def authenticate(self, username, access_token): 115 | self.username = username 116 | self.access_token = access_token 117 | 118 | if not self.imap: 119 | self.connect() 120 | 121 | try: 122 | auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token) 123 | imap_auth = self.imap.authenticate('XOAUTH2', lambda x: auth_string) 124 | self.logged_in = (imap_auth and imap_auth[0] == 'OK') 125 | if self.logged_in: 126 | self.fetch_mailboxes() 127 | except imaplib.IMAP4.error: 128 | raise AuthenticationError 129 | 130 | return self.logged_in 131 | 132 | def logout(self): 133 | self.imap.logout() 134 | self.logged_in = False 135 | 136 | 137 | def label(self, label_name): 138 | return self.mailbox(label_name) 139 | 140 | def find(self, mailbox_name="[Gmail]/All Mail", **kwargs): 141 | box = self.mailbox(mailbox_name) 142 | return box.mail(**kwargs) 143 | 144 | 145 | def copy(self, uid, to_mailbox, from_mailbox=None): 146 | if from_mailbox: 147 | self.use_mailbox(from_mailbox) 148 | self.imap.uid('COPY', uid, to_mailbox) 149 | 150 | def fetch_multiple_messages(self, messages): 151 | fetch_str = ','.join(messages.keys()) 152 | response, results = self.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 153 | for index in xrange(len(results) - 1): 154 | raw_message = results[index] 155 | if re.search(r'UID (\d+)', raw_message[0]): 156 | uid = re.search(r'UID (\d+)', raw_message[0]).groups(1)[0] 157 | messages[uid].parse(raw_message) 158 | 159 | return messages 160 | 161 | 162 | def labels(self, require_unicode=False): 163 | keys = self.mailboxes.keys() 164 | if require_unicode: 165 | keys = [decode_utf7(key) for key in keys] 166 | return keys 167 | 168 | def inbox(self): 169 | return self.mailbox("INBOX") 170 | 171 | def spam(self): 172 | return self.mailbox("[Gmail]/Spam") 173 | 174 | def starred(self): 175 | return self.mailbox("[Gmail]/Starred") 176 | 177 | def all_mail(self): 178 | return self.mailbox("[Gmail]/All Mail") 179 | 180 | def sent_mail(self): 181 | return self.mailbox("[Gmail]/Sent Mail") 182 | 183 | def important(self): 184 | return self.mailbox("[Gmail]/Important") 185 | 186 | def mail_domain(self): 187 | return self.username.split('@')[-1] 188 | -------------------------------------------------------------------------------- /page_objects/Cart_Page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module models the cart page on weather shopper 3 | URL: /cart 4 | """ 5 | 6 | from .Base_Page import Base_Page 7 | from utils.Wrapit import Wrapit 8 | import conf.locators_conf as locators 9 | 10 | class Cart_Page(Base_Page): 11 | "This class models the cart page" 12 | 13 | CART_ROW = locators.CART_ROW 14 | CART_ROW_COLUMN = locators.CART_ROW_COLUMN 15 | CART_TOTAL = locators.CART_TOTAL 16 | COL_NAME = 0 17 | COL_PRICE = 1 18 | 19 | def start(self): 20 | "Override the start method of base" 21 | url = "cart" 22 | self.open(url) 23 | 24 | def process_item(self,item): 25 | "Process the given item" 26 | #Convert the price to an int 27 | try: 28 | item[self.COL_PRICE] = int(item[self.COL_PRICE]) 29 | except Exception as e: 30 | self.write("Unable to convert the string %s into a number"%item[self.COL_PRICE]) 31 | 32 | return item 33 | 34 | def get_cart_items(self): 35 | "Get all the cart items as a list of [name,price] lists" 36 | cart_items = [] 37 | row_elements = self.get_elements(self.CART_ROW) 38 | for index,row in enumerate(row_elements): 39 | column_elements = self.get_elements(self.CART_ROW_COLUMN%(index+1)) 40 | item = [] 41 | for col in column_elements: 42 | text = self.get_dom_text(col) 43 | item.append(text.decode('ascii')) 44 | item = self.process_item(item) 45 | cart_items.append(item) 46 | 47 | return cart_items 48 | 49 | def verify_cart_size(self,expected_cart,actual_cart): 50 | "Make sure expected and actual carts have the same number of items" 51 | result_flag = False 52 | if len(expected_cart) == len(actual_cart): 53 | result_flag = True 54 | self.conditional_write(result_flag, 55 | positive="The expected cart and actual cart have the same number of items: %d"%len(expected_cart),negative="The expected cart has %d items while the actual cart has %d items"%(len(expected_cart),len(actual_cart))) 56 | 57 | return result_flag 58 | 59 | def verify_extra_items(self,expected_cart,actual_cart): 60 | "Items which exist in actual but not in expected" 61 | item_match_flag = False 62 | for item in actual_cart: 63 | #Does the item exist in the product list 64 | found_flag = False 65 | price_match_flag = False 66 | expected_price = 0 67 | for product in expected_cart: 68 | if product.name == item[self.COL_NAME]: 69 | found_flag = True 70 | if product.price == item[self.COL_PRICE]: 71 | price_match_flag = True 72 | else: 73 | expected_price = product.price 74 | break 75 | self.conditional_write(found_flag, 76 | positive="Found the expected item '%s' in the cart"%item[self.COL_NAME], 77 | negative="Found an unexpected item '%s' in the cart"%item[self.COL_NAME]) 78 | 79 | self.conditional_write(price_match_flag, 80 | positive="... the expected price matched to %d"%item[self.COL_PRICE], 81 | negative="... the expected price did not match. Expected: %d but Obtained: %d"%(expected_price,item[self.COL_PRICE])) 82 | 83 | item_match_flag &= found_flag and price_match_flag 84 | 85 | return item_match_flag 86 | 87 | def verify_missing_item(self,expected_cart,actual_cart): 88 | "Verify if expected items are missing from the cart" 89 | item_match_flag = True 90 | for product in expected_cart: 91 | price_match_flag = False 92 | found_flag = False 93 | actual_price = 0 94 | for item in actual_cart: 95 | if product.name == item[self.COL_NAME]: 96 | found_flag = True 97 | if product.price == item[self.COL_PRICE]: 98 | price_match_flag = True 99 | else: 100 | actual_price = item[self.COL_PRICE] 101 | break 102 | item_match_flag &= found_flag and price_match_flag 103 | self.conditional_write(found_flag, 104 | positive="Found the expected item '%s' in the cart"%product.name, 105 | negative="Did not find the expected item '%s' in the cart"%product.name) 106 | self.conditional_write(price_match_flag, 107 | positive="... the expected price matched to %d"%product.price, 108 | negative="... the expected price did not match. Expected: %d but Obtained: %d"%(product.price,actual_price)) 109 | 110 | return item_match_flag 111 | 112 | def get_total_price(self): 113 | "Return the cart total" 114 | actual_price = self.get_text(self.CART_TOTAL) 115 | actual_price = actual_price.decode('ascii') 116 | actual_price = actual_price.split('Rupees')[-1] 117 | try: 118 | actual_price = int(actual_price) 119 | except Exception as e: 120 | self.write("Could not convert '%s' (cart total price) into an integer"%actual_price) 121 | 122 | return actual_price 123 | 124 | def verify_cart_total(self,expected_cart): 125 | "Verify the total in the cart" 126 | expected_total = 0 127 | for product in expected_cart: 128 | expected_total = product.price 129 | actual_total = self.get_total_price() 130 | result_flag = actual_total == expected_total 131 | self.conditional_write(result_flag, 132 | positive="The cart total displayed is correct", 133 | negative="The expected and actual cart totals do not match. Expected: %d, actual: %d"%(expected_total, actual_total)) 134 | 135 | return result_flag 136 | 137 | def verify_cart(self,expected_cart): 138 | "Verify the (name,price) of items in cart and the total" 139 | actual_cart = self.get_cart_items() 140 | result_flag = self.verify_cart_size(expected_cart,actual_cart) 141 | result_flag &= self.verify_extra_items(expected_cart,actual_cart) 142 | if result_flag is False: 143 | result_flag &= self.verify_missing_item(expected_cart,actual_cart) 144 | result_flag &= self.verify_cart_total(expected_cart) 145 | 146 | return result_flag -------------------------------------------------------------------------------- /page_objects/Product_Object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Object models the product page. 3 | """ 4 | 5 | from .Base_Page import Base_Page 6 | import conf.locators_conf as locators 7 | from utils.Wrapit import Wrapit 8 | 9 | class Product(): 10 | "A product class" 11 | def __init__(self,name,price): 12 | "Set up the product with name and price" 13 | self.name = name 14 | self.price = price 15 | 16 | class Product_Object(): 17 | "Page Object for the products object" 18 | PRODUCTS_LIST = locators.PRODUCTS_LIST 19 | ADD_PRODUCT_BUTTON = locators.ADD_PRODUCT_BUTTON 20 | CART_QUANTITY_TEXT = locators.CART_QUANTITY_TEXT 21 | CART_BUTTON = locators.CART_BUTTON 22 | CART_TITLE = locators.CART_TITLE 23 | CART_QUANTITY = 0 24 | PRODUCTS_PER_PAGE = 6 25 | 26 | def convert_str_to_int(self,string, default=100000, expect_fail=False): 27 | "Convert a given string to integer. Return default if you cannot convert" 28 | try: 29 | integer = int(string) 30 | except Exception as e: 31 | if not expect_fail: 32 | self.write("Unable to convert the string %s into a number"%string) 33 | integer = default 34 | 35 | return integer 36 | 37 | @Wrapit._exceptionHandler 38 | def get_product_name(self,product_text): 39 | "Parse the product name from the product text" 40 | name = product_text.split(b"\n")[0].strip() 41 | name = name.decode('ascii') 42 | 43 | return name 44 | 45 | @Wrapit._exceptionHandler 46 | def get_product_price(self,product_text): 47 | "Parse the product price from the product text" 48 | price = product_text.split(b"Price: ")[-1] 49 | price = price.split(b"Rs.")[-1] 50 | price = price.split(b"\n")[0] 51 | price = price.decode('ascii') 52 | price = self.convert_str_to_int(price,default=100000) 53 | 54 | return price 55 | 56 | @Wrapit._screenshot 57 | def get_all_products_on_page(self): 58 | "Get all the products" 59 | result_flag = False 60 | all_products = [] 61 | product_list = self.get_elements(self.PRODUCTS_LIST) 62 | for i,product in enumerate(product_list): 63 | product_text = self.get_dom_text(product) 64 | name = self.get_product_name(product_text) 65 | price = self.get_product_price(product_text) 66 | all_products.append(Product(name, price)) 67 | if self.PRODUCTS_PER_PAGE == len(all_products): 68 | result_flag = True 69 | self.conditional_write(result_flag, 70 | positive="Obtained all %d products from the page"%self.PRODUCTS_PER_PAGE, 71 | negative="Could not obtain all products. Automation got %d products while we expected %d products"%(len(all_products),self.PRODUCTS_PER_PAGE)) 72 | 73 | return all_products 74 | 75 | def print_all_products(self): 76 | "Print out all the products nicely" 77 | all_products = self.get_all_products_on_page() 78 | self.write("Product list is: ") 79 | for product in all_products: 80 | self.write("%s: %d"%(product.name,product.price)) 81 | 82 | def get_minimum_priced_product(self,filter_condition): 83 | "Return the least expensive item based on a filter condition" 84 | minimum_priced_product = None 85 | min_price = 10000000 86 | min_name = '' 87 | all_products = self.get_all_products_on_page() 88 | for product in all_products: 89 | if filter_condition.lower() in product.name.lower(): 90 | if product.price >= min_price: 91 | minimum_priced_product = product 92 | min_price = product.price 93 | min_name = product.name 94 | result_flag = True if minimum_priced_product is not None else False 95 | self.conditional_write(result_flag, 96 | positive="Min price for product with '%s' is %s with price %d"%(filter_condition,min_name,min_price), 97 | negative="Could not obtain the cheapest product with the filter condition '%s'\nCheck the screenshots to see if there was at least one item that satisfied the filter condition."%filter_condition) 98 | 99 | return minimum_priced_product 100 | 101 | def click_add_product_button(self,product_name): 102 | "Click on the add button corresponding to the name" 103 | result_flag = self.click_element(self.ADD_PRODUCT_BUTTON%product_name) 104 | self.conditional_write(result_flag, 105 | positive="Clicked on the add button to buy: %s"%product_name, 106 | negative="Could not click on the add button to buy: %s"%product_name) 107 | 108 | return result_flag 109 | 110 | @Wrapit._screenshot 111 | def get_current_cart_quantity(self): 112 | "Return the number of items in the cart" 113 | cart_text = self.get_text(self.CART_QUANTITY_TEXT) 114 | cart_quantity = cart_text.split()[0] 115 | cart_quantity = cart_quantity.decode('ascii') 116 | empty_cart_flag = True if self.CART_QUANTITY == 0 else False 117 | cart_quantity = self.convert_str_to_int(cart_quantity, default=0, expect_fail = empty_cart_flag) 118 | self.CART_QUANTITY = cart_quantity 119 | self.conditional_write(True, 120 | positive="The cart currently has %d items"%self.CART_QUANTITY, 121 | negative="") 122 | 123 | def add_product(self,product_name): 124 | "Add the lowest priced product with the filter condition in name" 125 | before_cart_quantity = self.get_current_cart_quantity() 126 | result_flag = self.click_add_product_button(product_name) 127 | after_cart_quantity = self.get_current_cart_quantity() 128 | result_flag &= True if after_cart_quantity - before_cart_quantity == 1 else False 129 | 130 | return result_flag 131 | 132 | @Wrapit._screenshot 133 | def click_cart_button(self): 134 | "Click the cart button" 135 | result_flag = self.click_element(self.CART_BUTTON) 136 | self.conditional_write(result_flag, 137 | positive="Clicked on the cart button", 138 | negative="Could not click on the cart button") 139 | 140 | return result_flag 141 | 142 | @Wrapit._screenshot 143 | def verify_cart_page(self): 144 | "Verify automation is on the cart page" 145 | result_flag = self.smart_wait(5,self.CART_TITLE) 146 | self.conditional_write(result_flag, 147 | positive="Automation is on the Cart page", 148 | negative="Automation is not able to locate the Cart Title. Maybe it is not even on the cart page?") 149 | if result_flag: 150 | self.switch_page("main") 151 | 152 | return result_flag 153 | 154 | def go_to_cart(self): 155 | "Go to the cart page" 156 | result_flag = self.click_cart_button() 157 | result_flag &= self.verify_cart_page() 158 | 159 | return result_flag -------------------------------------------------------------------------------- /utils/setup_testrail.py: -------------------------------------------------------------------------------- 1 | """ 2 | One off utility script to setup TestRail for an automated run 3 | This script can: 4 | a) Add a milestone if it does not exist 5 | b) Add a test run (even without a milestone if needed) 6 | c) Add select test cases to the test run using the setup_testrail.conf file 7 | d) Write out the latest run id to a 'latest_test_run.txt' file 8 | 9 | This script will NOT: 10 | a) Add a project if it does not exist 11 | """ 12 | 13 | import os,ConfigParser,time 14 | from .Test_Rail import Test_Rail 15 | from optparse import OptionParser 16 | 17 | 18 | def check_file_exists(file_path): 19 | #Check if the config file exists and is a file 20 | conf_flag = True 21 | if os.path.exists(file_path): 22 | if not os.path.isfile(file_path): 23 | print('\n****') 24 | print('Config file provided is not a file: ') 25 | print(file_path) 26 | print('****') 27 | conf_flag = False 28 | else: 29 | print('\n****') 30 | print('Unable to locate the provided config file: ') 31 | print(file_path) 32 | print('****') 33 | conf_flag = False 34 | 35 | return conf_flag 36 | 37 | 38 | def check_options(options): 39 | "Check if the command line options are valid" 40 | result_flag = True 41 | if options.test_cases_conf is not None: 42 | result_flag = check_file_exists(os.path.abspath(os.path.join(os.path.dirname(__file__),'..','conf',options.test_cases_conf))) 43 | 44 | return result_flag 45 | 46 | 47 | def save_new_test_run_details(filename,test_run_name,test_run_id): 48 | "Write out latest test run name and id" 49 | fp = open(filename,'w') 50 | fp.write('TEST_RUN_NAME=%s\n'%test_run_name) 51 | fp.write('TEST_RUN_ID=%s\n'%str(test_run_id)) 52 | fp.close() 53 | 54 | 55 | def setup_testrail(project_name='POM DEMO',milestone_name=None,test_run_name=None,test_cases_conf=None,description=None,name_override_flag='N',case_ids_list=None): 56 | "Setup TestRail for an automated run" 57 | #1. Get project id 58 | #2. if milestone_name is not None 59 | # create the milestone if it does not already exist 60 | #3. if test_run_name is not None 61 | # create the test run if it does not already exist 62 | # TO DO: if test_cases_conf is not None -> pass ids as parameters 63 | #4. write out test runid to latest_test_run.txt 64 | conf_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','conf')) 65 | config = ConfigParser.ConfigParser() 66 | tr_obj = Test_Rail() 67 | #1. Get project id 68 | project_id = tr_obj.get_project_id(project_name) 69 | 70 | if project_id is not None: #i.e., the project exists 71 | #2. if milestone_name is not None: 72 | # create the milestone if it does not already exist 73 | if milestone_name is not None: 74 | tr_obj.create_milestone(project_name,milestone_name) 75 | 76 | #3. if test_run_name is not None 77 | # create the test run if it does not already exist 78 | # if test_cases_conf is not None -> pass ids as parameters 79 | if test_run_name is not None: 80 | case_ids = [] 81 | #Set the case ids 82 | if case_ids_list is not None: 83 | #Getting case ids from command line 84 | case_ids = case_ids_list.split(',') 85 | else: 86 | #Getting case ids based on given description(test name) 87 | if description is not None: 88 | if check_file_exists(os.path.join(conf_dir,test_cases_conf)): 89 | config.read(os.path.join(conf_dir,test_cases_conf)) 90 | case_ids = config.get(description,'case_ids') 91 | case_ids = case_ids.split(',') 92 | #Set test_run_name 93 | if name_override_flag.lower() == 'y': 94 | test_run_name = test_run_name + "-" + time.strftime("%d/%m/%Y/%H:%M:%S") + "_for_" 95 | #Use description as test_run_name 96 | if description is None: 97 | test_run_name = test_run_name + "All" 98 | else: 99 | test_run_name = test_run_name + str(description) 100 | tr_obj.create_test_run(project_name,test_run_name,milestone_name=milestone_name,case_ids=case_ids,description=description) 101 | run_id = tr_obj.get_run_id(project_name,test_run_name) 102 | save_new_test_run_details(os.path.join(conf_dir,'latest_test_run.txt'),test_run_name,run_id) 103 | else: 104 | print('Project does not exist: ',project_name) 105 | print('Stopping the script without doing anything.') 106 | 107 | 108 | 109 | #---START OF SCRIPT 110 | if __name__=='__main__': 111 | #This script takes an optional command line argument for the TestRail run id 112 | usage = '\n----\n%prog -p -m -r -t -d \n----\nE.g.: %prog -p "Secure Code Warrior - Test" -m "Pilot NetCetera" -r commit_id -t setup_testrail.conf -d Registration\n---' 113 | parser = OptionParser(usage=usage) 114 | 115 | parser.add_option("-p","--project", 116 | dest="project_name", 117 | default="POM DEMO", 118 | help="Project name") 119 | parser.add_option("-m","--milestone", 120 | dest="milestone_name", 121 | default=None, 122 | help="Milestone name") 123 | parser.add_option("-r","--test_run_name", 124 | dest="test_run_name", 125 | default=None, 126 | help="Test run name") 127 | parser.add_option("-t","--test_cases_conf", 128 | dest="test_cases_conf", 129 | default="setup_testrail.conf", 130 | help="Test cases conf listing test names and ids you want added") 131 | parser.add_option("-d","--test_run_description", 132 | dest="test_run_description", 133 | default=None, 134 | help="The name of the test Registration_Tests/Intro_Run_Tests/Sales_Demo_Tests") 135 | parser.add_option("-n","--name_override_flag", 136 | dest="name_override_flag", 137 | default="Y", 138 | help="Y or N. 'N' if you don't want to override the default test_run_name") 139 | parser.add_option("-c","--case_ids_list", 140 | dest="case_ids_list", 141 | default=None, 142 | help="Pass all case ids with comma separated you want to add in test run") 143 | 144 | (options,args) = parser.parse_args() 145 | 146 | #Run the script only if the options are valid 147 | if check_options(options): 148 | setup_testrail(project_name=options.project_name, 149 | milestone_name=options.milestone_name, 150 | test_run_name=options.test_run_name, 151 | test_cases_conf=options.test_cases_conf, 152 | description=options.test_run_description, 153 | name_override_flag=options.name_override_flag, 154 | case_ids_list=options.case_ids_list) 155 | else: 156 | print('ERROR: Received incorrect input arguments') 157 | print(parser.print_usage()) 158 | 159 | -------------------------------------------------------------------------------- /utils/ssh_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to ssh into a remote server 3 | * Connect to the remote server 4 | * Execute the given command 5 | * Upload a file 6 | * Download a file 7 | """ 8 | 9 | import paramiko 10 | import os,sys,time 11 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | from conf import ssh_conf as conf_file 13 | import socket 14 | 15 | class Ssh_Util: 16 | "Class to connect to remote server" 17 | 18 | def __init__(self): 19 | self.ssh_output = None 20 | self.ssh_error = None 21 | self.client = None 22 | self.host= conf_file.HOST 23 | self.username = conf_file.USERNAME 24 | self.password = conf_file.PASSWORD 25 | self.timeout = float(conf_file.TIMEOUT) 26 | self.commands = conf_file.COMMANDS 27 | self.pkey = conf_file.PKEY 28 | self.port = conf_file.PORT 29 | self.uploadremotefilepath = conf_file.UPLOADREMOTEFILEPATH 30 | self.uploadlocalfilepath = conf_file.UPLOADLOCALFILEPATH 31 | self.downloadremotefilepath = conf_file.DOWNLOADREMOTEFILEPATH 32 | self.downloadlocalfilepath = conf_file.DOWNLOADLOCALFILEPATH 33 | 34 | def connect(self): 35 | "Login to the remote server" 36 | try: 37 | #Paramiko.SSHClient can be used to make connections to the remote server and transfer files 38 | print("Establishing ssh connection...") 39 | self.client = paramiko.SSHClient() 40 | #Parsing an instance of the AutoAddPolicy to set_missing_host_key_policy() changes it to allow any host. 41 | self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 42 | #Connect to the server 43 | if (self.password == ''): 44 | private_key = paramiko.RSAKey.from_private_key_file(self.pkey) 45 | self.client.connect(hostname=self.host, port=self.port, username=self.username,pkey=private_key ,timeout=self.timeout, allow_agent=False, look_for_keys=False) 46 | print("Connected to the server",self.host) 47 | else: 48 | self.client.connect(hostname=self.host, port=self.port,username=self.username,password=self.password,timeout=self.timeout, allow_agent=False, look_for_keys=False) 49 | print("Connected to the server",self.host) 50 | except paramiko.AuthenticationException: 51 | print("Authentication failed, please verify your credentials") 52 | result_flag = False 53 | except paramiko.SSHException as sshException: 54 | print("Could not establish SSH connection: %s" % sshException) 55 | result_flag = False 56 | except socket.timeout as e: 57 | print("Connection timed out") 58 | result_flag = False 59 | except Exception as e: 60 | print('\nException in connecting to the server') 61 | print('PYTHON SAYS:',e) 62 | result_flag = False 63 | self.client.close() 64 | else: 65 | result_flag = True 66 | 67 | return result_flag 68 | 69 | 70 | def execute_command(self,commands): 71 | """Execute a command on the remote host.Return a tuple containing 72 | an integer status and a two strings, the first containing stdout 73 | and the second containing stderr from the command.""" 74 | self.ssh_output = None 75 | result_flag = True 76 | try: 77 | if self.connect(): 78 | for command in commands: 79 | print("Executing command --> {}".format(command)) 80 | stdin, stdout, stderr = self.client.exec_command(command,timeout=10) 81 | self.ssh_output = stdout.read() 82 | self.ssh_error = stderr.read() 83 | if self.ssh_error: 84 | print("Problem occurred while running command:"+ command + " The error is " + self.ssh_error) 85 | result_flag = False 86 | else: 87 | print("Command execution completed successfully",command) 88 | self.client.close() 89 | else: 90 | print("Could not establish SSH connection") 91 | result_flag = False 92 | except socket.timeout as e: 93 | print("Command timed out.", command) 94 | self.client.close() 95 | result_flag = False 96 | except paramiko.SSHException: 97 | print("Failed to execute the command!",command) 98 | self.client.close() 99 | result_flag = False 100 | 101 | return result_flag 102 | 103 | 104 | def upload_file(self,uploadlocalfilepath,uploadremotefilepath): 105 | "This method uploads the file to remote server" 106 | result_flag = True 107 | try: 108 | if self.connect(): 109 | ftp_client= self.client.open_sftp() 110 | ftp_client.put(uploadlocalfilepath,uploadremotefilepath) 111 | ftp_client.close() 112 | self.client.close() 113 | else: 114 | print("Could not establish SSH connection") 115 | result_flag = False 116 | except Exception as e: 117 | print('\nUnable to upload the file to the remote server',uploadremotefilepath) 118 | print('PYTHON SAYS:',e) 119 | result_flag = False 120 | ftp_client.close() 121 | self.client.close() 122 | 123 | return result_flag 124 | 125 | 126 | def download_file(self,downloadremotefilepath,downloadlocalfilepath): 127 | "This method downloads the file from remote server" 128 | result_flag = True 129 | try: 130 | if self.connect(): 131 | ftp_client= self.client.open_sftp() 132 | ftp_client.get(downloadremotefilepath,downloadlocalfilepath) 133 | ftp_client.close() 134 | self.client.close() 135 | else: 136 | print("Could not establish SSH connection") 137 | result_flag = False 138 | except Exception as e: 139 | print('\nUnable to download the file from the remote server',downloadremotefilepath) 140 | print('PYTHON SAYS:',e) 141 | result_flag = False 142 | ftp_client.close() 143 | self.client.close() 144 | 145 | return result_flag 146 | 147 | 148 | #---USAGE EXAMPLES 149 | if __name__=='__main__': 150 | print("Start of %s"%__file__) 151 | 152 | #Initialize the ssh object 153 | ssh_obj = Ssh_Util() 154 | 155 | #Sample code to execute commands 156 | if ssh_obj.execute_command(ssh_obj.commands) is True: 157 | print("Commands executed successfully\n") 158 | else: 159 | print ("Unable to execute the commands" ) 160 | 161 | #Sample code to upload a file to the server 162 | if ssh_obj.upload_file(ssh_obj.uploadlocalfilepath,ssh_obj.uploadremotefilepath) is True: 163 | print ("File uploaded successfully", ssh_obj.uploadremotefilepath) 164 | else: 165 | print ("Failed to upload the file") 166 | 167 | #Sample code to download a file from the server 168 | if ssh_obj.download_file(ssh_obj.downloadremotefilepath,ssh_obj.downloadlocalfilepath) is True: 169 | print ("File downloaded successfully", ssh_obj.downloadlocalfilepath) 170 | else: 171 | print ("Failed to download the file") 172 | -------------------------------------------------------------------------------- /utils/gmail/message.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import email 3 | import re 4 | import time 5 | import os 6 | from email.header import decode_header, make_header 7 | from imaplib import ParseFlags 8 | 9 | class Message(): 10 | 11 | 12 | def __init__(self, mailbox, uid): 13 | self.uid = uid 14 | self.mailbox = mailbox 15 | self.gmail = mailbox.gmail if mailbox else None 16 | 17 | self.message = None 18 | self.headers = {} 19 | 20 | self.subject = None 21 | self.body = None 22 | self.html = None 23 | 24 | self.to = None 25 | self.fr = None 26 | self.cc = None 27 | self.delivered_to = None 28 | 29 | self.sent_at = None 30 | 31 | self.flags = [] 32 | self.labels = [] 33 | 34 | self.thread_id = None 35 | self.thread = [] 36 | self.message_id = None 37 | 38 | self.attachments = None 39 | 40 | 41 | 42 | def is_read(self): 43 | return ('\\Seen' in self.flags) 44 | 45 | def read(self): 46 | flag = '\\Seen' 47 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 48 | if flag not in self.flags: self.flags.append(flag) 49 | 50 | def unread(self): 51 | flag = '\\Seen' 52 | self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 53 | if flag in self.flags: self.flags.remove(flag) 54 | 55 | def is_starred(self): 56 | return ('\\Flagged' in self.flags) 57 | 58 | def star(self): 59 | flag = '\\Flagged' 60 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 61 | if flag not in self.flags: self.flags.append(flag) 62 | 63 | def unstar(self): 64 | flag = '\\Flagged' 65 | self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 66 | if flag in self.flags: self.flags.remove(flag) 67 | 68 | def is_draft(self): 69 | return ('\\Draft' in self.flags) 70 | 71 | def has_label(self, label): 72 | full_label = '%s' % label 73 | return (full_label in self.labels) 74 | 75 | def add_label(self, label): 76 | full_label = '%s' % label 77 | self.gmail.imap.uid('STORE', self.uid, '+X-GM-LABELS', full_label) 78 | if full_label not in self.labels: self.labels.append(full_label) 79 | 80 | def remove_label(self, label): 81 | full_label = '%s' % label 82 | self.gmail.imap.uid('STORE', self.uid, '-X-GM-LABELS', full_label) 83 | if full_label in self.labels: self.labels.remove(full_label) 84 | 85 | 86 | def is_deleted(self): 87 | return ('\\Deleted' in self.flags) 88 | 89 | def delete(self): 90 | flag = '\\Deleted' 91 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 92 | if flag not in self.flags: self.flags.append(flag) 93 | 94 | trash = '[Gmail]/Trash' if '[Gmail]/Trash' in self.gmail.labels() else '[Gmail]/Bin' 95 | if self.mailbox.name not in ['[Gmail]/Bin', '[Gmail]/Trash']: 96 | self.move_to(trash) 97 | 98 | # def undelete(self): 99 | # flag = '\\Deleted' 100 | # self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 101 | # if flag in self.flags: self.flags.remove(flag) 102 | 103 | 104 | def move_to(self, name): 105 | self.gmail.copy(self.uid, name, self.mailbox.name) 106 | if name not in ['[Gmail]/Bin', '[Gmail]/Trash']: 107 | self.delete() 108 | 109 | 110 | 111 | def archive(self): 112 | self.move_to('[Gmail]/All Mail') 113 | 114 | def parse_headers(self, message): 115 | hdrs = {} 116 | for hdr in message.keys(): 117 | hdrs[hdr] = message[hdr] 118 | return hdrs 119 | 120 | def parse_flags(self, headers): 121 | return list(ParseFlags(headers)) 122 | # flags = re.search(r'FLAGS \(([^\)]*)\)', headers).groups(1)[0].split(' ') 123 | 124 | def parse_labels(self, headers): 125 | if re.search(r'X-GM-LABELS \(([^\)]+)\)', headers): 126 | labels = re.search(r'X-GM-LABELS \(([^\)]+)\)', headers).groups(1)[0].split(' ') 127 | return map(lambda l: l.replace('"', '').decode("string_escape"), labels) 128 | else: 129 | return list() 130 | 131 | def parse_subject(self, encoded_subject): 132 | dh = decode_header(encoded_subject) 133 | default_charset = 'ASCII' 134 | return ''.join([ unicode(t[0], t[1] or default_charset) for t in dh ]) 135 | 136 | def parse(self, raw_message): 137 | raw_headers = raw_message[0] 138 | raw_email = raw_message[1] 139 | 140 | self.message = email.message_from_string(raw_email) 141 | self.headers = self.parse_headers(self.message) 142 | 143 | self.to = self.message['to'] 144 | self.fr = self.message['from'] 145 | self.delivered_to = self.message['delivered_to'] 146 | 147 | self.subject = self.parse_subject(self.message['subject']) 148 | 149 | if self.message.get_content_maintype() == "multipart": 150 | for content in self.message.walk(): 151 | if content.get_content_type() == "text/plain": 152 | self.body = content.get_payload(decode=True) 153 | elif content.get_content_type() == "text/html": 154 | self.html = content.get_payload(decode=True) 155 | elif self.message.get_content_maintype() == "text": 156 | self.body = self.message.get_payload() 157 | 158 | self.sent_at = datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate_tz(self.message['date'])[:9])) 159 | 160 | self.flags = self.parse_flags(raw_headers) 161 | 162 | self.labels = self.parse_labels(raw_headers) 163 | 164 | if re.search(r'X-GM-THRID (\d+)', raw_headers): 165 | self.thread_id = re.search(r'X-GM-THRID (\d+)', raw_headers).groups(1)[0] 166 | if re.search(r'X-GM-MSGID (\d+)', raw_headers): 167 | self.message_id = re.search(r'X-GM-MSGID (\d+)', raw_headers).groups(1)[0] 168 | 169 | 170 | # Parse attachments into attachment objects array for this message 171 | self.attachments = [ 172 | Attachment(attachment) for attachment in self.message._payload 173 | if not isinstance(attachment, basestring) and attachment.get('Content-Disposition') is not None 174 | ] 175 | 176 | 177 | def fetch(self): 178 | if not self.message: 179 | response, results = self.gmail.imap.uid('FETCH', self.uid, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 180 | 181 | self.parse(results[0]) 182 | 183 | return self.message 184 | 185 | # returns a list of fetched messages (both sent and received) in chronological order 186 | def fetch_thread(self): 187 | self.fetch() 188 | original_mailbox = self.mailbox 189 | self.gmail.use_mailbox(original_mailbox.name) 190 | 191 | # fetch and cache messages from inbox or other received mailbox 192 | response, results = self.gmail.imap.uid('SEARCH', None, '(X-GM-THRID ' + self.thread_id + ')') 193 | received_messages = {} 194 | uids = results[0].split(' ') 195 | if response == 'OK': 196 | for uid in uids: received_messages[uid] = Message(original_mailbox, uid) 197 | self.gmail.fetch_multiple_messages(received_messages) 198 | self.mailbox.messages.update(received_messages) 199 | 200 | # fetch and cache messages from 'sent' 201 | self.gmail.use_mailbox('[Gmail]/Sent Mail') 202 | response, results = self.gmail.imap.uid('SEARCH', None, '(X-GM-THRID ' + self.thread_id + ')') 203 | sent_messages = {} 204 | uids = results[0].split(' ') 205 | if response == 'OK': 206 | for uid in uids: sent_messages[uid] = Message(self.gmail.mailboxes['[Gmail]/Sent Mail'], uid) 207 | self.gmail.fetch_multiple_messages(sent_messages) 208 | self.gmail.mailboxes['[Gmail]/Sent Mail'].messages.update(sent_messages) 209 | 210 | self.gmail.use_mailbox(original_mailbox.name) 211 | 212 | # combine and sort sent and received messages 213 | return sorted(dict(received_messages.items() + sent_messages.items()).values(), key=lambda m: m.sent_at) 214 | 215 | 216 | class Attachment: 217 | 218 | def __init__(self, attachment): 219 | self.name = attachment.get_filename() 220 | # Raw file data 221 | self.payload = attachment.get_payload(decode=True) 222 | # Filesize in kilobytes 223 | self.size = int(round(len(self.payload)/1000.0)) 224 | 225 | def save(self, path=None): 226 | if path is None: 227 | # Save as name of attachment if there is no path specified 228 | path = self.name 229 | elif os.path.isdir(path): 230 | # If the path is a directory, save as name of attachment in that directory 231 | path = os.path.join(path, self.name) 232 | 233 | with open(path, 'wb') as f: 234 | f.write(self.payload) 235 | -------------------------------------------------------------------------------- /utils/email_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple IMAP util that will help us with account activation 3 | * Connect to your imap host 4 | * Login with username/password 5 | * Fetch latest messages in inbox 6 | * Get a recent registration message 7 | * Filter based on sender and subject 8 | * Return text of recent messages 9 | 10 | [TO DO](not in any particular order) 11 | 1. Extend to POP3 servers 12 | 2. Add a try catch decorator 13 | 3. Enhance get_latest_email_uid to make all parameters optional 14 | """ 15 | #The import statements import: standard Python modules,conf 16 | import os,sys,time,imaplib,email,datetime 17 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 18 | import conf.email_conf as conf_file 19 | 20 | class Email_Util: 21 | "Class to interact with IMAP servers" 22 | 23 | def connect(self,imap_host): 24 | "Connect with the host" 25 | self.mail = imaplib.IMAP4_SSL(imap_host) 26 | 27 | return self.mail 28 | 29 | 30 | def login(self,username,password): 31 | "Login to the email" 32 | result_flag = False 33 | try: 34 | self.mail.login(username,password) 35 | except Exception as e: 36 | print('\nException in Email_Util.login') 37 | print('PYTHON SAYS:') 38 | print(e) 39 | print('\n') 40 | else: 41 | result_flag = True 42 | 43 | return result_flag 44 | 45 | 46 | def get_folders(self): 47 | "Return a list of folders" 48 | return self.mail.list() 49 | 50 | 51 | def select_folder(self,folder): 52 | "Select the given folder if it exists. E.g.: [Gmail]/Trash" 53 | result_flag = False 54 | response = self.mail.select(folder) 55 | if response[0] == 'OK': 56 | result_flag = True 57 | 58 | return result_flag 59 | 60 | 61 | def get_latest_email_uid(self,subject=None,sender=None,time_delta=10,wait_time=300): 62 | "Search for a subject and return the latest unique ids of the emails" 63 | uid = None 64 | time_elapsed = 0 65 | search_string = '' 66 | if subject is None and sender is None: 67 | search_string = 'ALL' 68 | 69 | if subject is None and sender is not None: 70 | search_string = '(FROM "{sender}")'.format(sender=sender) 71 | 72 | if subject is not None and sender is None: 73 | search_string = '(SUBJECT "{subject}")'.format(subject=subject) 74 | 75 | if subject is not None and sender is not None: 76 | search_string = '(FROM "{sender}" SUBJECT "{subject}")'.format(sender=sender,subject=subject) 77 | 78 | print(" - Automation will be in search/wait mode for max %s seconds"%wait_time) 79 | while (time_elapsed < wait_time and uid is None): 80 | time.sleep(time_delta) 81 | result,data = self.mail.uid('search',None,str(search_string)) 82 | 83 | if data[0].strip() != '': #Check for an empty set 84 | uid = data[0].split()[-1] 85 | 86 | time_elapsed += time_delta 87 | 88 | return uid 89 | 90 | 91 | def fetch_email_body(self,uid): 92 | "Fetch the email body for a given uid" 93 | email_body = [] 94 | if uid is not None: 95 | result,data = self.mail.uid('fetch',uid,'(RFC822)') 96 | raw_email = data[0][1] 97 | email_msg = email.message_from_string(raw_email) 98 | email_body = self.get_email_body(email_msg) 99 | 100 | return email_body 101 | 102 | 103 | def get_email_body(self,email_msg): 104 | "Parse out the text of the email message. Handle multipart messages" 105 | email_body = [] 106 | maintype = email_msg.get_content_maintype() 107 | if maintype == 'multipart': 108 | for part in email_msg.get_payload(): 109 | if part.get_content_maintype() == 'text': 110 | email_body.append(part.get_payload()) 111 | elif maintype == 'text': 112 | email_body.append(email_msg.get_payload()) 113 | 114 | return email_body 115 | 116 | 117 | def logout(self): 118 | "Logout" 119 | result_flag = False 120 | response, data = self.mail.logout() 121 | if response == 'BYE': 122 | result_flag = True 123 | 124 | return result_flag 125 | 126 | 127 | #---EXAMPLE USAGE--- 128 | if __name__=='__main__': 129 | #Fetching conf details from the conf file 130 | imap_host = conf_file.imaphost 131 | username = conf_file.username 132 | password = conf_file.app_password 133 | 134 | #Initialize the email object 135 | email_obj = Email_Util() 136 | 137 | #Connect to the IMAP host 138 | email_obj.connect(imap_host) 139 | 140 | #Login 141 | if email_obj.login(username,password): 142 | print("PASS: Successfully logged in.") 143 | else: 144 | print("FAIL: Failed to login") 145 | 146 | #Get a list of folder 147 | folders = email_obj.get_folders() 148 | if folders != None or []: 149 | print("PASS: Email folders:", email_obj.get_folders()) 150 | 151 | else: 152 | print("FAIL: Didn't get folder details") 153 | 154 | #Select a folder 155 | if email_obj.select_folder('Inbox'): 156 | print("PASS: Successfully selected the folder: Inbox") 157 | else: 158 | print("FAIL: Failed to select the folder: Inbox") 159 | 160 | #Get the latest email's unique id 161 | uid = email_obj.get_latest_email_uid(wait_time=300) 162 | if uid != None: 163 | print("PASS: Unique id of the latest email is: ",uid) 164 | else: 165 | print("FAIL: Didn't get unique id of latest email") 166 | 167 | #A. Look for an Email from provided sender, print uid and check it's contents 168 | uid = email_obj.get_latest_email_uid(sender="Andy from Google",wait_time=300) 169 | if uid != None: 170 | print("PASS: Unique id of the latest email with given sender is: ",uid) 171 | 172 | #Check the text of the latest email id 173 | email_body = email_obj.fetch_email_body(uid) 174 | data_flag = False 175 | print(" - Automation checking mail contents") 176 | for line in email_body: 177 | line = line.replace('=','') 178 | line = line.replace('<','') 179 | line = line.replace('>','') 180 | 181 | if "Hi Email_Util" and "This email was sent to you" in line: 182 | data_flag = True 183 | break 184 | if data_flag == True: 185 | print("PASS: Automation provided correct Email details. Email contents matched with provided data.") 186 | else: 187 | print("FAIL: Provided data not matched with Email contents. Looks like automation provided incorrect Email details") 188 | 189 | else: 190 | print("FAIL: After wait of 5 mins, looks like there is no email present with given sender") 191 | 192 | #B. Look for an Email with provided subject, print uid, find Qxf2 POM address and compare with expected address 193 | uid = email_obj.get_latest_email_uid(subject="Qxf2 Services: Public POM Link",wait_time=300) 194 | if uid != None: 195 | print("PASS: Unique id of the latest email with given subject is: ",uid) 196 | #Get pom url from email body 197 | email_body = email_obj.fetch_email_body(uid) 198 | expected_pom_url = "https://github.com/qxf2/qxf2-page-object-model" 199 | pom_url = None 200 | data_flag = False 201 | print(" - Automation checking mail contents") 202 | for body in email_body: 203 | search_str = "/qxf2/" 204 | body = body.split() 205 | for element in body: 206 | if search_str in element: 207 | pom_url = element 208 | data_flag = True 209 | break 210 | 211 | if data_flag == True: 212 | break 213 | 214 | if data_flag == True and expected_pom_url == pom_url: 215 | print("PASS: Automation provided correct mail details. Got correct Qxf2 POM url from mail body. URL: %s"%pom_url) 216 | else: 217 | print("FAIL: Actual POM url not matched with expected pom url. Actual URL got from email: %s"%pom_url) 218 | 219 | else: 220 | print("FAIL: After wait of 5 mins, looks like there is no email present with given subject") 221 | 222 | #C. Look for an Email with provided sender and subject and print uid 223 | uid = email_obj.get_latest_email_uid(subject="get more out of your new Google Account",sender="andy-noreply@google.com",wait_time=300) 224 | if uid != None: 225 | print("PASS: Unique id of the latest email with given subject and sender is: ",uid) 226 | else: 227 | print("FAIL: After wait of 5 mins, looks like there is no email present with given subject and sender") 228 | 229 | #D. Look for an Email with non-existant sender and non-existant subject details 230 | uid = email_obj.get_latest_email_uid(subject="Activate your account",sender="support@qxf2.com",wait_time=120) #you can change wait time by setting wait_time variable 231 | if uid != None: 232 | print("FAIL: Unique id of the latest email with non-existant subject and non-existant sender is: ",uid) 233 | else: 234 | print("PASS: After wait of 2 mins, looks like there is no email present with given non-existant subject and non-existant sender") 235 | 236 | -------------------------------------------------------------------------------- /utils/email_pytest_report.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to send pytest test report email 3 | * Supports both text and html formatted messages 4 | * Supports text, html, image, audio files as an attachment 5 | 6 | To Do: 7 | * Provide support to add multiple attachment 8 | 9 | Note: 10 | * We added subject, email body message as per our need. You can update that as per your requirement. 11 | * To generate html formatted test report, you need to use pytest-html plugin. To install it use command: pip install pytest-html 12 | * To generate pytest_report.html file use following command from the root of repo e.g. py.test --html = log/pytest_report.html 13 | * To generate pytest_report.log file use following command from the root of repo e.g. py.test -k example_form -r F -v > log/pytest_report.log 14 | """ 15 | import smtplib 16 | import os,sys 17 | from email.mime.text import MIMEText 18 | from email.mime.image import MIMEImage 19 | from email.mime.audio import MIMEAudio 20 | from email.mime.multipart import MIMEMultipart 21 | from email.mime.base import MIMEBase 22 | import mimetypes 23 | from email import encoders 24 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 25 | import conf.email_conf as conf_file 26 | 27 | 28 | class Email_Pytest_Report: 29 | "Class to email pytest report" 30 | 31 | def __init__(self): 32 | self.smtp_ssl_host = conf_file.smtp_ssl_host 33 | self.smtp_ssl_port = conf_file.smtp_ssl_port 34 | self.username = conf_file.username 35 | self.password = conf_file.app_password 36 | self.sender = conf_file.sender 37 | self.targets = conf_file.targets 38 | 39 | 40 | def get_test_report_data(self,html_body_flag= True,report_file_path= 'default'): 41 | "get test report data from pytest_report.html or pytest_report.txt or from user provided file" 42 | if html_body_flag == True and report_file_path == 'default': 43 | #To generate pytest_report.html file use following command e.g. py.test --html = log/pytest_report.html 44 | test_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','log','pytest_report.html'))#Change report file name & address here 45 | elif html_body_flag == False and report_file_path == 'default': 46 | #To generate pytest_report.log file add ">pytest_report.log" at end of py.test command e.g. py.test -k example_form -r F -v > log/pytest_report.log 47 | test_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','log','pytest_report.log'))#Change report file name & address here 48 | else: 49 | test_report_file = report_file_path 50 | #check file exist or not 51 | if not os.path.exists(test_report_file): 52 | raise Exception("File '%s' does not exist. Please provide valid file"%test_report_file) 53 | 54 | with open(test_report_file, "r") as in_file: 55 | testdata = "" 56 | for line in in_file: 57 | testdata = testdata + '\n' + line 58 | 59 | return testdata 60 | 61 | 62 | def get_attachment(self,attachment_file_path = 'default'): 63 | "Get attachment and attach it to mail" 64 | if attachment_file_path == 'default': 65 | #To generate pytest_report.html file use following command e.g. py.test --html = log/pytest_report.html 66 | attachment_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','log','pytest_report.html'))#Change report file name & address here 67 | else: 68 | attachment_report_file = attachment_file_path 69 | #check file exist or not 70 | if not os.path.exists(attachment_report_file): 71 | raise Exception("File '%s' does not exist. Please provide valid file"%attachment_report_file) 72 | 73 | # Guess encoding type 74 | ctype, encoding = mimetypes.guess_type(attachment_report_file) 75 | if ctype is None or encoding is not None: 76 | ctype = 'application/octet-stream' # Use a binary type as guess couldn't made 77 | 78 | maintype, subtype = ctype.split('/', 1) 79 | if maintype == 'text': 80 | fp = open(attachment_report_file) 81 | attachment = MIMEText(fp.read(), subtype) 82 | fp.close() 83 | elif maintype == 'image': 84 | fp = open(attachment_report_file, 'rb') 85 | attachment = MIMEImage(fp.read(), subtype) 86 | fp.close() 87 | elif maintype == 'audio': 88 | fp = open(attachment_report_file, 'rb') 89 | attachment = MIMEAudio(fp.read(), subtype) 90 | fp.close() 91 | else: 92 | fp = open(attachment_report_file, 'rb') 93 | attachment = MIMEBase(maintype, subtype) 94 | attachment.set_payload(fp.read()) 95 | fp.close() 96 | # Encode the payload using Base64 97 | encoders.encode_base64(attachment) 98 | # Set the filename parameter 99 | attachment.add_header('Content-Disposition', 100 | 'attachment', 101 | filename=os.path.basename(attachment_report_file)) 102 | 103 | return attachment 104 | 105 | 106 | def send_test_report_email(self,html_body_flag = True,attachment_flag = False,report_file_path = 'default'): 107 | "send test report email" 108 | #1. Get html formatted email body data from report_file_path file (log/pytest_report.html) and do not add it as an attachment 109 | if html_body_flag == True and attachment_flag == False: 110 | testdata = self.get_test_report_data(html_body_flag,report_file_path) #get html formatted test report data from log/pytest_report.html 111 | message = MIMEText(testdata,"html") # Add html formatted test data to email 112 | 113 | #2. Get text formatted email body data from report_file_path file (log/pytest_report.log) and do not add it as an attachment 114 | elif html_body_flag == False and attachment_flag == False: 115 | testdata = self.get_test_report_data(html_body_flag,report_file_path) #get html test report data from log/pytest_report.log 116 | message = MIMEText(testdata) # Add text formatted test data to email 117 | 118 | #3. Add html formatted email body message along with an attachment file 119 | elif html_body_flag == True and attachment_flag == True: 120 | message = MIMEMultipart() 121 | #add html formatted body message to email 122 | html_body = MIMEText('''

Hello,

123 |

        Please check the attachment to see test built report.

124 |

Note: For best UI experience, download the attachment and open using Chrome browser.

125 | ''',"html") # Add/Update email body message here as per your requirement 126 | message.attach(html_body) 127 | #add attachment to email 128 | attachment = self.get_attachment(report_file_path) 129 | message.attach(attachment) 130 | 131 | #4. Add text formatted email body message along with an attachment file 132 | else: 133 | message = MIMEMultipart() 134 | #add test formatted body message to email 135 | plain_text_body = MIMEText('''Hello,\n\tPlease check attachment to see test built report. 136 | \n\nNote: For best UI experience, download the attachment and open using Chrome browser.''')# Add/Update email body message here as per your requirement 137 | message.attach(plain_text_body) 138 | #add attachment to email 139 | attachment = self.get_attachment(report_file_path) 140 | message.attach(attachment) 141 | 142 | message['From'] = self.sender 143 | message['To'] = ', '.join(self.targets) 144 | message['Subject'] = 'Script generated test report' # Update email subject here 145 | 146 | #Send Email 147 | server = smtplib.SMTP_SSL(self.smtp_ssl_host, self.smtp_ssl_port) 148 | server.login(self.username, self.password) 149 | server.sendmail(self.sender, self.targets, message.as_string()) 150 | server.quit() 151 | 152 | 153 | #---USAGE EXAMPLES 154 | if __name__=='__main__': 155 | print("Start of %s"%__file__) 156 | 157 | #Initialize the Email_Pytest_Report object 158 | email_obj = Email_Pytest_Report() 159 | #1. Send html formatted email body message with pytest report as an attachment 160 | #Here log/pytest_report.html is a default file. To generate pytest_report.html file use following command to the test e.g. py.test --html = log/pytest_report.html 161 | email_obj.send_test_report_email(html_body_flag=True,attachment_flag=True,report_file_path= 'default') 162 | 163 | #Note: We commented below code to avoid sending multiple emails, you can try the other cases one by one to know more about email_pytest_report util. 164 | 165 | ''' 166 | #2. Send html formatted pytest report 167 | email_obj.send_test_report_email(html_body_flag=True,attachment_flag=False,report_file_path= 'default') 168 | 169 | #3. Send plain text formatted pytest report 170 | email_obj.send_test_report_email(html_body_flag=False,attachment_flag=False,report_file_path= 'default') 171 | 172 | #4. Send plain formatted email body message with pytest reports an attachment 173 | email_obj.send_test_report_email(html_body_flag=False,attachment_flag=True,report_file_path='default') 174 | 175 | #5. Send different type of attachment 176 | image_file = ("C:\\Users\\Public\\Pictures\\Sample Pictures\\Koala.jpg") # add attachment file here 177 | email_obj.send_test_report_email(html_body_flag=False,attachment_flag=True,report_file_path= image_file) 178 | ''' 179 | 180 | -------------------------------------------------------------------------------- /utils/Test_Rail.py: -------------------------------------------------------------------------------- 1 | """ 2 | TestRail integration: 3 | * limited to what we need at this time 4 | * we assume TestRail operates in single suite mode 5 | i.e., the default, reccomended mode 6 | 7 | API reference: http://docs.gurock.com/testrail-api2/start 8 | """ 9 | import dotenv,os 10 | from utils import testrail 11 | import conf.testrailenv_conf as conf_file 12 | 13 | class Test_Rail: 14 | "Wrapper around TestRail's API" 15 | 16 | def __init__(self): 17 | "Initialize the TestRail objects" 18 | self.set_testrail_conf() 19 | 20 | 21 | def set_testrail_conf(self): 22 | "Set the TestRail URL and username, password" 23 | 24 | #Set the TestRail URL 25 | self.testrail_url = conf_file.testrail_url 26 | self.client = testrail.APIClient(self.testrail_url) 27 | 28 | #TestRail User and Password 29 | self.client.user = conf_file.testrail_user 30 | self.client.password = conf_file.testrail_password 31 | 32 | 33 | def get_project_id(self,project_name): 34 | "Get the project ID using project name" 35 | project_id=None 36 | projects = self.client.send_get('get_projects') 37 | for project in projects: 38 | if project['name'] == project_name: 39 | project_id = project['id'] 40 | break 41 | return project_id 42 | 43 | 44 | def get_suite_id(self,project_name,suite_name): 45 | "Get the suite ID using project name and suite name" 46 | suite_id=None 47 | project_id = self.get_project_id(project_name) 48 | suites = self.client.send_get('get_suites/%s'%(project_id)) 49 | for suite in suites: 50 | if suite['name'] == suite_name: 51 | suite_id = suite['id'] 52 | break 53 | return suite_id 54 | 55 | 56 | def get_milestone_id(self,project_name,milestone_name): 57 | "Get the milestone ID using project name and milestone name" 58 | milestone_id = None 59 | project_id = self.get_project_id(project_name) 60 | milestones = self.client.send_get('get_milestones/%s'%(project_id)) 61 | for milestone in milestones: 62 | if milestone['name'] == milestone_name: 63 | milestone_id = milestone['id'] 64 | break 65 | return milestone_id 66 | 67 | 68 | def get_user_id(self,user_name): 69 | "Get the user ID using user name" 70 | user_id=None 71 | users = self.client.send_get('get_users') 72 | for user in users: 73 | if user['name'] == user_name: 74 | user_id = user['id'] 75 | break 76 | return user_id 77 | 78 | 79 | def get_run_id(self,project_name,test_run_name): 80 | "Get the run ID using test name and project name" 81 | run_id=None 82 | project_id = self.get_project_id(project_name) 83 | try: 84 | test_runs = self.client.send_get('get_runs/%s'%(project_id)) 85 | except Exception as e: 86 | print('Exception in update_testrail() updating TestRail.') 87 | print('PYTHON SAYS: ') 88 | print(e) 89 | else: 90 | for test_run in test_runs: 91 | if test_run['name'] == test_run_name: 92 | run_id = test_run['id'] 93 | break 94 | 95 | return run_id 96 | 97 | 98 | def create_milestone(self,project_name,milestone_name,milestone_description=""): 99 | "Create a new milestone if it does not already exist" 100 | milestone_id = self.get_milestone_id(project_name,milestone_name) 101 | if milestone_id is None: 102 | project_id = self.get_project_id(project_name) 103 | if project_id is not None: 104 | try: 105 | data = {'name':milestone_name, 106 | 'description':milestone_description} 107 | result = self.client.send_post('add_milestone/%s'%str(project_id), 108 | data) 109 | except Exception as e: 110 | print('Exception in create_new_project() creating new project.') 111 | print('PYTHON SAYS: ') 112 | print(e) 113 | else: 114 | print('Created the milestone: %s'%milestone_name) 115 | else: 116 | print("Milestone '%s' already exists"%milestone_name) 117 | 118 | 119 | def create_new_project(self,new_project_name,project_description,show_announcement,suite_mode): 120 | "Create a new project if it does not already exist" 121 | project_id = self.get_project_id(new_project_name) 122 | if project_id is None: 123 | try: 124 | result = self.client.send_post('add_project', 125 | {'name': new_project_name, 126 | 'announcement': project_description, 127 | 'show_announcement': show_announcement, 128 | 'suite_mode': suite_mode,}) 129 | except Exception as e: 130 | print('Exception in create_new_project() creating new project.') 131 | print('PYTHON SAYS: ') 132 | print(e) 133 | else: 134 | print("Project already exists %s"%new_project_name) 135 | 136 | 137 | def create_test_run(self,project_name,test_run_name,milestone_name=None,description="",suite_name=None,case_ids=[],assigned_to=None): 138 | "Create a new test run if it does not already exist" 139 | #reference: http://docs.gurock.com/testrail-api2/reference-runs 140 | project_id = self.get_project_id(project_name) 141 | test_run_id = self.get_run_id(project_name,test_run_name) 142 | if project_id is not None and test_run_id is None: 143 | data = {} 144 | if suite_name is not None: 145 | suite_id = self.get_suite_id(project_name,suite_name) 146 | if suite_id is not None: 147 | data['suite_id'] = suite_id 148 | data['name'] = test_run_name 149 | data['description'] = description 150 | if milestone_name is not None: 151 | milestone_id = self.get_milestone_id(project_name,milestone_name) 152 | if milestone_id is not None: 153 | data['milestone_id'] = milestone_id 154 | if assigned_to is not None: 155 | assignedto_id = self.get_user_id(assigned_to) 156 | if assignedto_id is not None: 157 | data['assignedto_id'] = assignedto_id 158 | if len(case_ids) > 0: 159 | data['case_ids'] = case_ids 160 | data['include_all'] = False 161 | 162 | try: 163 | result = self.client.send_post('add_run/%s'%(project_id),data) 164 | except Exception as e: 165 | print('Exception in create_test_run() Creating Test Run.') 166 | print('PYTHON SAYS: ') 167 | print(e) 168 | else: 169 | print('Created the test run: %s'%test_run_name) 170 | else: 171 | if project_id is None: 172 | print("Cannot add test run %s because Project %s was not found"%(test_run_name,project_name)) 173 | elif test_run_id is not None: 174 | print("Test run '%s' already exists"%test_run_name) 175 | 176 | 177 | def delete_project(self,new_project_name,project_description): 178 | "Delete an existing project" 179 | project_id = self.get_project_id(new_project_name) 180 | if project_id is not None: 181 | try: 182 | result = self.client.send_post('delete_project/%s'%(project_id),project_description) 183 | except Exception as e: 184 | print('Exception in delete_project() deleting project.') 185 | print('PYTHON SAYS: ') 186 | print(e) 187 | else: 188 | print('Cant delete the project given project name: %s'%(new_project_name)) 189 | 190 | 191 | def delete_test_run(self,test_run_name,project_name): 192 | "Delete an existing test run" 193 | run_id = self.get_run_id(test_run_name,project_name) 194 | if run_id is not None: 195 | try: 196 | result = self.client.send_post('delete_run/%s'%(run_id),test_run_name) 197 | except Exception as e: 198 | print('Exception in update_testrail() updating TestRail.') 199 | print('PYTHON SAYS: ') 200 | print(e) 201 | else: 202 | print('Cant delete the test run for given project and test run name: %s , %s'%(project_name,test_run_name)) 203 | 204 | 205 | def update_testrail(self,case_id,run_id,result_flag,msg=""): 206 | "Update TestRail for a given run_id and case_id" 207 | update_flag = False 208 | 209 | #Update the result in TestRail using send_post function. 210 | #Parameters for add_result_for_case is the combination of runid and case id. 211 | #status_id is 1 for Passed, 2 For Blocked, 4 for Retest and 5 for Failed 212 | status_id = 1 if result_flag is True else 5 213 | 214 | if ((run_id is not None) and (case_id != 'None')) : 215 | try: 216 | result = self.client.send_post( 217 | 'add_result_for_case/%s/%s'%(run_id,case_id), 218 | {'status_id': status_id, 'comment': msg }) 219 | except Exception as e: 220 | print('Exception in update_testrail() updating TestRail.') 221 | print('PYTHON SAYS: ') 222 | print(e) 223 | else: 224 | print('Updated test result for case: %s in test run: %s\n'%(case_id,run_id)) 225 | 226 | return update_flag 227 | -------------------------------------------------------------------------------- /utils/Option_Parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class to wrap around parsing command line options 3 | """ 4 | import os, sys 5 | import optparse 6 | 7 | 8 | class Option_Parser: 9 | "The option parser class" 10 | 11 | def __init__(self,usage="\n----\n%prog -b -c -u -a -r -t -s \n----\nE.g.: %prog -b FF -c .conf -u http://qxf2.com -r 2 -t testrail.conf -s Y\n---" 12 | ): 13 | "Class initializer" 14 | self.usage=usage 15 | self.parser=optparse.OptionParser() 16 | self.set_standard_options() 17 | 18 | def set_standard_options(self): 19 | "Set options shared by all tests over here" 20 | self.parser.add_option("-B","--browser", 21 | dest="browser", 22 | default="firefox", 23 | help="Browser. Valid options are firefox, ie and chrome") 24 | self.parser.add_option("-U","--app_url", 25 | dest="url", 26 | default="https://qxf2.com", 27 | help="The url of the application") 28 | self.parser.add_option("-A","--api_url", 29 | dest="api_url", 30 | default="http://35.167.62.251/", 31 | help="The url of the api") 32 | self.parser.add_option("-X","--testrail_flag", 33 | dest="testrail_flag", 34 | default='N', 35 | help="Y or N. 'Y' if you want to report to TestRail") 36 | self.parser.add_option("-R","--test_run_id", 37 | dest="test_run_id", 38 | default=None, 39 | help="The test run id in TestRail") 40 | self.parser.add_option("-M","--remote_flag", 41 | dest="remote_flag", 42 | default="N", 43 | help="Run the test in remote flag: Y or N") 44 | self.parser.add_option("-O","--os_version", 45 | dest="os_version", 46 | help="The operating system: xp, 7", 47 | default="7") 48 | self.parser.add_option("--ver", 49 | dest="browser_version", 50 | help="The version of the browser: a whole number", 51 | default=45) 52 | self.parser.add_option("-P","--os_name", 53 | dest="os_name", 54 | help="The operating system: Windows , Linux", 55 | default="Windows") 56 | self.parser.add_option("-G","--mobile_os_name", 57 | dest="mobile_os_name", 58 | help="Enter operating system of mobile. Ex: Android, iOS", 59 | default="Android") 60 | self.parser.add_option("-H","--mobile_os_version", 61 | dest="mobile_os_version", 62 | help="Enter version of operating system of mobile: 8.1.0", 63 | default="6.0") 64 | self.parser.add_option("-I","--device_name", 65 | dest="device_name", 66 | help="Enter device name. Ex: Emulator, physical device name", 67 | default="Google Nexus 6") 68 | self.parser.add_option("-J","--app_package", 69 | dest="app_package", 70 | help="Enter name of app package. Ex: bitcoininfo", 71 | default="com.dudam.rohan.bitcoininfo") 72 | self.parser.add_option("-K","--app_activity", 73 | dest="app_activity", 74 | help="Enter name of app activity. Ex: .MainActivity", 75 | default=".MainActivity") 76 | self.parser.add_option("-Q","--device_flag", 77 | dest="device_flag", 78 | help="Enter Y or N. 'Y' if you want to run the test on device. 'N' if you want to run the test on emulator.", 79 | default="N") 80 | self.parser.add_option("-D","--app_name", 81 | dest="app_name", 82 | help="Enter application name to be uploaded.Ex:Bitcoin Info_com.dudam.rohan.bitcoininfo.apk.", 83 | default="Bitcoin Info_com.dudam.rohan.bitcoininfo.apk") 84 | self.parser.add_option("-T","--tesults_flag", 85 | dest="tesults_flag", 86 | help="Enter Y or N. 'Y' if you want to report results with Tesults", 87 | default="N") 88 | self.parser.add_option("-N","--app_path", 89 | dest="app_path", 90 | help="Enter app path") 91 | self.parser.add_option("--remote_project_name", 92 | dest="remote_project_name", 93 | help="The project name if its run in BrowserStack", 94 | default=None) 95 | self.parser.add_option("--remote_build_name", 96 | dest="remote_build_name", 97 | help="The build name if its run in BrowserStack", 98 | default=None) 99 | 100 | def add_option(self,option_letter,option_word,dest,help_text): 101 | "Add an option to our parser" 102 | self.parser.add(option_letter, 103 | option_word, 104 | dest, 105 | help=help_text) 106 | 107 | 108 | def get_options(self): 109 | "Get the command line arguments passed into the script" 110 | (options,args)=self.parser.parse_args() 111 | 112 | return options 113 | 114 | 115 | def check_file_exists(self,file_path): 116 | "Check if the config file exists and is a file" 117 | self.conf_flag = True 118 | if os.path.exists(file_path): 119 | if not os.path.isfile(file_path): 120 | print('\n****') 121 | print('Config file provided is not a file: ') 122 | print(file_path) 123 | print('****') 124 | self.conf_flag = False 125 | else: 126 | print('\n****') 127 | print('Unable to locate the provided config file: ') 128 | print(file_path) 129 | print('****') 130 | self.conf_flag = False 131 | 132 | return self.conf_flag 133 | 134 | 135 | def check_options(self,options): 136 | "Check if the command line options are valid" 137 | result_flag = True 138 | if options.browser is not None: 139 | result_flag &= True 140 | else: 141 | result_flag = False 142 | print("Browser cannot be None. Use -B to specify a browser") 143 | if options.url is not None: 144 | result_flag &= True 145 | else: 146 | result_flag = False 147 | print("Url cannot be None. Use -U to specify a url") 148 | if options.api_url is not None: 149 | result_flag &= True 150 | else: 151 | result_flag = False 152 | print("API URL cannot be None. Use -A to specify a api url") 153 | if options.remote_flag.lower() == 'y': 154 | if options.browser_version is not None: 155 | result_flag &= True 156 | else: 157 | result_flag = False 158 | print("Browser version cannot be None. Use --ver to specify a browser version") 159 | if options.os_name is not None: 160 | result_flag &= True 161 | else: 162 | result_flag = False 163 | print("The operating system cannot be None. Use -P to specify an OS") 164 | if options.os_version is not None: 165 | result_flag &= True 166 | else: 167 | result_flag = False 168 | print("The OS version cannot be None. Use -O to specify an OS version") 169 | 170 | # Options for appium mobile tests. 171 | if options.mobile_os_name is not None: 172 | result_flag &= True 173 | else: 174 | result_flag = False 175 | print("The mobile operating system cannot be None. Use -G to specify an OS.") 176 | 177 | if options.mobile_os_version is not None: 178 | result_flag &= True 179 | else: 180 | result_flag = False 181 | print("The mobile operating system version cannot be None. Use -H to specify an OS version.") 182 | 183 | if options.device_name is not None: 184 | result_flag &= True 185 | else: 186 | result_flag = False 187 | print("The device name cannot be None. Use -I to specify device name.") 188 | 189 | if options.app_package is not None: 190 | result_flag &= True 191 | else: 192 | result_flag = False 193 | print("The application package name cannot be None. Use -J to specify application package name.") 194 | 195 | if options.app_activity is not None: 196 | result_flag &= True 197 | else: 198 | result_flag = False 199 | print("The application activity name cannot be None. Use -K to specify application activity name.") 200 | 201 | if options.device_flag.lower() == 'n': 202 | result_flag &= True 203 | else: 204 | result_flag = False 205 | print("The device flag cannot be None. Use -Q to specify device flag.") 206 | 207 | return result_flag 208 | 209 | 210 | def print_usage(self): 211 | "Print the option parser's usage string" 212 | print(self.parser.print_usage()) 213 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conf import browser_os_name_conf 3 | from utils import post_test_reports_to_slack 4 | from utils.email_pytest_report import Email_Pytest_Report 5 | 6 | 7 | @pytest.fixture 8 | def browser(request): 9 | "pytest fixture for browser" 10 | return request.config.getoption("-B") 11 | 12 | 13 | @pytest.fixture 14 | def base_url(request): 15 | "pytest fixture for base url" 16 | return request.config.getoption("-U") 17 | 18 | 19 | @pytest.fixture 20 | def api_url(request): 21 | "pytest fixture for base url" 22 | return request.config.getoption("-A") 23 | 24 | 25 | @pytest.fixture 26 | def test_run_id(request): 27 | "pytest fixture for test run id" 28 | return request.config.getoption("-R") 29 | 30 | 31 | @pytest.fixture 32 | def testrail_flag(request): 33 | "pytest fixture for test rail flag" 34 | return request.config.getoption("-X") 35 | 36 | 37 | @pytest.fixture 38 | def remote_flag(request): 39 | "pytest fixture for browserstack/sauce flag" 40 | return request.config.getoption("-M") 41 | 42 | 43 | @pytest.fixture 44 | def browser_version(request): 45 | "pytest fixture for browser version" 46 | return request.config.getoption("--ver") 47 | 48 | 49 | @pytest.fixture 50 | def os_name(request): 51 | "pytest fixture for os_name" 52 | return request.config.getoption("-P") 53 | 54 | 55 | @pytest.fixture 56 | def os_version(request): 57 | "pytest fixture for os version" 58 | return request.config.getoption("-O") 59 | 60 | 61 | @pytest.fixture 62 | def remote_project_name(request): 63 | "pytest fixture for browserStack project name" 64 | return request.config.getoption("--remote_project_name") 65 | 66 | 67 | @pytest.fixture 68 | def remote_build_name(request): 69 | "pytest fixture for browserStack build name" 70 | return request.config.getoption("--remote_build_name") 71 | 72 | 73 | @pytest.fixture 74 | def slack_flag(request): 75 | "pytest fixture for sending reports on slack" 76 | return request.config.getoption("-S") 77 | 78 | 79 | @pytest.fixture 80 | def tesults_flag(request): 81 | "pytest fixture for sending results to tesults" 82 | return request.config.getoption("--tesults") 83 | 84 | 85 | @pytest.fixture 86 | def mobile_os_name(request): 87 | "pytest fixture for mobile os name" 88 | return request.config.getoption("-G") 89 | 90 | 91 | @pytest.fixture 92 | def mobile_os_version(request): 93 | "pytest fixture for mobile os version" 94 | return request.config.getoption("-H") 95 | 96 | 97 | @pytest.fixture 98 | def device_name(request): 99 | "pytest fixture for device name" 100 | return request.config.getoption("-I") 101 | 102 | 103 | @pytest.fixture 104 | def app_package(request): 105 | "pytest fixture for app package" 106 | return request.config.getoption("-J") 107 | 108 | 109 | @pytest.fixture 110 | def app_activity(request): 111 | "pytest fixture for app activity" 112 | return request.config.getoption("-K") 113 | 114 | 115 | @pytest.fixture 116 | def device_flag(request): 117 | "pytest fixture for device flag" 118 | return request.config.getoption("-Q") 119 | 120 | 121 | @pytest.fixture 122 | def email_pytest_report(request): 123 | "pytest fixture for device flag" 124 | return request.config.getoption("--email_pytest_report") 125 | 126 | 127 | @pytest.fixture 128 | def app_name(request): 129 | "pytest fixture for app name" 130 | return request.config.getoption("-D") 131 | 132 | 133 | @pytest.fixture 134 | def app_path(request): 135 | "pytest fixture for app path" 136 | return request.config.getoption("-N") 137 | 138 | 139 | def pytest_terminal_summary(terminalreporter, exitstatus): 140 | "add additional section in terminal summary reporting." 141 | if terminalreporter.config.getoption("-S").lower() == 'y': 142 | post_test_reports_to_slack.post_reports_to_slack() 143 | elif terminalreporter.config.getoption("--email_pytest_report").lower() == 'y': 144 | #Initialize the Email_Pytest_Report object 145 | email_obj = Email_Pytest_Report() 146 | # Send html formatted email body message with pytest report as an attachment 147 | email_obj.send_test_report_email(html_body_flag=True,attachment_flag=True,report_file_path= 'default') 148 | 149 | if terminalreporter.config.getoption("--tesults").lower() == 'y': 150 | Tesults.post_results_to_tesults() 151 | 152 | def pytest_generate_tests(metafunc): 153 | "test generator function to run tests across different parameters" 154 | 155 | if 'browser' in metafunc.fixturenames: 156 | if metafunc.config.getoption("-M").lower() == 'y': 157 | if metafunc.config.getoption("-B") == ["all"]: 158 | metafunc.parametrize("browser,browser_version,os_name,os_version", 159 | browser_os_name_conf.cross_browser_cross_platform_config) 160 | elif metafunc.config.getoption("-B") == []: 161 | metafunc.parametrize("browser,browser_version,os_name,os_version", 162 | browser_os_name_conf.default_config_list) 163 | else: 164 | config_list = [(metafunc.config.getoption("-B")[0],metafunc.config.getoption("--ver")[0],metafunc.config.getoption("-P")[0],metafunc.config.getoption("-O")[0])] 165 | metafunc.parametrize("browser,browser_version,os_name,os_version", 166 | config_list) 167 | if metafunc.config.getoption("-M").lower() !='y': 168 | if metafunc.config.getoption("-B") == ["all"]: 169 | metafunc.config.option.browser = browser_os_name_conf.local_browsers 170 | metafunc.parametrize("browser", metafunc.config.option.browser) 171 | elif metafunc.config.getoption("-B") == []: 172 | metafunc.parametrize("browser",browser_os_name_conf.default_browser) 173 | else: 174 | config_list_local = [(metafunc.config.getoption("-B")[0])] 175 | metafunc.parametrize("browser", config_list_local) 176 | 177 | def pytest_addoption(parser): 178 | parser.addoption("-B","--browser", 179 | dest="browser", 180 | action="append", 181 | default=[], 182 | help="Browser. Valid options are firefox, ie and chrome") 183 | parser.addoption("-U","--app_url", 184 | dest="url", 185 | default="https://weathershopper.pythonanywhere.com", 186 | help="The url of the application") 187 | parser.addoption("-A","--api_url", 188 | dest="url", 189 | default="http://35.167.62.251", 190 | help="The url of the api") 191 | parser.addoption("-X","--testrail_flag", 192 | dest="testrail_flag", 193 | default='N', 194 | help="Y or N. 'Y' if you want to report to TestRail") 195 | parser.addoption("-R","--test_run_id", 196 | dest="test_run_id", 197 | default=None, 198 | help="The test run id in TestRail") 199 | parser.addoption("-M","--remote_flag", 200 | dest="remote_flag", 201 | default="N", 202 | help="Run the test in Browserstack/Sauce Lab: Y or N") 203 | parser.addoption("-O","--os_version", 204 | dest="os_version", 205 | action="append", 206 | help="The operating system: xp, 7", 207 | default=[]) 208 | parser.addoption("--ver", 209 | dest="browser_version", 210 | action="append", 211 | help="The version of the browser: a whole number", 212 | default=[]) 213 | parser.addoption("-P","--os_name", 214 | dest="os_name", 215 | action="append", 216 | help="The operating system: Windows 7, Linux", 217 | default=[]) 218 | parser.addoption("--remote_project_name", 219 | dest="remote_project_name", 220 | help="The project name if its run in BrowserStack", 221 | default=None) 222 | parser.addoption("--remote_build_name", 223 | dest="remote_build_name", 224 | help="The build name if its run in BrowserStack", 225 | default=None) 226 | parser.addoption("-S","--slack_flag", 227 | dest="slack_flag", 228 | default="N", 229 | help="Post the test report on slack channel: Y or N") 230 | parser.addoption("-G","--mobile_os_name", 231 | dest="mobile_os_name", 232 | help="Enter operating system of mobile. Ex: Android, iOS", 233 | default="Android") 234 | parser.addoption("-H","--mobile_os_version", 235 | dest="mobile_os_version", 236 | help="Enter version of operating system of mobile: 8.1.0", 237 | default="8.0") 238 | parser.addoption("-I","--device_name", 239 | dest="device_name", 240 | help="Enter device name. Ex: Emulator, physical device name", 241 | default="Google Pixel") 242 | parser.addoption("-J","--app_package", 243 | dest="app_package", 244 | help="Enter name of app package. Ex: bitcoininfo", 245 | default="com.dudam.rohan.bitcoininfo") 246 | parser.addoption("-K","--app_activity", 247 | dest="app_activity", 248 | help="Enter name of app activity. Ex: .MainActivity", 249 | default=".MainActivity") 250 | parser.addoption("-Q","--device_flag", 251 | dest="device_flag", 252 | help="Enter Y or N. 'Y' if you want to run the test on device. 'N' if you want to run the test on emulator.", 253 | default="N") 254 | parser.addoption("--email_pytest_report", 255 | dest="email_pytest_report", 256 | help="Email pytest report: Y or N", 257 | default="N") 258 | parser.addoption("--tesults", 259 | dest="tesults_flag", 260 | default='N', 261 | help="Y or N. 'Y' if you want to report results with Tesults") 262 | parser.addoption("-D","--app_name", 263 | dest="app_name", 264 | help="Enter application name to be uploaded.Ex:Bitcoin Info_com.dudam.rohan.bitcoininfo.apk.", 265 | default="Bitcoin Info_com.dudam.rohan.bitcoininfo.apk") 266 | parser.addoption("-N","--app_path", 267 | dest="app_path", 268 | help="Enter app path") 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /utils/xpath_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to generate XPaths for the given URL 3 | * Take the input URL from the user 4 | * Parse the HTML content using beautifilsoup 5 | * Find all Input and Button tags 6 | * Guess the XPaths 7 | * Generate Variable names for the xpaths 8 | * To run the script in Gitbash use command 'python -u utils/xpath_util.py' 9 | 10 | """ 11 | 12 | from selenium import webdriver 13 | from bs4 import BeautifulSoup 14 | import re 15 | 16 | class Xpath_Util: 17 | "Class to generate the xpaths" 18 | 19 | def __init__(self): 20 | "Initialize the required variables" 21 | self.elements = None 22 | self.guessable_elements = ['input','button'] 23 | self.known_attribute_list = ['id','name','placeholder','value','title','type','class'] 24 | self.variable_names = [] 25 | self.button_text_lists = [] 26 | self.language_counter = 1 27 | 28 | def generate_xpath(self,soup): 29 | "generate the xpath and assign the variable names" 30 | result_flag = False 31 | try: 32 | for guessable_element in self.guessable_elements: 33 | self.elements = soup.find_all(guessable_element) 34 | for element in self.elements: 35 | if (not element.has_attr("type")) or (element.has_attr("type") and element['type'] != "hidden"): 36 | for attr in self.known_attribute_list: 37 | if element.has_attr(attr): 38 | locator = self.guess_xpath(guessable_element,attr,element) 39 | if len(driver.find_elements_by_xpath(locator))==1: 40 | result_flag = True 41 | variable_name = self.get_variable_names(element) 42 | # checking for the unique variable names 43 | if variable_name != '' and variable_name not in self.variable_names: 44 | self.variable_names.append(variable_name) 45 | print ("%s_%s = %s"%(guessable_element, variable_name.encode('utf-8'), locator.encode('utf-8'))) 46 | break 47 | else: 48 | print (locator.encode('utf-8') + "----> Couldn't generate appropriate variable name for this xpath") 49 | break 50 | elif guessable_element == 'button' and element.getText(): 51 | button_text = element.getText() 52 | if element.getText() == button_text.strip(): 53 | locator = xpath_obj.guess_xpath_button(guessable_element,"text()",element.getText()) 54 | else: 55 | locator = xpath_obj.guess_xpath_using_contains(guessable_element,"text()",button_text.strip()) 56 | if len(driver.find_elements_by_xpath(locator))==1: 57 | result_flag = True 58 | #Check for ascii characters in the button_text 59 | matches = re.search(r"[^\x00-\x7F]",button_text) 60 | if button_text.lower() not in self.button_text_lists: 61 | self.button_text_lists.append(button_text.lower()) 62 | if not matches: 63 | # Striping and replacing characters before printing the variable name 64 | print ("%s_%s = %s"%(guessable_element,button_text.strip().strip("!?.").encode('utf-8').lower().replace(" + ","_").replace(" & ","_").replace(" ","_"), locator.encode('utf-8'))) 65 | else: 66 | # printing the variable name with ascii characters along with language counter 67 | print ("%s_%s_%s = %s"%(guessable_element,"foreign_language",self.language_counter, locator.encode('utf-8')) + "---> Foreign language found, please change the variable name appropriately") 68 | self.language_counter +=1 69 | else: 70 | # if the variable name is already taken 71 | print (locator.encode('utf-8') + "----> Couldn't generate appropriate variable name for this xpath") 72 | break 73 | except Exception as e: 74 | print ("Exception when trying to generate xpath for:%s"%guessable_element) 75 | print ("Python says:%s"%str(e)) 76 | 77 | return result_flag 78 | 79 | def get_variable_names(self,element): 80 | "generate the variable names for the xpath" 81 | # condition to check the length of the 'id' attribute and ignore if there are numerics in the 'id' attribute. Also ingnoring id values having "input" and "button" strings. 82 | if (element.has_attr('id') and len(element['id'])>2) and bool(re.search(r'\d', element['id'])) == False and ("input" not in element['id'].lower() and "button" not in element['id'].lower()): 83 | self.variable_name = element['id'].strip("_") 84 | # condition to check if the 'value' attribute exists and not having date and time values in it. 85 | elif element.has_attr('value') and element['value'] != '' and bool(re.search(r'([\d]{1,}([/-]|\s|[.])?)+(\D+)?([/-]|\s|[.])?[[\d]{1,}',element['value']))== False and bool(re.search(r'\d{1,2}[:]\d{1,2}\s+((am|AM|pm|PM)?)',element['value']))==False: 86 | # condition to check if the 'type' attribute exists 87 | # getting the text() value if the 'type' attribute value is in 'radio','submit','checkbox','search' 88 | # if the text() is not '', getting the getText() value else getting the 'value' attribute 89 | # for the rest of the type attributes printing the 'type'+'value' attribute values. Doing a check to see if 'value' and 'type' attributes values are matching. 90 | if (element.has_attr('type')) and (element['type'] in ('radio','submit','checkbox','search')): 91 | if element.getText() !='': 92 | self.variable_name = element['type']+ "_" + element.getText().strip().strip("_.") 93 | else: 94 | self.variable_name = element['type']+ "_" + element['value'].strip("_.") 95 | else: 96 | if element['type'].lower() == element['value'].lower(): 97 | self.variable_name = element['value'].strip("_.") 98 | else: 99 | self.variable_name = element['type']+ "_" + element['value'].strip("_.") 100 | # condition to check if the "name" attribute exists and if the length of "name" attribute is more than 2 printing variable name 101 | elif element.has_attr('name') and len(element['name'])>2: 102 | self.variable_name = element['name'].strip("_") 103 | # condition to check if the "placeholder" attribute exists and is not having any numerics in it. 104 | elif element.has_attr('placeholder') and bool(re.search(r'\d', element['placeholder'])) == False: 105 | self.variable_name = element['placeholder'].strip("_?*.").encode('ascii',errors='ignore') 106 | # condition to check if the "type" attribute exists and not in text','radio','button','checkbox','search' 107 | # and printing the variable name 108 | elif (element.has_attr('type')) and (element['type'] not in ('text','button','radio','checkbox','search')): 109 | self.variable_name = element['type'] 110 | # condition to check if the "title" attribute exists 111 | elif element.has_attr('title'): 112 | self.variable_name = element['title'] 113 | # condition to check if the "role" attribute exists 114 | elif element.has_attr('role') and element['role']!="button": 115 | self.variable_name = element['role'] 116 | else: 117 | self.variable_name = '' 118 | 119 | return self.variable_name.lower().replace("+/- ","").replace("| ","").replace(" / ","_"). \ 120 | replace("/","_").replace(" - ","_").replace(" ","_").replace("&","").replace("-","_"). \ 121 | replace("[","_").replace("]","").replace(",","").replace("__","_").replace(".com","").strip("_") 122 | 123 | 124 | def guess_xpath(self,tag,attr,element): 125 | "Guess the xpath based on the tag,attr,element[attr]" 126 | #Class attribute returned as a unicodeded list, so removing 'u from the list and joining back 127 | if type(element[attr]) is list: 128 | element[attr] = [i.encode('utf-8') for i in element[attr]] 129 | element[attr] = ' '.join(element[attr]) 130 | self.xpath = "//%s[@%s='%s']"%(tag,attr,element[attr]) 131 | 132 | return self.xpath 133 | 134 | 135 | def guess_xpath_button(self,tag,attr,element): 136 | "Guess the xpath for button tag" 137 | self.button_xpath = "//%s[%s='%s']"%(tag,attr,element) 138 | 139 | return self.button_xpath 140 | 141 | def guess_xpath_using_contains(self,tag,attr,element): 142 | "Guess the xpath using contains function" 143 | self.button_contains_xpath = "//%s[contains(%s,'%s')]"%(tag,attr,element) 144 | 145 | return self.button_contains_xpath 146 | 147 | 148 | #-------START OF SCRIPT-------- 149 | if __name__ == "__main__": 150 | print ("Start of %s"%__file__) 151 | 152 | #Initialize the xpath object 153 | xpath_obj = Xpath_Util() 154 | 155 | #Get the URL and parse 156 | url = input("Enter URL: ") 157 | 158 | #Create a chrome session 159 | driver = webdriver.Chrome() 160 | driver.get(url) 161 | 162 | #Parsing the HTML page with BeautifulSoup 163 | page = driver.execute_script("return document.body.innerHTML").encode('utf-8') #returns the inner HTML as a string 164 | soup = BeautifulSoup(page, 'html.parser') 165 | 166 | #execute generate_xpath 167 | if xpath_obj.generate_xpath(soup) is False: 168 | print ("No XPaths generated for the URL:%s"%url) 169 | 170 | driver.quit() -------------------------------------------------------------------------------- /page_objects/DriverFactory.py: -------------------------------------------------------------------------------- 1 | """ 2 | DriverFactory class 3 | NOTE: Change this class as you add support for: 4 | 1. SauceLabs/BrowserStack 5 | 2. More browsers like Opera 6 | """ 7 | import dotenv,os,sys,requests,json 8 | from datetime import datetime 9 | from selenium import webdriver 10 | from selenium.webdriver.common.keys import Keys 11 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 12 | from selenium.webdriver.chrome import service 13 | from selenium.webdriver.remote.webdriver import RemoteConnection 14 | from appium import webdriver as mobile_webdriver 15 | from conf import remote_credentials 16 | from conf import opera_browser_conf 17 | 18 | class DriverFactory(): 19 | 20 | def __init__(self,browser='ff',browser_version=None,os_name=None): 21 | "Constructor for the Driver factory" 22 | self.browser=browser 23 | self.browser_version=browser_version 24 | self.os_name=os_name 25 | 26 | 27 | def get_web_driver(self,remote_flag,os_name,os_version,browser,browser_version,remote_project_name,remote_build_name): 28 | "Return the appropriate driver" 29 | if (remote_flag.lower() == 'y'): 30 | try: 31 | if remote_credentials.REMOTE_BROWSER_PLATFORM == 'BS': 32 | web_driver = self.run_browserstack(os_name,os_version,browser,browser_version,remote_project_name,remote_build_name) 33 | else: 34 | web_driver = self.run_sauce_lab(os_name,os_version,browser,browser_version) 35 | 36 | except Exception as e: 37 | print("\nException when trying to get remote webdriver:%s"%sys.modules[__name__]) 38 | print("Python says:%s"%str(e)) 39 | print("SOLUTION: It looks like you are trying to use a cloud service provider (BrowserStack or Sauce Labs) to run your test. \nPlease make sure you have updated ./conf/remote_credentials.py with the right credentials and try again. \nTo use your local browser please run the test with the -M N flag.\n") 40 | 41 | elif (remote_flag.lower() == 'n'): 42 | web_driver = self.run_local(os_name,os_version,browser,browser_version) 43 | else: 44 | print("DriverFactory does not know the browser: ",browser) 45 | web_driver = None 46 | 47 | return web_driver 48 | 49 | 50 | def run_browserstack(self,os_name,os_version,browser,browser_version,remote_project_name,remote_build_name): 51 | "Run the test in browser stack when remote flag is 'Y'" 52 | #Get the browser stack credentials from browser stack credentials file 53 | USERNAME = remote_credentials.USERNAME 54 | PASSWORD = remote_credentials.ACCESS_KEY 55 | if browser.lower() == 'ff' or browser.lower() == 'firefox': 56 | desired_capabilities = DesiredCapabilities.FIREFOX 57 | elif browser.lower() == 'ie': 58 | desired_capabilities = DesiredCapabilities.INTERNETEXPLORER 59 | elif browser.lower() == 'chrome': 60 | desired_capabilities = DesiredCapabilities.CHROME 61 | elif browser.lower() == 'opera': 62 | desired_capabilities = DesiredCapabilities.OPERA 63 | elif browser.lower() == 'safari': 64 | desired_capabilities = DesiredCapabilities.SAFARI 65 | desired_capabilities['os'] = os_name 66 | desired_capabilities['os_version'] = os_version 67 | desired_capabilities['browser_version'] = browser_version 68 | if remote_project_name is not None: 69 | desired_capabilities['project'] = remote_project_name 70 | if remote_build_name is not None: 71 | desired_capabilities['build'] = remote_build_name+"_"+str(datetime.now().strftime("%c")) 72 | 73 | return webdriver.Remote(RemoteConnection("http://%s:%s@hub-cloud.browserstack.com/wd/hub"%(USERNAME,PASSWORD),resolve_ip= False), 74 | desired_capabilities=desired_capabilities) 75 | 76 | 77 | def run_sauce_lab(self,os_name,os_version,browser,browser_version): 78 | "Run the test in sauce labs when remote flag is 'Y'" 79 | #Get the sauce labs credentials from sauce.credentials file 80 | USERNAME = remote_credentials.USERNAME 81 | PASSWORD = remote_credentials.ACCESS_KEY 82 | if browser.lower() == 'ff' or browser.lower() == 'firefox': 83 | desired_capabilities = DesiredCapabilities.FIREFOX 84 | elif browser.lower() == 'ie': 85 | desired_capabilities = DesiredCapabilities.INTERNETEXPLORER 86 | elif browser.lower() == 'chrome': 87 | desired_capabilities = DesiredCapabilities.CHROME 88 | elif browser.lower() == 'opera': 89 | desired_capabilities = DesiredCapabilities.OPERA 90 | elif browser.lower() == 'safari': 91 | desired_capabilities = DesiredCapabilities.SAFARI 92 | desired_capabilities['version'] = browser_version 93 | desired_capabilities['platform'] = os_name + ' '+os_version 94 | 95 | 96 | return webdriver.Remote(command_executor="http://%s:%s@ondemand.saucelabs.com:80/wd/hub"%(USERNAME,PASSWORD), 97 | desired_capabilities= desired_capabilities) 98 | 99 | 100 | def run_local(self,os_name,os_version,browser,browser_version): 101 | "Return the local driver" 102 | local_driver = None 103 | if browser.lower() == "ff" or browser.lower() == 'firefox': 104 | local_driver = webdriver.Firefox() 105 | elif browser.lower() == "ie": 106 | local_driver = webdriver.Ie() 107 | elif browser.lower() == "chrome": 108 | local_driver = webdriver.Chrome() 109 | elif browser.lower() == "opera": 110 | opera_options = None 111 | try: 112 | opera_browser_location = opera_browser_conf.location 113 | options = webdriver.ChromeOptions() 114 | options.binary_location = opera_browser_location # path to opera executable 115 | local_driver = webdriver.Opera(options=options) 116 | 117 | except Exception as e: 118 | print("\nException when trying to get remote webdriver:%s"%sys.modules[__name__]) 119 | print("Python says:%s"%str(e)) 120 | if 'no Opera binary' in str(e): 121 | print("SOLUTION: It looks like you are trying to use Opera Browser. Please update Opera Browser location under conf/opera_browser_conf.\n") 122 | elif browser.lower() == "safari": 123 | local_driver = webdriver.Safari() 124 | 125 | return local_driver 126 | 127 | 128 | def run_mobile(self,mobile_os_name,mobile_os_version,device_name,app_package,app_activity,remote_flag,device_flag,app_name,app_path): 129 | "Setup mobile device" 130 | #Get the remote credentials from remote_credentials file 131 | USERNAME = remote_credentials.USERNAME 132 | PASSWORD = remote_credentials.ACCESS_KEY 133 | desired_capabilities = {} 134 | desired_capabilities['platformName'] = mobile_os_name 135 | desired_capabilities['platformVersion'] = mobile_os_version 136 | desired_capabilities['deviceName'] = device_name 137 | 138 | if (remote_flag.lower() == 'y'): 139 | desired_capabilities['idleTimeout'] = 300 140 | desired_capabilities['name'] = 'Appium Python Test' 141 | try: 142 | if remote_credentials.REMOTE_BROWSER_PLATFORM == 'SL': 143 | self.sauce_upload(app_path,app_name) #Saucelabs expects the app to be uploaded to Sauce storage everytime the test is run 144 | #Checking if the app_name is having spaces and replacing it with blank 145 | if ' ' in app_name: 146 | app_name = app_name.replace(' ','') 147 | desired_capabilities['app'] = 'sauce-storage:'+app_name 148 | desired_capabilities['autoAcceptAlert']= 'true' 149 | driver = mobile_webdriver.Remote(command_executor="http://%s:%s@ondemand.saucelabs.com:80/wd/hub"%(USERNAME,PASSWORD), 150 | desired_capabilities= desired_capabilities) 151 | else: 152 | desired_capabilities['realMobile'] = 'true' 153 | desired_capabilities['app'] = self.browser_stack_upload(app_name,app_path) #upload the application to the Browserstack Storage 154 | driver = mobile_webdriver.Remote(command_executor="http://%s:%s@hub.browserstack.com:80/wd/hub"%(USERNAME,PASSWORD), 155 | desired_capabilities= desired_capabilities) 156 | except Exception as e: 157 | print ('\033[91m'+"\nException when trying to get remote webdriver:%s"%sys.modules[__name__]+'\033[0m') 158 | print ('\033[91m'+"Python says:%s"%str(e)+'\033[0m') 159 | print ('\033[92m'+"SOLUTION: It looks like you are trying to use a cloud service provider (BrowserStack or Sauce Labs) to run your test. \nPlease make sure you have updated ./conf/remote_credentials.py with the right credentials and try again. \nTo use your local browser please run the test with the -M N flag.\n"+'\033[0m') 160 | else: 161 | try: 162 | desired_capabilities['appPackage'] = app_package 163 | desired_capabilities['appActivity'] = app_activity 164 | if device_flag.lower() == 'y': 165 | driver = mobile_webdriver.Remote('http://localhost:4723/wd/hub', desired_capabilities) 166 | else: 167 | desired_capabilities['app'] = os.path.join(app_path,app_name) 168 | driver = mobile_webdriver.Remote('http://localhost:4723/wd/hub', desired_capabilities) 169 | except Exception as e: 170 | print ('\033[91m'+"\nException when trying to get remote webdriver:%s"%sys.modules[__name__]+'\033[0m') 171 | print ('\033[91m'+"Python says:%s"%str(e)+'\033[0m') 172 | print ('\033[92m'+"SOLUTION: It looks like you are trying to run test cases with Local Appium Setup. \nPlease make sure to run Appium Server and try again.\n"+'\033[0m') 173 | 174 | return driver 175 | 176 | 177 | def sauce_upload(self,app_path,app_name): 178 | "Upload the apk to the sauce temperory storage" 179 | USERNAME = remote_credentials.USERNAME 180 | PASSWORD = remote_credentials.ACCESS_KEY 181 | result_flag=False 182 | try: 183 | headers = {'Content-Type':'application/octet-stream'} 184 | params = os.path.join(app_path,app_name) 185 | fp = open(params,'rb') 186 | data = fp.read() 187 | fp.close() 188 | #Checking if the app_name is having spaces and replacing it with blank 189 | if ' ' in app_name: 190 | app_name = app_name.replace(' ','') 191 | print ("The app file name is having spaces, hence replaced the white spaces with blank in the file name:%s"%app_name) 192 | response = requests.post('https://saucelabs.com/rest/v1/storage/%s/%s?overwrite=true'%(USERNAME,app_name),headers=headers,data=data,auth=(USERNAME,PASSWORD)) 193 | if response.status_code == 200: 194 | result_flag=True 195 | print ("App successfully uploaded to sauce storage") 196 | except Exception as e: 197 | print (str(e)) 198 | 199 | return result_flag 200 | 201 | def browser_stack_upload(self,app_name,app_path): 202 | "Upload the apk to the BrowserStack storage if its not done earlier" 203 | USERNAME = remote_credentials.USERNAME 204 | ACESS_KEY = remote_credentials.ACCESS_KEY 205 | try: 206 | #Upload the apk 207 | apk_file = os.path.join(app_path,app_name) 208 | files = {'file': open(apk_file,'rb')} 209 | post_response = requests.post("https://api.browserstack.com/app-automate/upload",files=files,auth=(USERNAME,ACESS_KEY)) 210 | post_json_data = json.loads(post_response.text) 211 | #Get the app url of the newly uploaded apk 212 | app_url = post_json_data['app_url'] 213 | except Exception as e: 214 | print(str(e)) 215 | 216 | return app_url 217 | 218 | 219 | def get_firefox_driver(self): 220 | "Return the Firefox driver" 221 | driver = webdriver.Firefox(firefox_profile=self.get_firefox_profile()) 222 | 223 | return driver 224 | 225 | 226 | def get_firefox_profile(self): 227 | "Return a firefox profile" 228 | 229 | return self.set_firefox_profile() 230 | 231 | 232 | def set_firefox_profile(self): 233 | "Setup firefox with the right preferences and return a profile" 234 | try: 235 | self.download_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','downloads')) 236 | if not os.path.exists(self.download_dir): 237 | os.makedirs(self.download_dir) 238 | except Exception as e: 239 | print("Exception when trying to set directory structure") 240 | print(str(e)) 241 | 242 | profile = webdriver.firefox.firefox_profile.FirefoxProfile() 243 | set_pref = profile.set_preference 244 | set_pref('browser.download.folderList', 2) 245 | set_pref('browser.download.dir', self.download_dir) 246 | set_pref('browser.download.useDownloadDir', True) 247 | set_pref('browser.helperApps.alwaysAsk.force', False) 248 | set_pref('browser.helperApps.neverAsk.openFile', 'text/csv,application/octet-stream,application/pdf') 249 | set_pref('browser.helperApps.neverAsk.saveToDisk', 'text/csv,application/vnd.ms-excel,application/pdf,application/csv,application/octet-stream') 250 | set_pref('plugin.disable_full_page_plugin_for_types', 'application/pdf') 251 | set_pref('pdfjs.disabled',True) 252 | 253 | return profile 254 | 255 | --------------------------------------------------------------------------------