├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── greetings.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Examples ├── .gitkeep ├── FilesCompare │ ├── .gitkeep │ ├── highway.jpg │ ├── highway_altered.jpg │ ├── samplecomparisontest.py │ ├── source.xlsx │ ├── source_csv.csv │ ├── target.xlsx │ └── target_csv.csv ├── OCR_Example_Image │ └── example_01.png ├── sample_captcha.py ├── samplerestapitest.py ├── samplesaucelabseleniumtest.py ├── sampleseleniumtest.py ├── samplespellcheckker.py └── samplewebspidertest.py ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── Tests └── TestSeleniumKeywords.py ├── _config.yml ├── chrome_driver_install.sh ├── prodigyqa ├── __init__.py ├── apitester.py ├── browseractions.py ├── comparison.py ├── setup_scrapper.tmpl ├── spellchecker.py ├── spider.py └── utils.py ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank you so much for your first observation.'' first issue' 13 | pr-message: 'Thanks a ton for your contributions'' first pr' 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | # sudo: false 4 | 5 | matrix: 6 | include: 7 | - python: 3.5 8 | - python: 3.6 9 | - python: 3.7 10 | dist: xenial 11 | sudo: true 12 | 13 | addons: 14 | firefox: latest 15 | chrome: stable 16 | 17 | 18 | before_script: 19 | - "sudo apt-get install python-opencv" 20 | # - bash chrome_driver_install.sh 21 | 22 | script: 23 | # - pytest Examples/samplerestapitest.py 24 | # - pytest Examples/FilesCompare/samplecomparisontest.py 25 | - "flake8 prodigyqa/*.py" 26 | - "flake8 Examples/*.py && flake8 Examples/*/*.py" 27 | - "pycodestyle prodigyqa/*.py && pycodestyle Examples/*/*.py" 28 | 29 | deploy: 30 | provider: pypi 31 | user: Revant 32 | password: Revantpypi 33 | distributions: "sdist bdist_wheel" 34 | skip_existing: true 35 | skip_cleanup: true 36 | on: 37 | branch: master 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at prodigyqa@googlegroups.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at prodigyqa@googlegroups.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /Examples/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/.gitkeep -------------------------------------------------------------------------------- /Examples/FilesCompare/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Examples/FilesCompare/highway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/FilesCompare/highway.jpg -------------------------------------------------------------------------------- /Examples/FilesCompare/highway_altered.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/FilesCompare/highway_altered.jpg -------------------------------------------------------------------------------- /Examples/FilesCompare/samplecomparisontest.py: -------------------------------------------------------------------------------- 1 | """Example for Comparison module.""" 2 | from prodigyqa import Compare 3 | 4 | # Variable Stack / Data 5 | image1 = "Examples/FilesCompare/highway.jpg" 6 | image2 = "Examples/FilesCompare/highway_altered.jpg" 7 | source_xl = "Examples/FilesCompare/source.xlsx" 8 | target_xl = "Examples/FilesCompare/target.xlsx" 9 | source_csv = "Examples/FilesCompare/source_csv.csv" 10 | target_csv = "Examples/FilesCompare/target_csv.csv" 11 | source_json = "{'as': 1," \ 12 | "'a': {'b': {'cs':10, 'qqq': {'qwe':1}}," \ 13 | "'d': {'csd':30}}}" 14 | target_json = "{'as': 1," \ 15 | "'a': {'b': {'ds':10, 'qqq': {'qwe':11}}," \ 16 | "'d': {'dsd':40}}}" 17 | 18 | 19 | class TestCompareFiles(Compare): 20 | """Sample Test Suite.""" 21 | 22 | def test_compare_images(self): 23 | """Compare images.""" 24 | self.assertEqual(self.compare_images(image1, image1), 1.0) 25 | 26 | def test_compare_jsons(self): 27 | """Compare jsons.""" 28 | self.assertNotEqual(self.compare_json(source_json, target_json), '{}') 29 | 30 | def test_compare_workbooks(self): 31 | """Compare spreadsheet. 32 | 33 | xl file will be generated with file difference. 34 | """ 35 | self.compare_files(source_xl, target_xl) 36 | 37 | def test_compare_files(self): 38 | """Compare spreadsheet. 39 | 40 | xl file will be generated with file difference. 41 | """ 42 | self.compare_files(source_csv, target_csv) 43 | -------------------------------------------------------------------------------- /Examples/FilesCompare/source.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/FilesCompare/source.xlsx -------------------------------------------------------------------------------- /Examples/FilesCompare/source_csv.csv: -------------------------------------------------------------------------------- 1 | Year,Industry_aggregation_NZSIOC,Industry_code_NZSIOC,Industry_name_NZSIOC,Units,Variable_code,Variable_name,Variable_category,Value,Industry_code_ANZSIC06 2 | 2017,Level 4,AA111,Horticulture and Fruit Growing,Dollars (millions),H04,"Sales, government funding, grants and subsidies",Financial performance,3999,"ANZSIC06 groups A011, A012, and A013" 3 | 2017,Level 4,AA111,Horticulture and Fruit Growing,Dollars (millions),H04,"Sales, government funding, grants and subsidies",Financial performance,3999,"ANZSIC06 groups A011, A012, and A013" -------------------------------------------------------------------------------- /Examples/FilesCompare/target.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/FilesCompare/target.xlsx -------------------------------------------------------------------------------- /Examples/FilesCompare/target_csv.csv: -------------------------------------------------------------------------------- 1 | Year,Industry_aggregation_NZSIOC,Industry_code_NZSIOC,Industry_name_NZSIOC,Units,Variable_code,Variable_name,Variable_category,Value,Industry_code_ANZSIC06 2 | 2017,Level 4,AA111,Horticulture and Fruit Growing,Dollars (millions),H04,"Sales, government funding, grants and subsidies",Financial performance,3999,"ANZSIC06 groups A011, A012, and A013" 3 | 2016,Level 3,AA111,Horticulture and Fruit Growing,Dollars (millions),H03,"Sales, government funding, grants and subsidies",Financial performance,3998,"ANZSIC06 groups A011, A012, and A014" -------------------------------------------------------------------------------- /Examples/OCR_Example_Image/example_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyqa/prodigyqa/85772d40f6306b3ba4e05ca19ca10f7434fab5f3/Examples/OCR_Example_Image/example_01.png -------------------------------------------------------------------------------- /Examples/sample_captcha.py: -------------------------------------------------------------------------------- 1 | """CAPTCHA reading Sample Test file.""" 2 | from prodigyqa.utils import crackcaptcha 3 | 4 | 5 | # Variable stack 6 | 7 | imgpath = "OCR_Example_Image\\example_01.png" 8 | 9 | 10 | class TestClass(crackcaptcha): 11 | """Sample Test Suite.""" 12 | 13 | def test_read_captcha(self): 14 | """Get users from application.""" 15 | print(self.captcharead(imgpath)) 16 | -------------------------------------------------------------------------------- /Examples/samplerestapitest.py: -------------------------------------------------------------------------------- 1 | """REST Api Sample Test file.""" 2 | from prodigyqa import ApiTester 3 | import json 4 | import logging 5 | import pytest 6 | 7 | # Variable Stack 8 | login_uri = 'https://reqres.in/api/login' 9 | uri = "https://reqres.in/api/users" 10 | credentials = {"email": "peter@klaven", 11 | "password": "cityslicka"} 12 | payload = {"name": "morpheus", 13 | "job": "leader"} 14 | header = {'Accept': 'application/json', 15 | 'Content-Type': 'application/json'} 16 | 17 | 18 | class TestClass(ApiTester): 19 | """Sample Test Suite.""" 20 | 21 | @pytest.mark.get 22 | def test_get_users(self): 23 | """Get users from application.""" 24 | users = self.apirequest(method="GET", url=uri) 25 | resp = json.loads(users.text) 26 | logging.info(resp) 27 | self.assert_in_resp(resp=resp, member='orge', 28 | container='resp.data.0.first_name') 29 | self.assert_equal_resp(resp=resp, member='George', 30 | container='resp.data.0.first_name') 31 | self.assert_not_equal_resp(resp=resp, member='George1', 32 | container='resp.data.0.first_name') 33 | self.assert_not_in_resp(resp=resp, member='John', 34 | container='resp.data.0.first_name') 35 | 36 | @pytest.mark.post 37 | def test_post_users(self): 38 | """Create users.""" 39 | users = self.apirequest(method="post", url=uri, data=payload) 40 | resp = json.loads(users.text) 41 | logging.warning(resp) 42 | self.assert_not_in_resp(resp, member='2018-11-20', 43 | container='resp.createdAt') 44 | -------------------------------------------------------------------------------- /Examples/samplesaucelabseleniumtest.py: -------------------------------------------------------------------------------- 1 | """Sample test scripts for saucelab-python integration.""" 2 | 3 | import pytest 4 | from selenium.webdriver.common.keys import Keys 5 | from selenium import webdriver 6 | from prodigyqa import BrowserActions 7 | 8 | 9 | class Page: 10 | """Stored all the needed variables at one place.""" 11 | 12 | base_url = "http://www.python.org" 13 | fb_base_url = "http://www.facebook.com" 14 | two_url = "https://xkcd.com/979/" 15 | search_box = {"locatorvalue": 'id-search-field', 16 | "by": "By.ID", "value": "selenium " + Keys.ENTER} 17 | button = {"locatorvalue": '#topLeft > ul > li:nth-child(1) > a', 18 | "by": "By.CSS_SELECTOR"} 19 | # Use your sauce lab credentials. 20 | username = '' 21 | access_key = '' 22 | desired_cap = { 23 | 'platform': "Mac OS X 10.12", 24 | 'browserName': "chrome", 25 | 'version': "latest", 26 | } 27 | 28 | 29 | @pytest.fixture(scope="class") 30 | def setup_teardown_fixture(request): 31 | """Fixture for class level setup and teardown methods. 32 | 33 | :param request: request for a particular functionality. 34 | :return: NA 35 | :rtype: NA 36 | """ 37 | class setup_teardown(BrowserActions): 38 | def __init__(self): 39 | """Configuring remote web driver. 40 | 41 | :param: NA 42 | :return: NA 43 | :rtype: NA 44 | """ 45 | self.driver = webdriver.Remote( 46 | command_executor='https://{}:{}@ondemand.saucelabs.' 47 | 'com/wd/hub'.format(Page.username, 48 | Page.access_key), 49 | desired_capabilities=Page.desired_cap) 50 | 51 | request.cls.st = setup_teardown() 52 | yield 53 | request.cls.st.driver.quit() 54 | 55 | 56 | @pytest.mark.usefixtures("setup_teardown_fixture") 57 | class TestClass(): 58 | 59 | def test_python_search(self): 60 | self.st.open(Page.base_url) 61 | self.st.set_window_size(1200, 800) 62 | self.st.maximize() 63 | self.st.assertIn("Python", self.st.get_title()) 64 | self.st.click(Page.search_box) 65 | self.st.send_keys(Page.search_box) 66 | self.st.open(Page.two_url) 67 | self.st.click(Page.button) 68 | 69 | def test_facebook_search(self): 70 | self.st.open(Page.fb_base_url) 71 | self.st.set_window_size(1200, 800) 72 | self.st.maximize() 73 | self.st.assertIn("Facebook", self.st.get_title()) 74 | -------------------------------------------------------------------------------- /Examples/sampleseleniumtest.py: -------------------------------------------------------------------------------- 1 | """Sample test scripts for selenium pytest sample.""" 2 | 3 | from prodigyqa import BrowserActions 4 | import os 5 | from selenium.webdriver.chrome.options import Options 6 | from time import sleep 7 | 8 | chrome_options = Options() 9 | 10 | 11 | class PageObjects: 12 | """hold all locators for POM style implementation.""" 13 | 14 | base_url = "http://the-internet.herokuapp.com/" 15 | RadioButtons_all = \ 16 | {"bmwButton": {"locatorvalue": 'bmwradio', 17 | "by": "By.ID", 18 | "value": 'ch_signup_icon'}, 19 | "benzButton": 20 | {"locatorvalue": 'benzradio', 21 | "by": "By.ID", 22 | "value": 'ch_signup_icon'}} 23 | SelectButton1 = {"locatorvalue": 'carselect', 24 | "by": "By.ID", "value": 'benz'} 25 | SelectButton2 = {"locatorvalue": 'carselect', 26 | "by": "By.ID", "value": 'bmw'} 27 | checkBox_all = { 28 | "checkbox1": 29 | {"locatorvalue": 'input[type="checkbox"]:nth-child(1)', 30 | "by": "By.CSS_SELECTOR", 31 | "value": '#checkboxes > input[type="checkbox"]:nth-child(1)'} 32 | } 33 | buttons_all = { 34 | "dropdown": {"locatorvalue": 'dropdown', 35 | "by": "By.ID", "value": 'openwindow'}, 36 | "option1": { 37 | "locatorvalue": '#dropdown > option:nth-child(2)', 38 | "by": "By.CSS_SELECTOR", "value": '1'}, 39 | "option2": { 40 | "locatorvalue": '#dropdown > option:nth-child(3)', 41 | "by": "By.CSS_SELECTOR", "value": '2'}, 42 | "mousehover": {"locatorvalue": 'mousehover', "by": "By.ID", 43 | "value": 'mousehover'}, 44 | 'js_alert': { 45 | "locatorvalue": '#content > div > ul > li:nth-child(1) > button', 46 | "by": "By.CSS_SELECTOR", 47 | "value": '#content > div > ul > li:nth-child(1) > button'}, 48 | 'js_confirm_alert': { 49 | "locatorvalue": '#content > div > ul > li:nth-child(2) > button', 50 | "by": "By.CSS_SELECTOR", 51 | "value": '#content > div > ul > li:nth-child(2) > button'}, 52 | 'js_prompt_alert': { 53 | "locatorvalue": '#content > div > ul > li:nth-child(3) > button', 54 | "by": "By.CSS_SELECTOR", 55 | "value": '#content > div > ul > li:nth-child(3) > button'}, 56 | 'enable_btn': { 57 | 'locatorvalue': '#input-example > button', 58 | 'by': 'By.CSS_SELECTOR', 'value': 'test'} 59 | } 60 | labels_all = { 61 | "header": { 62 | "locatorvalue": '.large-12.columns.atom.text-center>h1', 63 | "by": "By.CSS_SELECTOR", 64 | "value": '.large-12.columns.atom.text-center>h1'}, 65 | "practice": { 66 | "locatorvalue": "/pages/practice", 67 | "by": "By.LINK_TEXT", "value": '/pages/practice'}} 68 | links_all = { 69 | 'checkboxes': { 70 | "locatorvalue": "#content > ul > li:nth-child(5) > a", 71 | "by": "By.CSS_SELECTOR", 72 | "value": '#content > ul > li:nth-child(5) > a'}, 73 | 'page_footer': {"locatorvalue": "#page-footer > div > div > a", 74 | "by": "By.CSS_SELECTOR", 75 | "value": '#page-footer > div > div > a'}, 76 | 'dropdown': { 77 | "locatorvalue": "/dropdown", 78 | "by": "By.LINK_TEXT", "value": '/dropdown'}, 79 | 'alerts': { 80 | "locatorvalue": "#content > ul > li:nth-child(25) > a", 81 | "by": "By.CSS_SELECTOR", 82 | "value": '#content > ul > li:nth-child(25) > a'}, 83 | 'frames': { 84 | "locatorvalue": "#content > ul > li:nth-child(19) > a", 85 | "by": "By.CSS_SELECTOR", 86 | "value": '#content > ul > li:nth-child(19) > a'}, 87 | 'iframe': { 88 | "locatorvalue": "#content > div > ul > li:nth-child(2) > a", 89 | "by": "By.CSS_SELECTOR", 90 | "value": '#content > div > ul > li:nth-child(2) > a'}, 91 | 'dynamic_controls': { 92 | "locatorvalue": "#content > ul > li:nth-child(11) > a", 93 | "by": "By.CSS_SELECTOR", 94 | "value": '#content > ul > li:nth-child(11) > a'}} 95 | text_boxes = { 96 | 'text_in_frame': { 97 | "locatorvalue": "#tinymce > p", "by": "By.CSS_SELECTOR", 98 | "value": 'test message'}, 99 | 'text_box': { 100 | 'locatorvalue': '#input-example > input', 101 | 'by': 'By.CSS_SELECTOR', 'value': 'test'}} 102 | 103 | 104 | class TestClass(BrowserActions): 105 | """Test Class Container for test cases.""" 106 | 107 | def test_browser_actions(self): 108 | """Sample case for navigating to python site and search selenium.""" 109 | self.open(PageObjects.base_url) 110 | self.page_readiness_wait() 111 | self.set_window_size(1200, 800) 112 | self.maximize() 113 | self.assertTrue(str(self.driver_name), 'chrome') 114 | self.assertTrue(str(self.domain_url), PageObjects.base_url) 115 | self.assertTrue(str(self.title), "The Internet") 116 | self.assertTrue(self.get_page_source().startswith(" Examples** folder. To run the sample tests, open command prompt/terminal, go to prodigyqa --> Examples folder and run the following command: 45 | 46 | 47 | `pytest {filename}.py -s` (-s indicates the standard output, please refer [here](https://docs.pytest.org/en/latest/contents.html) for a detailed understanding around pytest framework and its features/plugins/options etc.) 48 | 49 | ## Browser Actions 50 | Browser Actions Module method Summary 51 | --- 52 | This module wraps around selenium web driver functions. 53 | Below are the major areas handled in this module: 54 | * Most frequently, a DOM refresh will cause an exception (StaleElementReferenceException) if the object is changing state. we are handling this by checking Web Page Expected to be in the ready state and Catch the exception to prevent a failure if the object is not present in web page after web page is in the expected state. 55 | * Most of the functions of this module will Catch the exception to prevent a failure. 56 | 57 | | Method Name | Description | Args | Usage | 58 | |---|---|---|---| 59 | | page_readiness_wait | Web Page Expected to be in ready state. | | self.page_readiness_wait() | 60 | | locator_check | Local Method to classify the type of locator. | (a)locator_dict: dictionary of locator value, locator by and value | self.locator_check( locator_dict) | 61 | | open | Open the passed 'url'. | | self.open(url) | 62 | | reload_page | Method to refresh the page by selenium or java script. | | self.reload_page() | 63 | | get_page_source | Return the entire HTML source of the current page or frame. | | self.get_page_source() | 64 | | get_title | Return the title of current page.| | self.get_title()| 65 | | get_location | Return the current browser URL using Selenium/Java Script.| | self.get_location() | 66 | | get_attribute | fetch attribute from provided locator/element/parent element with child locator.| (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). (b) attribute_name: attribute name to get it's value . (c) element: it is a webelement . (d) type : the type value should be 'locator' or 'element' or 'mixed'| self.get_attribute(locator=None, element=None, attribute_name=None, type='locator')| 67 | | click | Click an element. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.click(locator) | 68 | | send_keys | Send text but does not clear the existing text. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). (b) string to send. | self.send_keys(locator) | 69 | | get_text | Get text from provided Locator. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.get_text(locator) | 70 | | go_back | Simulate back button on browser using selenium or js. | | self.go_back() | 71 | | go_forward | Simulate forward button on browser using selenium or js. | | self.go_forward() | 72 | | set_window_size | Set width and height of the current window. (window.resizeTo) | (a) width: the width in pixels to set the window to. (b) height: the height in pixels to set the window to. | self.set_window_size(800,600) | 73 | | maximize | Maximize the current window. | | self.maximize() | 74 | | get_driver_name | Return the name of webdriver instance. | | self.get_driver_name() | 75 | | get_domain_url | Method to extract domain url from webdriver itself. | | self.get_domain_url() | 76 | | clear_text | Clear the text if it's a text entry element | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.clear_text(locator) | 77 | | capture_screenshot | Save screenshot to the directory(existing or new one). | (a) filepath: file name with directory path(C:/images/image.png). | self.capture_screenshot(self, filepath) | 78 | | switch_to_active_element | Return the element with focus, or BODY if nothing has focus. | | self.switch_to_active_element() | 79 | | switch_to_window | Switch focus to the specified window using selenium/javascript. | (a) name of the window to switch | self.switch_to_window(window) | 80 | | switch_to_frame | Switch focus to the specified frame using selenium/javascript. | (a) framename: name of the frame to switch. | self.switch_to_frame(framename) | 81 | | switch_to_default_content | Switch focus to the default frame. | | self.switch_to_default_content() | 82 | | switch_to_alert | Switch focus to an alert on the page. | | self.switch_to_alert() | 83 | | hover_on_element | Hover on a particular element. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.hover_on_element(locator) | 84 | | hover_on_click | Hover & click a particular element. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.hover_on_click(locator) | 85 | | wait_for_element | Wait for an element to exist in UI. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.wait_for_element(locator) | 86 | | wait_and_accept_alert | Wait and accept alert present on the page. | | self.wait_and_accept_alert() | 87 | | wait_and_reject_alert | Wait for alert and rejects. | | self.wait_and_reject_alert() | 88 | | select_option_by_index | Select the option by index. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). (b) index: integer value for index. | self.select_option_by_index(locator, index) | 89 | | select_option_by_value | Select the option by using value. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). (b) value: string value to select option. | self.select_option_by_value(locator, value) | 90 | | select_option_by_text | Select the value by using text. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). (b) text: string value to select option. | self.select_option_by_text(locator, text) | 91 | | scroll_to_footer | Scroll till end of the page. | | self.scroll_to_footer() | 92 | | scroll_to_element | Scroll to a particular element on the page. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.scroll_to_element(locator) | 93 | | javascript_click | Perfom a click using javscript | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.java(locator) | 94 | | is_element_displayed | Return boolean value based on the element is displayed with locator. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.is_element_displayed(locator) | 95 | | is_element_enabled | Return boolean value based on the element is enabled with locator. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.is_element_enabled(locator) | 96 | | is_element_selected | Return boolean value based on the element is selected with locator. | (a) locator: dictionary of identifier type and value ({'by':'id', 'value':'start-of-content.'}). | self.is_element_selected(locator) | 97 | | switch_to_active_window | Switch focus to new window | Switch focus to new window from current window | self.switch_to_active_window() | 98 | | switch_to_frame_by_index | Switch focus to the specified frame based on index | Index of the frame | self.switch_to_frame_by_index(index) | 99 | 100 | 101 | 102 | ## API Test Module 103 | 104 | 105 | REST API Module method Summary 106 | --- 107 | 108 | | Method Name | Description | Args | Usage | 109 | |---|---|---|---| 110 | | apirequest | triggers rest api request based on the input method and kwargs | (a).method: GET/POST/PUT/PATCH/DELETE (b).kwargs: Refer below REST API kwarg section table | self.apirequest(method='GET') | 111 | | assert_in_resp | Check whether response data contain input member.| (a)resp: response to validate. (b)member: value to check in response. (c)container: response key path in dot format which should starts with 'resp.'. example: resp.data.0.name | self.assert_in_resp(resp, member, container) | 112 | | assert_not_in_resp | Check whether response data contain input member.| (a)resp: response to validate. (b)member: value to check in response. (c)container: response key path in dot format which should starts with 'resp.'. example: resp.data.0.name | self.assert_not_in_resp(resp, member, container) | 113 | | assert_equal_resp | Check whether response data contain input member.| (a)resp: response to validate. (b)member: value to check in response. (c)container: response key path in dot format which should starts with 'resp.'. example: resp.data.0.name | self.assert_equal_resp(resp, member, container) | 114 | | assert_not_equal_resp | Check whether response data contain input member.| (a)resp: response to validate. (b)member: value to check in response. (c)container: response key path in dot format which should starts with 'resp.'. example: resp.data.0.name | self.assert_not_equal_resp(resp, member, container) | 115 | 116 | REST API kwarg section 117 | --- 118 | 119 | | Arg Name | Arg type | Description | 120 | |---|---|---| 121 | | url | standard | API request url | 122 | | params | optional | Dictionary, list of tuples or bytes to send in the body of the Request.| 123 | | data | optional | Dictionary, list of tuples, bytes, or file-like object to send in the body of the Request. | 124 | | json | optional | A JSON serializable Python object to send in the body of the Request. | 125 | | headers | optional | Dictionary of HTTP Headers to send with the Request. | 126 | | cookies | optional | Dict or CookieJar object to send with the Request. | 127 | | files | optional | Dictionary of 'name': file-like-objects(or {'name': file-tuple}) for multipart encoding upload. file-tuple can be a 2-tuple ('filename', fileobj), 3-tuple ('filename', fileobj, 'content_type') or a 4-tuple ('filename', fileobj, 'content_type', custom_headers), where 'content-type' is a string defining the content type of the given file and custom_headers a dict-like object containing additional headers to add for the file. | 128 | | auth | optional | Auth tuple to enable Basic/Digest/Custom HTTP Auth. | 129 | | timeout (float or tuple) | optional | How many seconds to wait for the server to send data before giving up, as a float, or a (connect timeout, read timeout) tuple. | 130 | | allow_redirects (bool) | optional | Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to True. | 131 | | proxies | optional | Dictionary mapping protocol to the URL of the proxy. | 132 | | verify | optional | Either a boolean, in which case it controls whether we verify the servers TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to True. | 133 | | stream | optional | if False, the response content will be immediately downloaded. | 134 | | cert | optional | if String, path to ssl client cert file (.pem). If Tuple, (cert, key) pair. | 135 | 136 | 137 | ## Utils 138 | Spell checker of utils Module method Summary 139 | --- 140 | This Spell checker method of utils module will capture web page content and Validate it against the standard dictionary to ensure spell is correct. 141 | 142 | | Method Name | Description | Args | Usage | 143 | |---|---|---|---| 144 | | spell_checker | Spell checker. | (a) url: web page url (b) words: list of application specific words | spell_checker(self, url, words=[]) | 145 | | l 146 | 147 | ## Comparison Module 148 | Module has a utility methods for comparing various file type 149 | 150 | | Method Name | Description | Args | Usage | 151 | | ------------- |:-------------:| -----:| -----: | 152 | | compare_images | Compare images and returns structural similarity over the image. Measure of SSIM is returned between 0-1.0 where 1.0 is the most identical and 0 being completely different. | a) source image path. b)target image path. | self.compare_images(source, target) | 153 | | compare_json | Compare json files and returns dictionary of difference of target compared to source. | a) source json. b)target json | self.compare_json(source, target) | 154 | | compare_files | Compare two files and return xl of difference(if any). SupportedfFile Types are xls or xlsx csv | a) Source file Path. b)target file Path. | self.compare_files(source, target) | 155 | -------------------------------------------------------------------------------- /Tests/TestSeleniumKeywords.py: -------------------------------------------------------------------------------- 1 | """Sample test scripts for selenium pytest sample.""" 2 | import pytest 3 | from prodigyqa import BrowserActions 4 | from selenium.webdriver.chrome.options import Options 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.common.keys import Keys 7 | from prodigyqa import browseractions 8 | from proboscis.asserts import assert_true 9 | import time 10 | from selenium.webdriver.chrome.options import Options 11 | chrome_options = Options() 12 | #chrome_options.add_argument("--headless") 13 | 14 | class PageObjects(): 15 | """hold all locators for POM style implementation.""" 16 | 17 | base_url = "https://learn.letskodeit.com/p/practice" 18 | two_url = "https://xkcd.com/979/" 19 | # search_boxes = {{"locatorvalue": 'id-search-field', 20 | # "by": "By.ID", "value": "selenium " + Keys.ENTER}} 21 | RadioButtons_all = {"bmwButton": {"locatorvalue": 'bmwradio',"by": "By.ID", "value": 'ch_signup_icon'}, 22 | "benzButton":{"locatorvalue": 'benzradio',"by": "By.ID", "value": 'ch_signup_icon'}} 23 | SelectButton1={"locatorvalue": 'carselect',"by": "By.ID", "value": 'benz'} 24 | SelectButton2 = {"locatorvalue": 'carselect', "by": "By.ID", "value": 'bmw'} 25 | checkBox_all = {"bmwbutton": {"locatorvalue": 'bmwcheck',"by": "By.ID", "value": 'bmw'}, 26 | 'benzbutton':{"locatorvalue": 'benzcheck',"by": "By.ID", "value": 'bmw'}} 27 | buttons_all={"openwindow": {"locatorvalue": 'openwindow',"by": "By.ID", "value": 'openwindow'}, 28 | "alertBtn":{"locatorvalue": 'alertbtn',"by": "By.ID", "value": 'alertbtn'}, 29 | "confirmbtn":{"locatorvalue": 'confirmbtn',"by": "By.ID", "value": 'confirmbtn'}, 30 | "mousehover": {"locatorvalue": 'mousehover',"by": "By.ID", "value": 'mousehover'}} 31 | labels_all = {"footer": {"locatorvalue": 'powered-by', "by": "By.CLASS_NAME", "value": 'powered-by'}, 32 | "practice": {"locatorvalue": "/pages/practice", "by": "By.LINK_TEXT", "value": '/pages/practice'}} 33 | links_all = {'top_mousehover':{"locatorvalue": "#top","by": "By.LINK_TEXT", "value": '#top'}} 34 | 35 | text_boxes_all = {"search_text_box": {"locatorvalue": 'search-courses', 36 | "by": "By.ID", "value": "selenium" + Keys.ENTER}, 37 | "mobile_no_text_box": {"locatorvalue": 'ch_signup_phone', 38 | "by": "By.ID", "value": "9502668772" + Keys.ENTER}, 39 | "pwd_text_box": {"locatorvalue": 'ch_signup_password', 40 | "by": "By.ID", "value": "9502668772" + Keys.ENTER}} 41 | 42 | 43 | 44 | class TestClass(BrowserActions): 45 | """Test Class Container for test cases.""" 46 | 47 | # def setUp(self): 48 | # # Added the below line for travis to enable headless mode test 49 | # self.driver = self.driver(chrome_options=chrome_options) 50 | # # pass 51 | # 52 | # def tearDown(self): 53 | # self.driver.quit() 54 | 55 | 56 | def test_BrowserActions1(self): 57 | 58 | """Sample case for navigating to python site and search selenium.""" 59 | self.open(PageObjects.base_url) 60 | self.page_readiness_wait() 61 | self.set_window_size(1200, 800) 62 | self.maximize() 63 | self.scroll_to_element( 64 | PageObjects.buttons_all.get("mousehover")) 65 | self.hover_on_element( 66 | PageObjects.buttons_all.get("mousehover")) 67 | self.wait_for_element( 68 | PageObjects.links_all.get("top_mousehover")) 69 | self.hover_on_click( 70 | PageObjects.links_all.get("top_mousehover")) 71 | self.wait_for_element( 72 | PageObjects.RadioButtons_all.get("benzButton")) 73 | self.title = self.get_title() 74 | self.drivername = self.get_driver_name() 75 | self.domainurl = self.get_domain_url() 76 | assert_true(str(self.drivername) == 77 | 'chrome', 'driver name is not matched') 78 | assert_true(str(self.domainurl) == 79 | 'https://learn.letskodeit.com', 'domain url is not matched') 80 | assert_true(str(self.title) == "Practice | Let's Kode It", 81 | "Title is not matched with expected") 82 | self.pagesource = self.get_page_source() 83 | assert_true( 84 | self.pagesource.startswith("' object 71 | :rtype: requests.Response 72 | """ 73 | try: 74 | if self._validate_kwargs(**kwargs) and kwargs['url']: 75 | return requests.get(**kwargs) 76 | except InvalidURL: 77 | logger.warn("The URL provided is invalid, please recheck") 78 | 79 | def _post_method(self, **kwargs): 80 | """Send a POST request. 81 | 82 | :param url: URL for the new :class:'Request' object. 83 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like 84 | object to send in the body of the :class:'Request'. 85 | :param json: (optional) json data 86 | to send in the body of the :class:'Request'. 87 | :param **kwargs: Optional arguments that ''request'' takes. 88 | :return: :class:'Response ' object 89 | :rtype: requests.Response 90 | """ 91 | try: 92 | if self._validate_kwargs(**kwargs): 93 | if kwargs['url']: 94 | if (('json' in kwargs and kwargs['json']) or 95 | ('data' in kwargs and kwargs['data'])): 96 | return requests.post(**kwargs) 97 | except InvalidURL: 98 | logger.warn("The URL provided is invalid, please recheck") 99 | 100 | def _put_method(self, **kwargs): 101 | """Send a PUT request. 102 | 103 | :param url: URL for the new :class:'Request' object. 104 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like 105 | object to send in the body of the :class:'Request'. 106 | :param json: (optional) json data 107 | to send in the body of the :class:'Request'. 108 | :param **kwargs: Optional arguments that ''request'' takes. 109 | :return: :class:'Response ' object 110 | :rtype: requests.Response 111 | """ 112 | try: 113 | if kwargs['url']: 114 | if (('json' in kwargs and kwargs['json']) or 115 | ('data' in kwargs and kwargs['data'])): 116 | return requests.put(**kwargs) 117 | except InvalidURL: 118 | logger.warn("The URL provided is invalid, please recheck") 119 | 120 | def _patch_method(self, **kwargs): 121 | """Send a PATCH request. 122 | 123 | :param url: URL for the new :class:'Request' object. 124 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like 125 | object to send in the body of the :class:'Request'. 126 | :param json: (optional) json data 127 | to send in the body of the :class:'Request'. 128 | :param **kwargs: Optional arguments that ''request'' takes. 129 | :return: :class:'Response ' object 130 | :rtype: requests.Response 131 | """ 132 | try: 133 | if kwargs['url']: 134 | if (('json' in kwargs and kwargs['json']) or 135 | ('data' in kwargs and kwargs['data'])): 136 | return requests.patch(**kwargs) 137 | except InvalidURL: 138 | logger.warn("The URL provided is invalid, please recheck") 139 | 140 | def _delete_method(self, **kwargs): 141 | """Send a DELETE request. 142 | 143 | :param url: URL for the new :class:'Request' object. 144 | :param **kwargs: Optional arguments that ''request'' takes. 145 | :return: :class:'Response ' object 146 | :rtype: requests.Response 147 | """ 148 | try: 149 | if self._validate_kwargs(**kwargs) and kwargs['url']: 150 | return requests.delete(**kwargs) 151 | except InvalidURL: 152 | logger.warn("The URL provided is invalid,please recheck") 153 | 154 | def _validate_kwargs(self, **kwargs): 155 | """ 156 | Verify key presence in input kwargs and return the key value. 157 | 158 | standard keywords are defined as per, 159 | http://docs.python-requests.org/en/master/api/. 160 | :parm method: method for the new Request object. 161 | :parm url: URL for the new Request object. 162 | :parm params: (optional) Dictionary, list of tuples or bytes 163 | to send in the body of the Request. 164 | :parm data: (optional) Dictionary, list of tuples, bytes, 165 | or file-like object to send in the body of the Request. 166 | :parm json: (optional) A JSON serializable Python object 167 | to send in the body of the Request. 168 | :parm headers: (optional) Dictionary of HTTP Headers 169 | to send with the Request. 170 | :parm cookies: (optional) Dict or CookieJar object 171 | to send with the Request. 172 | :parm files: (optional) Dictionary of 'name': file-like-objects 173 | (or {'name': file-tuple}) for multipart encoding upload. 174 | file-tuple can be a 2-tuple ('filename', fileobj), 175 | 3-tuple ('filename', fileobj, 'content_type') or 176 | a 4-tuple ('filename', fileobj, 'content_type', custom_headers), 177 | where 'content-type' is a string defining the content type of 178 | the given file and custom_headers a dict-like object containing 179 | additional headers to add for the file. 180 | :parm auth: (optional) Auth tuple 181 | to enable Basic/Digest/Custom HTTP Auth. 182 | :parm timeout (float or tuple): (optional) How many seconds 183 | to wait for the server to send data before giving up, as a float, 184 | or a (connect timeout, read timeout) tuple. 185 | :parm allow_redirects (bool): (optional) Boolean. 186 | Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. 187 | Defaults to True. 188 | :parm proxies: (optional) Dictionary mapping protocol 189 | to the URL of the proxy. 190 | :parm verify: (optional) Either a boolean, 191 | in which case it controls whether we verify the servers 192 | TLS certificate, or a string, in which case it must be a path 193 | to a CA bundle to use. Defaults to True. 194 | :parm stream: (optional) if False, the response content will be 195 | immediately downloaded. 196 | :parm cert: (optional) if String, path to ssl client cert file (.pem). 197 | If Tuple, (cert, key) pair. 198 | """ 199 | stand_kw = ["method", "url", "params", "data", "json", 200 | "headers", "cookies", "files", "auth", "timeout", 201 | "allow_redirects", "proxies", "verify", "stream", "cert"] 202 | try: 203 | non_stand_kw = [ 204 | key for key in kwargs.keys() if key not in stand_kw] 205 | if not len(non_stand_kw): 206 | return True 207 | else: 208 | raise KeyError("%s keywords are invalid" % non_stand_kw) 209 | except KeyError as e: 210 | logger.warning(e) 211 | 212 | def assert_in_resp(self, resp, member, container): 213 | """Check whether response data member contain input member. 214 | 215 | :parm resp: response to validate. 216 | :parm member: value to check in response. 217 | :parm container: response key path in dot format 218 | which should starts with 'resp.'. example: resp.data.0.name 219 | """ 220 | actual_val = self._get_val_from_resp_by_path(resp, container) 221 | return self.assertIn(member, actual_val) 222 | 223 | def assert_not_in_resp(self, resp, member, container): 224 | """Check whether response data member doesn't contain input member. 225 | 226 | :parm resp: response to validate. 227 | :parm member: value to check in response. 228 | :parm container: response key path in dot format 229 | which should starts with 'resp.'. example: resp.data.0.name 230 | """ 231 | actual_val = self._get_val_from_resp_by_path(resp, container) 232 | return self.assertNotIn(member, actual_val) 233 | 234 | def assert_equal_resp(self, resp, member, container): 235 | """Check whether response data member is same as input member. 236 | 237 | :parm resp: response to validate. 238 | :parm member: value to check in response. 239 | :parm container: response key path in dot format 240 | which should starts with 'resp.'. example: resp.data.0.name 241 | """ 242 | actual_val = self._get_val_from_resp_by_path(resp, container) 243 | return self.assertEqual(member, actual_val) 244 | 245 | def assert_not_equal_resp(self, resp, member, container): 246 | """Check whether response data member is not same as input member. 247 | 248 | :parm resp: response to validate. 249 | :parm member: value to check in response. 250 | :parm container: response key path in dot format 251 | which should starts with 'resp.'. example: resp.data.0.name 252 | """ 253 | actual_val = self._get_val_from_resp_by_path(resp, container) 254 | return self.assertNotEqual(member, actual_val) 255 | 256 | def _get_val_from_resp_by_path(self, resp, path): 257 | """Get value from response by dot format key path of response . 258 | 259 | :parm resp: response 260 | :parm path: key path in dot format which should starts with 'resp.'. 261 | example: resp.data.0.name 262 | """ 263 | val = '' 264 | items = path.split('.') 265 | for index in range(len(items)): 266 | if index == 0: 267 | val = items[index] 268 | else: 269 | try: 270 | val += '[%s]' % int(items[index]) 271 | except ValueError: 272 | val += "['%s']" % str(items[index]) 273 | return eval(val) 274 | -------------------------------------------------------------------------------- /prodigyqa/browseractions.py: -------------------------------------------------------------------------------- 1 | """UI utility functions of all selenium self.driver based actions.""" 2 | from loguru import logger 3 | 4 | import os 5 | 6 | import platform 7 | 8 | import unittest 9 | 10 | from datetime import datetime 11 | 12 | from time import sleep 13 | 14 | from selenium import webdriver 15 | 16 | from selenium.common import exceptions as selenium_exceptions 17 | 18 | from selenium.webdriver.common.action_chains import ActionChains 19 | 20 | from selenium.webdriver.common.by import By 21 | 22 | from selenium.webdriver.support import expected_conditions as ec 23 | 24 | from selenium.webdriver.support.select import Select 25 | 26 | from selenium.webdriver.remote.webelement import WebElement 27 | 28 | from selenium.webdriver.support.ui import WebDriverWait as Wait 29 | 30 | if platform.system() == 'Darwin': 31 | from PIL import ImageGrab 32 | 33 | WAIT_SLEEP_TIME = 0.1 # Seconds 34 | 35 | TIME_OUT = 10 # Seconds 36 | 37 | chrome_options = webdriver.ChromeOptions() 38 | chrome_options.add_argument('--headless') 39 | chrome_options.add_argument('--no-sandbox') 40 | 41 | 42 | class BrowserActions(unittest.TestCase): 43 | """PageActions Class is the gateway for using Framework. 44 | 45 | It inherits Python's unittest.TestCase class, and runs with Pytest. 46 | """ 47 | 48 | def __init__(self, *args, **kwargs): 49 | """Init Method for webdriver declarations.""" 50 | super(BrowserActions, self).__init__(*args, **kwargs) 51 | self.by_value = None 52 | if platform.system() == 'Linux': 53 | self.driver = webdriver.Chrome(chrome_options=chrome_options) 54 | else: 55 | self.driver = webdriver.Chrome() 56 | 57 | def __del__(self): 58 | """Destructor method to kill the driver instance. 59 | This helps to kill the driver instance at the end of the execution. 60 | """ 61 | self.driver.quit() 62 | 63 | def page_readiness_wait(self): 64 | """Web Page Expected to be in ready state.""" 65 | start = datetime.now() 66 | while (datetime.now() - start).total_seconds() < TIME_OUT: 67 | pagestate = self.__execute_script('''return document.readyState''') 68 | pagestate = pagestate.lower() 69 | if pagestate == 'complete': 70 | current_state = "Current page is in expected state {}" 71 | logger.info(current_state.format(pagestate)) 72 | break 73 | sleep(0.2) 74 | loop_time_now = (datetime.now() - start).seconds 75 | if loop_time_now > TIME_OUT and pagestate != 'complete': 76 | raise AssertionError( 77 | "Opened browser is in state of %s" % pagestate) 78 | 79 | def locator_check(self, locator_dict): 80 | """Local Method to classify locator type. 81 | 82 | :type locator_dict: dict 83 | """ 84 | text_retrived = locator_dict['by'].upper() 85 | if 'ID' in text_retrived: 86 | by = By.ID 87 | if 'CLASS_NAME' in text_retrived: 88 | by = By.CLASS_NAME 89 | if 'CSS_SELECTOR' in text_retrived: 90 | by = By.CSS_SELECTOR 91 | if 'NAME' in text_retrived: 92 | by = By.NAME 93 | if 'LINK_TEXT' in text_retrived: 94 | by = By.LINK_TEXT 95 | if 'PARTIAL_LINK_TEXT' in text_retrived: 96 | by = By.PARTIAL_LINK_TEXT 97 | if 'XPATH' in text_retrived: 98 | by = By.XPATH 99 | if 'TAG_NAME' in text_retrived: 100 | by = By.TAG_NAME 101 | self.by_value = by 102 | 103 | def open(self, url): 104 | """Open the passed 'url'.""" 105 | if url is not None: 106 | try: 107 | self.driver.get(url) 108 | logger.info("Browser opened with url '{0}'".format(url)) 109 | except Exception: 110 | logger.info("Browser with session id %s failed" 111 | " to navigate to url '%s'." % ( 112 | self.driver.session_id, url)) 113 | raise AssertionError( 114 | 'Opened browser with session id {}'.format( 115 | self.driver.session_id)) 116 | else: 117 | raise AssertionError("Invalid/ URL cannot be null") 118 | 119 | def reload_page(self): 120 | """Method to refresh the page by selenium or java script.""" 121 | try: 122 | self.driver.refresh() 123 | except BaseException: 124 | check_point1 = self.__execute_script( 125 | '''return performance.navigation.type''') 126 | self.__execute_script('''document.location.reload()''') 127 | check_point2 = self.__execute_script( 128 | '''return performance.navigation.type''') 129 | if check_point1 == 0 and check_point2 == 1: 130 | logger.info("Page Refresh Complete") 131 | else: 132 | logger.error("Page Refresh Error") 133 | 134 | def get_page_source(self): 135 | """Return the entire HTML source of the current page or frame.""" 136 | self.page_readiness_wait() 137 | return self.driver.page_source 138 | 139 | def get_title(self): 140 | """Return the title of current page.""" 141 | self.page_readiness_wait() 142 | try: 143 | return self.driver.title 144 | except BaseException: 145 | return self.__execute_script("return document.title") 146 | 147 | def get_location(self): 148 | """Return the current browser URL using Selenium/Java Script.""" 149 | self.page_readiness_wait() 150 | try: 151 | url = self.driver.current_url 152 | except BaseException: 153 | url = self.__execute_script("return window.location['href']") 154 | finally: 155 | return url if 'http' in url else None 156 | 157 | def get_attribute(self, locator=None, element=None, 158 | attribute_name=None, type='locator'): 159 | """Fetch attribute from locator/element/parent. 160 | 161 | element with child locator. 162 | :param locator: dictionary of identifier type 163 | and value ({'by':'id', 'value':'start-of-content.'}). 164 | :param attribute_name: attribute name to get it's vale 165 | :param element: it is a webelement 166 | :param type : value can only be 'locator' or 'element' or 'mixed' 167 | :type locator: dict 168 | :type type: str 169 | """ 170 | valid_arguments_of_type = ['locator', 'element', 'mixed'] 171 | type = type.lower() 172 | if type not in valid_arguments_of_type: 173 | raise AssertionError("Invalid Type Specified") 174 | if locator is None and element is None and attribute_name is None: 175 | raise AssertionError("Invalid Specification/condition") 176 | if type == 'locator': 177 | if locator is not None and attribute_name is not None: 178 | self.locator_check(locator) 179 | self.page_readiness_wait() 180 | if attribute_name is not None and isinstance(locator, dict): 181 | return self.driver.find_element( 182 | self.by_value, 183 | value=locator['locatorvalue']).get_attribute( 184 | attribute_name) 185 | else: 186 | raise AssertionError( 187 | "Invalid locator or Attribute is'{}'".format( 188 | attribute_name)) 189 | 190 | if type == 'element': 191 | if element is not None and attribute_name is not None: 192 | self.page_readiness_wait() 193 | return element.get_attribute(attribute_name) 194 | else: 195 | raise AssertionError( 196 | "Invalid Element/Attribute passed:'{}'".format( 197 | attribute_name)) 198 | if type == 'mixed': 199 | if element is not None: 200 | if locator is not None and attribute_name is not None: 201 | self.locator_check(locator) 202 | self.page_readiness_wait() 203 | if isinstance(locator, dict): 204 | return element.find_element( 205 | self.by_value, 206 | value=locator['locatorvalue']).get_attribute( 207 | attribute_name) 208 | else: 209 | raise AssertionError( 210 | "Invalid locator/element/attribute'{}'".format( 211 | attribute_name)) 212 | 213 | def click(self, locator, index=None): 214 | """Click an element. 215 | 216 | :param locator: dictionary of identifier type 217 | and value ({'by':'id', 'value':'start-of-content.'}). 218 | :param index: Defaults None, number/position of element 219 | """ 220 | self.page_readiness_wait() 221 | if isinstance(locator, dict): 222 | self.locator_check(locator) 223 | if index is not None: 224 | web_elts = self.find_elements(locator) 225 | if index < len(web_elts): 226 | web_elts[index].click() 227 | else: 228 | raise AssertionError( 229 | "Index is greater than no. of elements present") 230 | else: 231 | self.__find_element(locator).click() 232 | elif isinstance(locator, WebElement): 233 | locator.click() 234 | else: 235 | raise AssertionError( 236 | "Dictionary/Weblement are valid Locator types.") 237 | 238 | def javascript_click(self, locator, index=None): 239 | """Javascript Click on provided element. 240 | 241 | :param locator: dictionary of identifier type 242 | and value ({'by':'id', 'value':'start-of-content.'}) 243 | or a Webelement. 244 | :param index: Number/position of element present 245 | """ 246 | self.page_readiness_wait() 247 | if isinstance(locator, dict): 248 | self.locator_check(locator) 249 | if index is not None: 250 | web_elts = self.find_elements(locator) 251 | if index < len(web_elts): 252 | self.driver.execute_script( 253 | "arguments[0].click();", web_elts[index]) 254 | else: 255 | raise AssertionError( 256 | "Index is greater than the number of elements") 257 | else: 258 | self.driver.execute_script( 259 | "arguments[0].click();", self.driver.find_element( 260 | self.by_value, value=locator['locatorvalue'])) 261 | elif isinstance(locator, WebElement): 262 | self.__execute_script("arguments[0].click();", locator) 263 | else: 264 | raise AssertionError( 265 | "Locator type should be either dictionary or Weblement.") 266 | 267 | def is_element_displayed(self, locator: dict): 268 | """ 269 | Check whether an element is diplayed. 270 | 271 | :param locator: dictionary of identifier type 272 | and value ({'by':'id', 'value':'start-of-content.'}). 273 | :type locator: dict 274 | """ 275 | self.locator_check(locator) 276 | self.page_readiness_wait() 277 | if isinstance(locator, dict): 278 | return self.driver.find_element( 279 | self.by_value, 280 | value=locator['locatorvalue']).is_displayed() 281 | else: 282 | raise AssertionError("Locator type should be dictionary.") 283 | 284 | def is_element_enabled(self, locator: dict): 285 | """ 286 | Check whether an element is enabled. 287 | 288 | :param locator: dictionary of identifier type 289 | and value ({'by':'id', 'value':'start-of-content.'}). 290 | :type locator: dict 291 | """ 292 | self.locator_check(locator) 293 | self.page_readiness_wait() 294 | if isinstance(locator, dict): 295 | return self.__find_element(locator).is_enabled() 296 | else: 297 | raise AssertionError("Locator type should be dictionary.") 298 | 299 | def is_element_selected(self, locator: dict): 300 | """ 301 | Check whether an element is selecte. 302 | 303 | :param locator: dictionary of identifier type 304 | and value ({'by':'id', 'value':'start-of-content.'}). 305 | :type locator: dict 306 | """ 307 | self.locator_check(locator) 308 | self.page_readiness_wait() 309 | if isinstance(locator, dict): 310 | return self.__find_element(locator).is_selected() 311 | else: 312 | raise AssertionError("Locator type should be dictionary.") 313 | 314 | def send_keys(self, locator: dict, value=None): 315 | """Send text but does not clear the existing text. 316 | 317 | :param locator: dictionary of identifier type 318 | and value ({'by':'id', 'value':'start-of-content.'}). 319 | :type locator: dict 320 | """ 321 | self.page_readiness_wait() 322 | if isinstance(locator, dict): 323 | self.locator_check(locator) 324 | 325 | self.__find_element(locator).send_keys( 326 | locator['value'] if value is None else value) 327 | else: 328 | raise AssertionError("Locator type should be dictionary.") 329 | 330 | def get_text(self, locator, index=None): 331 | """Get text from provided Locator. 332 | 333 | :param locator: dictionary of identifier type 334 | and value ({'by':'id', 'value':'start-of-content.'}). 335 | """ 336 | self.page_readiness_wait() 337 | if isinstance(locator, dict): 338 | self.locator_check(locator) 339 | if index is not None: 340 | web_elts = self.find_elements(locator) 341 | if index < len(web_elts): 342 | return web_elts[index].text 343 | else: 344 | raise AssertionError( 345 | "Index is greater than the number of elements") 346 | else: 347 | return self.__find_element(locator).text 348 | 349 | elif isinstance(locator, WebElement): 350 | return locator.text 351 | else: 352 | raise AssertionError( 353 | "Locator type should be either dictionary or Weblement.") 354 | 355 | def go_back(self): 356 | """Simulate back button on browser using selenium or js.""" 357 | try: 358 | self.driver.back() 359 | except BaseException: 360 | self.__execute_script("window.history.go(-1)") 361 | 362 | def go_forward(self): 363 | """Simulate forward button on browser using selenium or js.""" 364 | try: 365 | self.driver.forward() 366 | except BaseException: 367 | self.__execute_script("window.history.go(+1)") 368 | 369 | def set_window_size(self, width, height): 370 | """Set width and height of the current window. (window.resizeTo). 371 | 372 | :param width: the width in pixels to set the window to 373 | :param height: the height in pixels to set the window to 374 | :Usage: 375 | driver.set_window_size(800,600). 376 | """ 377 | width_value_check = isinstance(width, (int, float)) 378 | height_value_check = isinstance(height, (int, float)) 379 | if width_value_check and height_value_check: 380 | self.driver.set_window_size(width, height) 381 | else: 382 | AssertionError("Window size Invalid") 383 | 384 | def maximize(self): 385 | """Maximize the current window.""" 386 | # https://bugs.chromium.org/p/chromedriver/issues/detail?id=985 387 | # reoccurs again and again 388 | if platform.system() == 'Darwin': 389 | img = ImageGrab.grab() 390 | screen_size = img.size 391 | self.set_window_size(screen_size[0], screen_size[1]) 392 | else: 393 | self.driver.maximize_window() 394 | 395 | def get_driver_name(self): 396 | """Return the name of webdriver instance.""" 397 | return self.driver.name 398 | 399 | def get_domain_url(self): 400 | """Method to extract domain url from webdriver itself.""" 401 | url = self.driver.current_url 402 | return url.split('//')[0] + '//' + url.split('/')[2] 403 | 404 | def clear_text(self, locator: dict): 405 | """Clear the text if it's a text entry element. 406 | 407 | :param locator: dictionary of identifier type 408 | and value ({'by':'id', 'value':'start-of-content.'}). 409 | :type locator: dict 410 | """ 411 | self.locator_check(locator) 412 | self.page_readiness_wait() 413 | if isinstance(locator, dict): 414 | return self.__find_element(locator).clear() 415 | else: 416 | raise AssertionError("Locator type should be dictionary") 417 | 418 | def capture_screenshot(self, filepath): 419 | """Save screenshot to the directory(existing or new one). 420 | 421 | :param filepath: file name with directory path(C:/images/image.png). 422 | """ 423 | self.page_readiness_wait() 424 | 425 | if not self.driver.service.process: 426 | logger.info('Cannot capture ScreenShot' 427 | ' because no browser is open.') 428 | return 429 | path = filepath.replace('/', os.sep) 430 | 431 | if not os.path.exists(path.split(os.sep)[0]): 432 | os.makedirs(path.split(os.sep)[0]) 433 | elif os.path.exists(path): 434 | os.remove(path) 435 | if not self.driver.get_screenshot_as_file(path): 436 | raise RuntimeError("Failed to save screenshot '{}'.".format(path)) 437 | return path 438 | 439 | def switch_to_active_element(self): 440 | """Return the element with focus, or BODY if nothing has focus.""" 441 | self.page_readiness_wait() 442 | try: 443 | return self.driver.switch_to.active_element 444 | except BaseException: 445 | return self.__execute_script('''document.activeElement''') 446 | 447 | def switch_to_window(self, window): 448 | """Switch focus to the specified window using selenium/javascript. 449 | 450 | :param window: name of the window to switch 451 | """ 452 | try: 453 | self.driver.switch_to.window(window) 454 | except selenium_exceptions.NoSuchWindowException: 455 | AssertionError( 456 | "Targeted window {} to be switched doesn't exist".window) 457 | 458 | def switch_to_active_window(self): 459 | """Switch focus to Active window.""" 460 | try: 461 | handles = self.driver.window_handles 462 | size = len(handles) 463 | for x in range(size): 464 | if handles[x] != self.driver.current_window_handle: 465 | self.driver.switch_to.window(handles[x]) 466 | except selenium_exceptions.NoSuchWindowException: 467 | AssertionError( 468 | "Targeted window {} to be switched doesn't exist".window) 469 | 470 | def switch_to_frame(self, framename): 471 | """Switch focus to the specified frame. 472 | 473 | :param framename: name of the frame to switch. 474 | """ 475 | self.page_readiness_wait() 476 | try: 477 | self.driver.switch_to.frame(framename) 478 | except selenium_exceptions.NoSuchFrameException: 479 | AssertionError( 480 | "Targeted frame {} to be switched doesn't exist".framename) 481 | 482 | def switch_to_frame_by_index(self, index): 483 | """Switch focus to the specified frame . 484 | 485 | :param framename: index/frame number to switch. 486 | """ 487 | self.page_readiness_wait() 488 | try: 489 | self.driver.switch_to.frame(index) 490 | except selenium_exceptions.NoSuchFrameException: 491 | raise AssertionError( 492 | "Targeted frame {} doesn't exist at passed index".index) 493 | 494 | def switch_to_default_content(self): 495 | """Switch focus to the default frame.""" 496 | self.page_readiness_wait() 497 | try: 498 | self.driver.switch_to.default_content() 499 | except selenium_exceptions.InvalidSwitchToTargetException: 500 | AssertionError( 501 | "Frame or Window targeted to be switched doesn't exist") 502 | 503 | def switch_to_alert(self): 504 | """Switch focus to an alert on the page.""" 505 | try: 506 | return self.driver.switch_to.alert 507 | except selenium_exceptions.NoAlertPresentException: 508 | AssertionError("Alert targeted to be switched doesn't exist") 509 | 510 | def hover_on_element(self, locator: dict): 511 | """Hover on a particular element. 512 | 513 | :param locator: dictionary of identifier type 514 | and value ({'by':'id', 'value':'start-of-content.'}). 515 | :type locator: dict 516 | """ 517 | self.locator_check(locator) 518 | self.page_readiness_wait() 519 | if isinstance(locator, dict): 520 | try: 521 | ActionChains(self.driver).move_to_element( 522 | self.__find_element(locator)).perform() 523 | except selenium_exceptions.NoSuchElementException: 524 | AssertionError( 525 | "Element{} not found".format(locator['by']) + 526 | '=' + locator['locatorvalue']) 527 | else: 528 | raise AssertionError("Locator type should be dictionary") 529 | 530 | def hover_on_click(self, locator): 531 | """Hover & click a particular element. 532 | 533 | :param locator: dictionary of identifier type 534 | and value ({'by':'id', 'value':'start-of-content.'}). 535 | """ 536 | self.locator_check(locator) 537 | self.page_readiness_wait() 538 | try: 539 | self.hover_on_element(locator) 540 | self.click(locator) 541 | except selenium_exceptions.NoSuchElementException: 542 | AssertionError( 543 | "Element {} not found".format( 544 | locator['by']) + '=' + locator['locatorvalue']) 545 | 546 | def wait_for_element(self, locator) -> bool: 547 | """Wait for an element to exist in UI. 548 | 549 | :param locator: dictionary of identifier type 550 | and value ({'by':'id', 'value':'start-of-content.'}). 551 | :rtype: bool 552 | """ 553 | self.locator_check(locator) 554 | self.page_readiness_wait() 555 | try: 556 | if self.__find_element(locator): 557 | return True 558 | except selenium_exceptions.NoSuchElementException: 559 | AssertionError("Failed to wait for element {}".format( 560 | locator['by'] + '=' + locator['locatorvalue'])) 561 | 562 | def wait_and_accept_alert(self): 563 | """Wait and accept alert present on the page.""" 564 | try: 565 | Wait(self.driver, TIME_OUT).until(ec.alert_is_present()) 566 | self.driver.switch_to.alert.accept() 567 | logger.info("alert accepted") 568 | except selenium_exceptions.TimeoutException: 569 | logger.error( 570 | "Could Not Find Alert Within The Permissible Time Limit") 571 | 572 | def wait_and_reject_alert(self): 573 | """Wait for alert and rejects.""" 574 | try: 575 | Wait(self.driver, TIME_OUT).until(ec.alert_is_present()) 576 | self.driver.switch_to.alert.dismiss() 577 | logger.info("alert dismissed") 578 | except selenium_exceptions.TimeoutException: 579 | logger.error( 580 | "Could Not Find Alert Within The Permissible Time Limit") 581 | 582 | def select_option_by_index(self, locator: dict, index: int): 583 | """Select the option by index. 584 | 585 | :param locator: dictionary of identifier type 586 | and value ({'by':'id', 'value':'start-of-content.'}). 587 | :param index: integer value for index. 588 | :type locator: dict 589 | :type index: int 590 | """ 591 | self.by_value = locator['by'] 592 | if isinstance(locator, dict) and isinstance(index, int): 593 | self.locator_check(locator) 594 | try: 595 | Select(self.__find_element(locator)).select_by_index(index) 596 | except selenium_exceptions.NoSuchElementException: 597 | logger.error("Exception : Element '{}' Not Found".format( 598 | locator['by'] + '=' + locator['locatorvalue'])) 599 | else: 600 | AssertionError( 601 | "Invalid locator '{}' or index '{}'".format(locator, index)) 602 | 603 | def select_option_by_value(self, locator: dict, value: int): 604 | """Select the option by using value. 605 | 606 | :param locator: dictionary of identifier type 607 | and value ({'by':'id', 'value':'start-of-content.'}). 608 | :param value: string value to select option. 609 | :type locator: dict 610 | :type value: int 611 | """ 612 | self.page_readiness_wait() 613 | if isinstance(locator, dict) and isinstance(value, int): 614 | try: 615 | Select(self.__find_element(locator)).select_by_value(value) 616 | 617 | except selenium_exceptions.NoSuchElementException: 618 | logger.error("Exception : Element '{}' Not Found".format( 619 | locator['by'] + '=' + locator['locatorvalue'])) 620 | else: 621 | AssertionError( 622 | "Invalid locator '{}' or value '{}'".format(locator, value)) 623 | 624 | def select_option_by_text(self, locator: dict, text): 625 | """Select the value by using text. 626 | 627 | :param locator: dictionary of identifier type 628 | and value ({'by':'id', 'value':'start-of-content.'}). 629 | :param text: string value to select option. 630 | :type locator: dict 631 | """ 632 | self.page_readiness_wait() 633 | if isinstance(locator, dict): 634 | self.locator_check(locator) 635 | try: 636 | Select(self.__find_element(locator) 637 | ).select_by_visible_text(text) 638 | except selenium_exceptions.NoSuchElementException: 639 | logger.error("Exception : Element '{}' Not Found".format( 640 | locator['by'] + '=' + locator['locatorvalue'])) 641 | else: 642 | AssertionError("Invalid locator type") 643 | 644 | def scroll_to_footer(self): 645 | """Scroll till end of the page.""" 646 | self.page_readiness_wait() 647 | try: 648 | self.__execute_script( 649 | "window.scrollTo(0,document.body.scrollHeight)") 650 | except selenium_exceptions.JavascriptException: 651 | logger.error('Exception : Not Able to Scroll To Footer') 652 | 653 | def find_elements(self, locator: dict): 654 | """Return elements matched with locator. 655 | 656 | :param locator: dictionary of identifier type 657 | and value ({'by':'id', 'value':'start-of-content.'}). 658 | :type locator: dict 659 | """ 660 | self.locator_check(locator) 661 | self.page_readiness_wait() 662 | if isinstance(locator, dict): 663 | return self.driver.find_elements( 664 | self.by_value, 665 | value=locator['locatorvalue']) 666 | else: 667 | AssertionError("Invalid locator type") 668 | 669 | def scroll_to_element(self, locator: dict): 670 | """Scroll to a particular element on the page. 671 | 672 | :param locator: dictionary of identifier type 673 | and value ({'by':'id', 'value':'start-of-content.'}). 674 | :type locator: dict 675 | """ 676 | self.page_readiness_wait() 677 | if isinstance(locator, dict): 678 | try: 679 | self.locator_check(locator) 680 | element = self.__find_element(locator) 681 | actions = ActionChains(self.driver) 682 | actions.move_to_element(element).perform() 683 | except selenium_exceptions.NoSuchElementException: 684 | logger.error('Exception : Not Able To Scroll to Element') 685 | except BaseException: 686 | self.__execute_script( 687 | "arguments[0].scrollIntoView(true)", 688 | self.__find_element(locator)) 689 | else: 690 | AssertionError("Invalid locator type") 691 | 692 | def __find_element(self, locator: dict): 693 | """Private method simplified finding element. 694 | 695 | :type locator: dict 696 | """ 697 | if isinstance(locator, dict): 698 | self.locator_check(locator) 699 | return self.driver.find_element( 700 | self.by_value, 701 | value=locator['locatorvalue']) 702 | 703 | def __execute_script(self, script, web_elm=None): 704 | """ 705 | Private method to Exeucte the passed script. 706 | 707 | :param script_to_execute: must contain the valid JS to be executed. 708 | 709 | """ 710 | if web_elm is None: 711 | return self.driver.execute_script(script) 712 | if isinstance(web_elm, WebElement): 713 | return self.driver.execute_script(script, web_elm) 714 | if isinstance(web_elm, dict): 715 | return self.driver.execute_script( 716 | script, 717 | self.__find_element(web_elm)) 718 | -------------------------------------------------------------------------------- /prodigyqa/comparison.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Comparison Module for Images, Files like CSV, Excel, PDF etc.""" 3 | import unittest 4 | import cv2 5 | from skimage.measure import compare_ssim as ssim 6 | from loguru import logger 7 | import pandas as pd 8 | from jsondiff import diff 9 | import os 10 | 11 | 12 | class Compare(unittest.TestCase): 13 | """File Comparison module which includes image, csv and workbook.""" 14 | 15 | def __init__(self, *args, **kwargs): 16 | """Variable Stack Declaration.""" 17 | super(Compare, self).__init__(*args, **kwargs) 18 | self.source = None 19 | self.target = None 20 | self.source_extn = None 21 | self.target_extn = None 22 | self.source_name = None 23 | self.target_name = None 24 | self.image_extn = ('jpg', 'jpeg', "png") 25 | self.excel_extn = ('xls', 'xlsx') 26 | self.file_extn = ('xls', 'xlsx', 'csv', 'tsv', 'hdf', 'html') 27 | 28 | def compare_images(self, source, target): 29 | """Compare images and returns structural similarity over the image. 30 | 31 | Measure of SSIM is returned between 0-1.0 where 1.0 is 32 | the most identical 33 | and 0 being completely different 34 | :param source: source image path. 35 | :param target: target image path. 36 | :return: SSIM difference of images which ranges between 0 and 1 37 | :rtype: float 38 | """ 39 | self.source = cv2.imread(source) 40 | self.target = cv2.imread(target) 41 | self.source_extn = source.split(".")[1] 42 | self.target_extn = target.split(".")[1] 43 | if self.source_extn and self.target_extn not in self.image_extn: 44 | logger.error("Invalid image extension") 45 | self.target = cv2.resize( 46 | self.target, (int(self.source.shape[1]), 47 | int(self.source.shape[0]))) 48 | return ssim(self.source, self.target, multichannel=True) 49 | 50 | def compare_json(self, source, target): 51 | """Compare json files. 52 | 53 | :param source: source json. 54 | :param target: target json. 55 | :return: difference of target compared to source 56 | :rtype: Dictionary 57 | """ 58 | return diff(source, target) 59 | 60 | def compare_files(self, source, target): 61 | """Compare two files and return difference(if any). 62 | 63 | File Types Supported: xls or xlsx or html or hdf or csv or tsv 64 | :param source: Source file Path. 65 | :param target: Target file path. 66 | :return: file difference 67 | :rtype: data frame of file difference. 68 | """ 69 | self.source = source 70 | self.target = target 71 | self.source_extn = source.split('.')[1] 72 | self.target_extn = target.split('.')[1] 73 | self.source_name = source.split('.')[0] 74 | self.target_name = target.split('.')[0] 75 | if self.source_extn and self.target_extn in self.file_extn: 76 | if self.source_extn and self.target_extn in self.excel_extn: 77 | return self.__compare_workbooks() 78 | else: 79 | self.source_data = self.__load_into_dataframe(source) 80 | self.target_data = self.__load_into_dataframe(target) 81 | return self.__compare_non_workbook_files() 82 | else: 83 | logger.error('File Extension not supported') 84 | 85 | def __compare_workbooks(self): 86 | """Compare two xls or xlsx files and return difference and boolean. 87 | 88 | :return: True/False. 89 | :rtype: bool. 90 | """ 91 | source_df = pd.ExcelFile(self.source) 92 | target_df = pd.ExcelFile(self.target) 93 | 94 | if source_df.sheet_names == target_df.sheet_names: 95 | for sheet in source_df.sheet_names: 96 | self.__compare_sheets(sheet) 97 | 98 | def __compare_sheets(self, sheet, unique_col="account number"): 99 | """Compare sreadsheets and return difference to xl file. 100 | 101 | :param sheet: sheet name 102 | :param unique_col: column name 103 | :return: 104 | """ 105 | source_df = pd.read_excel(self.source, sheet).fillna('NA') 106 | target_df = pd.read_excel(self.target, sheet).fillna('NA') 107 | file_path = os.path.dirname(self.source) 108 | 109 | column_list = source_df.columns.tolist() 110 | 111 | source_df['version'] = "source" 112 | target_df['version'] = "target" 113 | 114 | source_df.sort_values(by=unique_col) 115 | source_df = source_df.reindex() 116 | target_df.sort_values(by=unique_col) 117 | target_df = target_df.reindex() 118 | 119 | # full_set = pd.concat([source_df, target_df], ignore_index=True) 120 | diff_panel = pd.concat([source_df, target_df], 121 | axis='columns', keys=['df1', 'df2'], 122 | join='outer', sort=False) 123 | diff_output = diff_panel.apply(self.__report_diff, axis=0) 124 | diff_output['has_change'] = diff_output.apply(self.__has_change) 125 | 126 | full_set = pd.concat([source_df, target_df], ignore_index=True) 127 | changes = full_set.drop_duplicates(subset=column_list, keep='last') 128 | dupe_records = changes.set_index(unique_col).index.unique() 129 | 130 | changes['duplicate'] = changes[unique_col].isin(dupe_records) 131 | removed_parts = changes[(~changes["duplicate"]) & ( 132 | changes["version"] == "source")] 133 | new_part_set = full_set.drop_duplicates( 134 | subset=column_list, keep='last') 135 | new_part_set['duplicate'] = new_part_set[unique_col].isin(dupe_records) 136 | added_parts = new_part_set[(~new_part_set["duplicate"]) & ( 137 | new_part_set["version"] == "target")] 138 | 139 | # Save the changes to excel but only include the columns we care about 140 | diff_file = file_path + "file_diff.xlsx" 141 | if os.path.exists(diff_file): 142 | os.remove(diff_file) 143 | writer = pd.ExcelWriter(file_path + "file_diff.xlsx") 144 | diff_output.to_excel(writer, "changed") 145 | removed_parts.to_excel( 146 | writer, "removed", index=False, columns=column_list) 147 | added_parts.to_excel(writer, "added", index=False, columns=column_list) 148 | writer.save() 149 | 150 | def __report_diff(self, x): 151 | """Report data difference. 152 | 153 | :param x: 154 | :return: difference 155 | """ 156 | return x[0] if x[0] == x[1] else '{} ---> {}'.format(*x) 157 | 158 | def __has_change(self, row): 159 | """Return Yes if cell has different data else No. 160 | 161 | :param row: row data 162 | :return: Yes/No 163 | """ 164 | if "--->" in row: 165 | return "Yes" 166 | else: 167 | return "No" 168 | 169 | def __compare_non_workbook_files(self): 170 | """Return xl of spreadsheet difference. 171 | 172 | :return: True/False. 173 | :rtype: bool. 174 | """ 175 | df = pd.concat([self.source_data, self.target_data]) 176 | df = df.reset_index(drop=True) 177 | df_groupby = df.groupby(list(df.columns)) 178 | index = [x[0] for x in df_groupby.groups.values() if len(x) == 1] 179 | diff = df.reindex(index) 180 | 181 | # Save the changes to excel 182 | file_path = os.path.dirname(self.source) 183 | diff_file = file_path + "file_diff.xlsx" 184 | if os.path.exists(diff_file): 185 | os.remove(diff_file) 186 | writer = pd.ExcelWriter(file_path + "file_diff.xlsx") 187 | diff.to_excel(writer, "difference") 188 | writer.save() 189 | 190 | def __load_into_dataframe(self, data): 191 | """Load hdf or csv or tsv file and return data. 192 | 193 | :param data: file. 194 | :return: file data. 195 | :rtype: data frame. 196 | """ 197 | if data.split(".")[1] == 'csv': 198 | return self.__read_csv(data) 199 | elif data.split(".")[1] == 'hdf': 200 | return self.__read_hdf(data) 201 | elif data.split(".")[1] == 'html': 202 | return self.__read_html(data) 203 | elif data.split(".")[1] == 'tsv': 204 | return self.__read_tsv(data) 205 | 206 | def __read_csv(self, data, sep=None): 207 | """Load csv file and return data. 208 | 209 | :param data: data file path 210 | :return: file data. 211 | :rtype: data frame. 212 | """ 213 | return pd.read_csv(data) 214 | 215 | def __read_hdf(self, data): 216 | """Load hdf file and return data. 217 | 218 | :param data: data file path 219 | :return: file data. 220 | :rtype: data frame. 221 | """ 222 | return pd.read_hdf(data) 223 | 224 | def __read_html(self, data): 225 | """Load html file and return data. 226 | 227 | :param data: data file path 228 | :return: file data. 229 | :rtype: data frame. 230 | """ 231 | return pd.read_html(data) 232 | 233 | def __read_tsv(self, data, sep='\t'): 234 | """Load tsv file and return data. 235 | 236 | :param data: data file path 237 | :return: file data. 238 | :rtype: data frame. 239 | """ 240 | return pd.read_csv(data, sep=sep) 241 | -------------------------------------------------------------------------------- /prodigyqa/setup_scrapper.tmpl: -------------------------------------------------------------------------------- 1 | import scrapy 2 | from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor 3 | from scrapy.contrib.spiders import CrawlSpider, Rule 4 | from scrapy.item import Item, Field 5 | import csv 6 | from urlparse import urlparse 7 | import scrapy 8 | 9 | class Spider(scrapy.Spider): 10 | name = "URLScraper" 11 | # This is a spider to crawl the URLs 12 | url = "{0}" 13 | start_urls = [] 14 | start_urls.append(url) 15 | allowed_domains = [] 16 | allowed_domains.append(urlparse(url).netloc) 17 | rules = (Rule(SgmlLinkExtractor(), callback='parse', follow=False),) 18 | list2 = [] 19 | list3=[] 20 | def parse(self, response): 21 | list1=[str(res.extract()) for res in response.xpath('//a/@href') 22 | if str(res.extract()).startswith('http:')] 23 | for url in response.xpath('//a/@href'): 24 | Spider.list2.append( 25 | str(url.extract())) 26 | yield url.extract() 27 | for URLtoHit in set(Spider.list2): 28 | next_link_to_hit=response.urljoin(URLtoHit) 29 | if(next_link_to_hit not in set(Spider.list3)): 30 | yield scrapy.Request(url=next_link_to_hit,callback=self.parse) 31 | Spider.list3.append(next_link_to_hit) 32 | with open("Result.csv", "wb") as f: 33 | writer = csv.writer(f) 34 | writer.writerows(set(Spider.list2)) 35 | outF = open("Result.txt", "w") 36 | for line in set(Spider.list2): 37 | # write line to output file 38 | outF.write(line) 39 | outF.write("\n") 40 | outF.close() 41 | -------------------------------------------------------------------------------- /prodigyqa/spellchecker.py: -------------------------------------------------------------------------------- 1 | """Spell Check Module.""" 2 | from prodigyqa import BrowserActions 3 | import re 4 | import nltk 5 | import aspell 6 | import string 7 | 8 | nltk.download('punkt') 9 | 10 | 11 | class SpellChecker(BrowserActions): 12 | """Spell Checker with Custom Libraries.""" 13 | 14 | def spell_check_on_page(self, url, words): 15 | """Spell checker. 16 | 17 | :param url: webpage url 18 | :param words: expected word list 19 | :type words: list 20 | :return: list of misspelled words 21 | TODO: Expand the parameter of words into txt/csv/custom string. 22 | """ 23 | self.open(url) 24 | cleanr = re.compile('<.*?>') 25 | page_content = re.sub(cleanr, '', self.get_page_source()) 26 | cleantext = [] 27 | speller_obj = aspell.Speller("lang", "en") 28 | if len(words): 29 | for word in words: 30 | speller_obj.addtoSession(word) 31 | 32 | invalidchars = set(string.punctuation.replace("_", "")) 33 | for word in nltk.word_tokenize(page_content): 34 | if any(invalidchar in word for invalidchar in invalidchars) or \ 35 | len(word) < 2: 36 | continue 37 | else: 38 | cleantext.append(word) 39 | 40 | misspelled = list(set([word.encode('ascii', 'ignore') 41 | for word in cleantext if not 42 | speller_obj.check( 43 | word) and re.match('^[a-zA-Z ]*$', word)])) 44 | return misspelled 45 | -------------------------------------------------------------------------------- /prodigyqa/spider.py: -------------------------------------------------------------------------------- 1 | """Module for all spider mechanisms to extract URL from given page.""" 2 | from prodigyqa import BrowserActions 3 | from bs4 import BeautifulSoup 4 | import pandas as pd 5 | import os 6 | try: 7 | from urlparse import urlparse 8 | except ImportError: 9 | from urllib.parse import urlparse 10 | 11 | 12 | class Webspider(BrowserActions): 13 | """Crawl a page and extract all urls recursively within same domain.""" 14 | 15 | def spider(self, parent_url, username, 16 | password, login_button, login=False): 17 | """Hold the Web Spider using selenium fo browser based login. 18 | 19 | :type login: bool 20 | :type username: dict 21 | :type password: dict 22 | :type login_button: dict 23 | """ 24 | self.url = parent_url 25 | self.url_list = list() 26 | self.crawled_urls = list() 27 | self.domain = urlparse(self.url).netloc 28 | self.url_list.append(self.url) 29 | 30 | self.open(self.url) 31 | if login: 32 | if isinstance(username, dict) and \ 33 | isinstance(password, dict) and \ 34 | isinstance(login_button, dict): 35 | self.send_keys(username) 36 | self.send_keys(password) 37 | self.click(login_button) 38 | 39 | # Initiate the crawling by passing the beginning url 40 | self.crawled_urls, self.url_list = self.__crawl_urls() 41 | 42 | # Load the matched url list to excel 43 | self.__load_to_excel() 44 | self.driver.quit() 45 | else: 46 | raise AssertionError("credentials are mandatory") 47 | else: 48 | tmp = open("..\\prodigyqa\\setup_scrapper.tmpl", "r") 49 | setup = open("..\\prodigyqa\\setup_scrapper.py", "w+") 50 | setup.write(tmp.read().format(self.url, self.domain)) 51 | setup.close() 52 | tmp.close() 53 | os.system('scrapy crawl URLScraper') 54 | 55 | def __crawl_urls(self): 56 | """Get a set of urls and crawl each url recursively.""" 57 | self.crawled_urls.append(self.url) 58 | 59 | html_source = self.get_page_source() 60 | html = html_source.encode("utf-8") 61 | soup = BeautifulSoup(html) 62 | urls = soup.findAll("a") 63 | 64 | # Even if the url is not part of the same domain, it is still collected 65 | # But those urls not in the same domain are not parsed 66 | for a in urls: 67 | if a.get("href") not in self.url_list: 68 | self.url_list.append(a.get("href")) 69 | 70 | # Recursively parse each url within same domain 71 | for page in set(self.url_list): 72 | if (urlparse(page).netloc == self.domain)\ 73 | and (page not in self.crawled_urls): 74 | self.__crawl_urls(self.url_list, 75 | self.crawled_urls, self.driver, page) 76 | else: 77 | return self.crawled_urls, self.url_list 78 | 79 | def __load_to_excel(self): 80 | """Load the list into excel file using pandas.""" 81 | df = pd.DataFrame(self.url_list) 82 | 83 | # So that the excel column starts from 1 84 | df.index += 1 85 | path = os.getcwd() 86 | xlw = pd.ExcelWriter(path + "\\crawler.xlsx") 87 | df.to_excel(xlw, sheet_name="URLs", 88 | index_label="S.NO", header=["URL"]) 89 | xlw.save() 90 | -------------------------------------------------------------------------------- /prodigyqa/utils.py: -------------------------------------------------------------------------------- 1 | """Useful Utilities which are used across modules will be put here.""" 2 | 3 | from PIL import Image 4 | 5 | import pytesseract 6 | 7 | 8 | class Utilities(object): 9 | """Utilities to contain few random methods which are easy and reusable.""" 10 | 11 | def captcha_to_text(self, image): 12 | """Method to return extracted text from passed image.""" 13 | return pytesseract.image_to_string(Image.open(image)) 14 | 15 | def is_string(self, item) -> bool: 16 | """Evaluate if an item sent is string. 17 | :rtype: bool 18 | """ 19 | if isinstance(item, str): 20 | return True 21 | else: 22 | return False 23 | 24 | def is_ok(self, item): 25 | """Evaluate if an item sent is OK.""" 26 | if self.is_string(item): 27 | return item.upper() not in ('FALSE', 'NO', '', 'NONE', '0', 'OFF') 28 | return bool(item) 29 | 30 | def is_not_ok(self, item): 31 | """Evaluate if an item sent is NOT OK.""" 32 | return not self.is_ok(item) 33 | 34 | def is_none(self, item): 35 | """Evaluate if an item sent is None.""" 36 | return item is None or self.is_string(item) and item.upper() == 'NONE' 37 | 38 | def are_equal(self, source, target, ignore_case=True): 39 | """Evaluate if an two strings shared are equal.""" 40 | if ignore_case: 41 | return source.lower() == target.lower() 42 | else: 43 | return source == target 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pip 2 | setuptools 3 | selenium 4 | nose 5 | jsondiff 6 | openpyxl 7 | pytesseract 8 | beautifulsoup4 9 | opencv-python 10 | flake8==3.7.9 11 | pycodestyle<2.6.0,>=2.5.0 12 | pandas==0.24.2 13 | urllib3==1.25.7 14 | requests>=2.21.0 15 | ipdb==0.12.2 16 | pytest>=4.0.2 17 | pytest-html<2.1.0 18 | xlrd>=0.9.0 19 | scrapy 20 | loguru 21 | -e . 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup package installs Prodigy QA Package dependencies and plugins.""" 2 | 3 | from setuptools import setup, find_packages # noqa 4 | from os import path 5 | 6 | 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | long_description = None 9 | try: 10 | with open(path.join(this_directory, 'README.md'), 'rb') as f: 11 | long_description = f.read().decode('utf-8') 12 | except IOError: 13 | long_description = 'Unified Automation Testing Framework' 14 | 15 | setup( 16 | name='prodigyqa', 17 | version='1.2.2', 18 | description='Test Automation Framework', 19 | long_description=long_description, 20 | long_description_content_type='text/markdown', 21 | url='https://pyqa.github.io/prodigyqa/', 22 | platforms=["Windows", "Linux", "Unix", "Mac OS-X"], 23 | author='Revant', 24 | author_email='revanth.mvs@hotmail.com', 25 | maintainer='Revant', 26 | classifiers=[ 27 | "Development Status :: 5 - Production/Stable", 28 | "Topic :: Internet", 29 | "Topic :: Scientific/Engineering", 30 | "Topic :: Software Development", 31 | "Topic :: Software Development :: Quality Assurance", 32 | "Topic :: Software Development :: Testing", 33 | "Topic :: Software Development :: Testing :: Acceptance", 34 | "Topic :: Utilities", 35 | "Operating System :: Microsoft :: Windows", 36 | "Operating System :: Unix", 37 | "Operating System :: MacOS", 38 | "Programming Language :: Python", 39 | "Programming Language :: Python :: 3.5", 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | ], 43 | install_requires=[ 44 | 'pip', 45 | 'setuptools', 46 | 'selenium', 47 | 'nose', 48 | 'jsondiff', 49 | 'openpyxl', 50 | 'pytesseract', 51 | 'beautifulsoup4', 52 | 'opencv-python', 53 | 'flake8==3.7.9', 54 | 'pycodestyle<2.6.0,>=2.5.0', 55 | 'pandas==0.24.2', 56 | 'urllib3==1.25.7', 57 | 'requests>=2.21.0', 58 | 'ipdb==0.12.2', 59 | 'pytest>=4.0.2', 60 | 'pytest-html<2.1.0', 61 | 'xlrd>=0.9.0', 62 | 'scrapy', 63 | 'nltk', 64 | 'loguru', 65 | ],) 66 | print("\n*** Prodigy QA Framework Installation Complete! ***\n") 67 | --------------------------------------------------------------------------------