├── .circleci └── config.yml ├── .github └── workflows │ └── stale.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Readme.md ├── __init__.py ├── api_auto_generator ├── Endpoint_Generator.md ├── endpoint_module_generator.py ├── endpoint_name_generator.py ├── openapi_spec_parser.py └── templates │ └── endpoint_template.jinja2 ├── bandit.yml ├── conf ├── __init__.py ├── api_example_conf.py ├── base_url_conf.py ├── browser_os_name_conf.py ├── cars_api_openapi_spec.json ├── clean_up_repo_conf.py ├── copy_framework_template_conf.py ├── env_remote_enc ├── example_form_conf.py ├── example_table_conf.py ├── gpt_summarization_prompt.py ├── locators_conf.py ├── mobile_bitcoin_conf.py ├── mobile_weather_shopper_conf.py ├── ports_conf.py ├── remote_url_conf.py ├── screenshot_conf.py ├── snapshot │ ├── snapshot_output_contact.json │ ├── snapshot_output_main.json │ └── snapshot_output_redirect.json ├── snapshot_dir_conf.py ├── successive_form_creation_conf.py ├── testrail_caseid_conf.py └── weather_shopper_mobile_conf.py ├── conftest.py ├── core_helpers ├── custom_pytest_plugins.py ├── drivers │ ├── __init__.py │ ├── driverfactory.py │ └── local_options.py ├── gif_maker.py ├── logging_objects.py ├── mobile_app_helper.py ├── prettytable_object.py ├── remote_objects.py ├── screenshot_objects.py ├── selenium_action_objects.py └── web_app_helper.py ├── endpoints ├── __init__.py ├── api_interface.py ├── api_player.py ├── base_api.py ├── cars_api_endpoints.py ├── registration_api_endpoints.py └── user_api_endpoints.py ├── entrypoint.sh ├── env_conf ├── env_remote ├── env_ssh_conf ├── integrations ├── __init__.py ├── cross_browsers │ ├── BrowserStack_Library.py │ ├── __init__.py │ ├── browserstack_runner.py │ ├── lambdatest_runner.py │ ├── remote_options.py │ └── saucelab_runner.py ├── reporting_channels │ ├── email_pytest_report.py │ ├── email_util.py │ ├── gmail │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── gmail.py │ │ ├── gmailtest.py │ │ ├── mailbox.py │ │ ├── message.py │ │ ├── utf.py │ │ └── utils.py │ └── post_test_reports_to_slack.py └── reporting_tools │ ├── Test_Rail.py │ ├── Tesults.py │ ├── setup_testrail.py │ └── testrail_client.py ├── page_objects ├── PageFactory.py ├── __init__.py ├── examples │ ├── __init__.py │ ├── bitcoin_mobile_app │ │ ├── __init__.py │ │ ├── bitcoin_main_page.py │ │ └── bitcoin_price_page.py │ ├── selenium_tutorial_webpage │ │ ├── __init__.py │ │ ├── contact_form_object.py │ │ ├── contact_page.py │ │ ├── footer_object.py │ │ ├── form_object.py │ │ ├── hamburger_menu_object.py │ │ ├── header_object.py │ │ ├── table_object.py │ │ ├── tutorial_main_page.py │ │ └── tutorial_redirect_page.py │ └── weather_shopper_mobile_app │ │ ├── cart_objects.py │ │ ├── homepage_objects.py │ │ ├── navigation_menu_objects.py │ │ ├── product_page_objects.py │ │ ├── weather_shopper_cart_page.py │ │ ├── weather_shopper_home_page.py │ │ ├── weather_shopper_payment_objects.py │ │ ├── weather_shopper_payment_page.py │ │ ├── weather_shopper_product_page.py │ │ └── webview_chrome.py ├── zero_mobile_page.py └── zero_page.py ├── pytest.ini ├── requirements.txt ├── tests ├── __init__.py ├── test_accessibility.py ├── test_api_async_example.py ├── test_api_endpoint_auto_generation.py ├── test_api_example.py ├── test_boilerplate.py ├── test_example_form.py ├── test_example_table.py ├── test_mobile_bitcoin_price.py ├── test_successive_form_creation.py ├── test_weather_shopper_app.py ├── test_weather_shopper_app_menu_options.py └── test_weather_shopper_payment_app.py ├── tox.ini └── utils ├── Base_Logging.py ├── Image_Compare.py ├── Wrapit.py ├── __init__.py ├── accessibility_util.py ├── axe.min.js ├── clean_up_repo.py ├── copy_framework_template.py ├── csv_compare.py ├── excel_compare.py ├── gpt_summary_generator.py ├── interactive_mode.py ├── qxf2_pom_essentials_dockerfiles ├── Readme.md ├── dockerfile ├── entrypoint.sh ├── sample_test_chrome.py └── sample_test_ff.py ├── results.py ├── snapshot_util.py ├── ssh_util.py ├── stop_test_exception_util.py └── xpath_util.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | 5 | toxify: 6 | 7 | docker: 8 | - image: divio/multi-python 9 | 10 | parallelism: 3 11 | 12 | steps: 13 | 14 | - checkout 15 | 16 | - run: pip install tox 17 | 18 | - run: git clone https://github.com/qxf2/bitcoin-info.git 19 | 20 | - run: openssl aes-256-cbc -d -md sha256 -in ./conf/env_remote_enc -out ./.env.remote -pass env:KEY 21 | 22 | - run: git clone https://github.com/qxf2/weather-shopper-app-apk.git 23 | 24 | - run: sudo apt-get update 25 | 26 | - run: 27 | name: Run different Tox environments on different Containers 28 | command: | 29 | if [ $CIRCLE_NODE_INDEX == "0" ] ; then tox -e py39 ; fi 30 | if [ $CIRCLE_NODE_INDEX == "1" ] ; then tox -e py310 ; fi 31 | if [ $CIRCLE_NODE_INDEX == "2" ] ; then tox -e py311 ; fi 32 | 33 | - store_artifacts: 34 | path: ./screenshots 35 | destination: screenshots-file 36 | 37 | - store_artifacts: 38 | path: ./log 39 | destination: logs-file 40 | 41 | - store_artifacts: 42 | path: ./tests/snapshots 43 | destination: snapshots-file 44 | 45 | 46 | workflows: 47 | version: 2 48 | 49 | myproj: 50 | jobs: 51 | - toxify 52 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | days-before-stale: '60' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Add files you want to ignore 2 | .DS_Store 3 | *.pyc 4 | *.lnk 5 | *.log 6 | logs/ 7 | screenshots/ 8 | log/ 9 | .cache/ 10 | .pytest_cache/ 11 | geckodriver.log 12 | debug.log 13 | __pycache__ 14 | venv* 15 | .env* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Contributing Guidelines 5 | -------- 6 | Your contributions are always welcome! There are a number of ways you can contribute. These guidelines instruct how to submit issues and contribute code or documentation to [Qxf2 Automation Framework](https://github.com/qxf2/qxf2-page-object-model). 7 | 8 | Reporting bugs 9 | -------- 10 | This section guides you through submitting a bug report for Qxf2. Before submitting a new issue, it is always a good idea to check if the same bug or enhancement is already reported. If it is, please add your comments to the existing issue instead of creating a new one. 11 | Bugs are tracked as [GitHub issues](https://github.com/qxf2/qxf2-page-object-model/issues). After you've determined which repository your bug is related to, create an issue on that repository and provide the following information: 12 | 13 | * __Use a clear and descriptive title__ for the issue to identify the problem 14 | * __Explain the steps to reproduce__ so that others can understand it and and preferably also reproduce it 15 | * __Provide snippets to demonstrate the steps__ which you might think is causing the bug 16 | * Expected result 17 | * Actual results 18 | * Environment details: Operating system and its version, packages installed 19 | 20 | 21 | Enhancement requests 22 | -------- 23 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/qxf2/qxf2-page-object-model/issues). Enhancements can be anything including completely new features and minor improvements to existing functionality. After you've determined which repository your enhancement suggestion is related to, create an issue on that repository and provide the following information: 24 | 25 | * __Use a clear and descriptive title__ for the issue to identify the suggestion. 26 | * __Provide a step-by-step description__ of the suggested enhancement in as many details as possible. 27 | * __Provide specific examples__ to demonstrate the steps. 28 | * __Describe the current behavior__ and __describe which behavior you expected to see instead__ and why. 29 | * __Include screenshots or animated GIF's__ which help you demonstrate the steps or point out the part of framework which the suggestion is related to. 30 | * __Explain how and why this enhancement__ would be useful 31 | * __Specify the name and version of the OS__ you're using. 32 | * __Specify the browser version__ you're using. 33 | * __Describe the new feature and use cases__ for it in as much detail as possible 34 | 35 | 36 | Code Contributions 37 | -------- 38 | 39 | This part of the document will guide you through the contribution process. If you have fixed a bug or implemented an enhancement, you can contribute your changes via GitHub's Pull requests. If this is your first time contributing to a project via GitHub follow below guidelines 40 | 41 | Here is the basic workflow: 42 | 43 | 1) Fork the Qxf2 repo. You can do this via the GitHub website. By doing this, you will have a local copy of the qxf2 repo under your Github account. 44 | 2) Clone the Qxf2 repo to your local machine 45 | 3) Create a feature branch and start hacking 46 | 4) Commit the changes on that branch 47 | 5) Push your change to your repo 48 | 6) Bug fixes and features should have tests. Before you submit your pull request make sure you pass all the tests. 49 | 7) Use the github UI to open a PR 50 | 8) When code review is complete, a committer will take your PR and merge it on our master branch. 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Pull ubuntu 22.04 base image 2 | FROM ubuntu:22.04 3 | LABEL maintainer="Qxf2 Services" 4 | 5 | ENV DISPLAY=:20 6 | 7 | # Essential tools and xvfb 8 | RUN apt-get update && apt-get install -y \ 9 | software-properties-common \ 10 | unzip \ 11 | wget \ 12 | bzip2 \ 13 | xvfb \ 14 | x11vnc \ 15 | fluxbox \ 16 | xterm 17 | 18 | # Install Google Chrome and dependencies 19 | RUN wget -qO /tmp/google.pub https://dl-ssl.google.com/linux/linux_signing_key.pub \ 20 | && apt-key add /tmp/google.pub \ 21 | && rm /tmp/google.pub \ 22 | && echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' > /etc/apt/sources.list.d/google.list \ 23 | && mkdir -p /usr/share/desktop-directories \ 24 | && apt-get -y update \ 25 | && apt-get install -y google-chrome-stable \ 26 | && apt-get clean \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | # Disable the SUID sandbox so that Chrome can launch without being in a privileged container 30 | RUN dpkg-divert --add --rename --divert /opt/google/chrome/google-chrome.real /opt/google/chrome/google-chrome \ 31 | && printf "#!/bin/bash\nexec /opt/google/chrome/google-chrome.real --no-sandbox --disable-setuid-sandbox \"\$@\"" > /opt/google/chrome/google-chrome \ 32 | && chmod 755 /opt/google/chrome/google-chrome 33 | 34 | # Install Chrome Driver (latest version) 35 | RUN CHROME_VER=$(google-chrome --version | grep -oP "Google Chrome \K[\d.]+") \ 36 | && echo "Chrome version: $CHROME_VER" \ 37 | && wget --no-verbose -O /tmp/chromedriver-linux64.zip "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VER}/linux64/chromedriver-linux64.zip" \ 38 | && rm -rf /opt/selenium/chromedriver \ 39 | && mkdir -p /opt/selenium \ 40 | && unzip /tmp/chromedriver-linux64.zip -d /opt/selenium \ 41 | && rm /tmp/chromedriver-linux64.zip \ 42 | && mv /opt/selenium/chromedriver-linux64/chromedriver /usr/bin/chromedriver \ 43 | && chmod 755 /usr/bin/chromedriver 44 | 45 | ARG FIREFOX_VERSION=latest 46 | RUN FIREFOX_DOWNLOAD_URL="$(if [ "$FIREFOX_VERSION" = "latest" ]; then echo "https://download.mozilla.org/?product=firefox-"$FIREFOX_VERSION"-ssl&os=linux64&lang=en-US"; else echo "https://download-installer.cdn.mozilla.net/pub/firefox/releases/"$FIREFOX_VERSION"/linux-x86_64/en-US/firefox-"$FIREFOX_VERSION".tar.bz2"; fi)" \ 47 | && echo "Firefox download URL: $FIREFOX_DOWNLOAD_URL" \ 48 | && apt-get -qqy update \ 49 | && apt-get -qqy --no-install-recommends install firefox \ 50 | && apt-get -y install libdbus-glib-1-2 \ 51 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* \ 52 | && wget --no-verbose -O /tmp/firefox.tar.bz2 "$FIREFOX_DOWNLOAD_URL" \ 53 | && apt-get -y purge firefox \ 54 | && rm -rf /opt/firefox \ 55 | && tar -C /opt -xjf /tmp/firefox.tar.bz2 \ 56 | && rm /tmp/firefox.tar.bz2 \ 57 | && mv /opt/firefox /opt/firefox-"$FIREFOX_VERSION" \ 58 | && ln -fs /opt/firefox-"$FIREFOX_VERSION"/firefox /usr/bin/firefox \ 59 | && apt-get clean \ 60 | && rm -rf /var/lib/apt/lists/* 61 | 62 | # Download and install the latest Geckodriver binary 63 | RUN GECKODRIVER_VERSION=$(wget -qO- 'https://api.github.com/repos/mozilla/geckodriver/releases/latest' | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') \ 64 | && wget --no-verbose -O /tmp/geckodriver.tar.gz "https://github.com/mozilla/geckodriver/releases/download/v${GECKODRIVER_VERSION}/geckodriver-v${GECKODRIVER_VERSION}-linux64.tar.gz" \ 65 | && tar -xzf /tmp/geckodriver.tar.gz -C /tmp \ 66 | && mv /tmp/geckodriver /usr/bin/geckodriver \ 67 | && rm /tmp/geckodriver.tar.gz 68 | 69 | # Python 3.10 and Python Pip 70 | RUN apt-get update && apt-get install -y \ 71 | python3.10 \ 72 | python3-setuptools=59.6.0-1.2ubuntu0.22.04.1 \ 73 | python3-pip=22.0.2+dfsg-1ubuntu0.4 \ 74 | && apt-get clean \ 75 | && rm -rf /var/lib/apt/lists/* 76 | 77 | # Creating a new directory 78 | RUN mkdir /shell_script 79 | 80 | # Copying shell script to directory 81 | COPY entrypoint.sh /shell_script 82 | 83 | # Setting the working directory 84 | WORKDIR /shell_script 85 | 86 | # Setting the entry point 87 | ENTRYPOINT ["/bin/bash", "/shell_script/entrypoint.sh"] 88 | 89 | # Setting the default command to be run in the container 90 | CMD ["sh", "-c", "Xvfb :20 -screen 0 1366x768x16 & x11vnc -passwd password -display :20 -N -forever"] 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Qxf2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/__init__.py -------------------------------------------------------------------------------- /api_auto_generator/Endpoint_Generator.md: -------------------------------------------------------------------------------- 1 | # Auto Generate Endpoint modules for API Automation Framework # 2 | The Endpoint generator project helps automate creating API automation tests using Qxf2's API Automation framework. It generates Endpoint modules - an abstraction for endpoints in the application under test from an OpenAPI specification. 3 | 4 | ## Requirements ## 5 | - An V3.x.x OpenAPI specification for your API app. 6 | - The spec file can be a `JSON` or `YAML` file. 7 | 8 | ## How to run the script? ## 9 | - Validate the OpenAPI specification 10 | ``` 11 | python api_auto_generator/endpoint_module_generator --spec 12 | ``` 13 | This command will help check if the OpenAPI spec can be used to generate Endpoints file. It will raise an exception for invalid or incomplete specs. 14 | - Generate the `Endpoint` module 15 | ``` 16 | python api_auto_generator/endpoint_module_generator --spec --generate-endpoints 17 | ``` 18 | This command will generate `_Endpoint.py` module in the `endpoints` dir. 19 | 20 | ## How does the script work? ## 21 | - The script uses `openapi3_parser` module to parse and read the contents from an OpenAPI spec. 22 | - The endpoints and its details are read from the spec 23 | - A module-name, class-name, instance-method-names are all generated for the endpoint 24 | - The Path & Query parameters to be passed to the Endpoint class is generated 25 | - The json/data params to be passed to the requests method is generated from the request body 26 | - A Python dictionary collecting all these values is generated 27 | - The generated Python dictionary is redered on a Jinja2 template 28 | 29 | ## Limitations/Constraints on using the Generate Endpoint script ## 30 | 31 | ### Invalid OpenAPI spec ### 32 | - The Generate Endpoint script validates the OpenAPI spec at the start of the execution, using an invalid spec triggers an exception 33 | - The JSON Schema validation is also done in step #1, but the exception raised regarding a JSON Schema error can sometimes be a little confusing, in such cases replace the failing schema with {} to proceed to generate Endpoint files 34 | 35 | ### Minimal spec ### 36 | - When using a minimal spec, to check if Endpoint files can be generated from it, run the script using --spec CLI param alone, you can proceed to use --generate-endpoint param if no issue was seen with the previous step -------------------------------------------------------------------------------- /api_auto_generator/endpoint_module_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | What does this module do? 3 | - It creates an Endpoint file with a class from the OpenAPI spec 4 | - The path key in the spec is translated to an Endpoint 5 | - The operations(http methods) for a path is translated to instance methods for the Endpoint 6 | - The parameters for operations are translated to function parameters for the instance methods 7 | - HTTP Basic, HTTP Bearer and API Keys Auth are currently supported by this module & should passed 8 | through headers 9 | """ 10 | 11 | 12 | from argparse import ArgumentParser 13 | from pathlib import Path 14 | from jinja2 import FileSystemLoader, Environment 15 | from jinja2.exceptions import TemplateNotFound 16 | from loguru import logger 17 | from openapi_spec_parser import OpenAPISpecParser 18 | 19 | 20 | # pylint: disable=line-too-long 21 | # Get the template file location & endpoint destination location relative to this script 22 | ENDPOINT_TEMPLATE_NAME = Path(__file__).parent.joinpath("templates").joinpath("endpoint_template.jinja2") # <- Jinja2 template needs to be on the same directory as this script 23 | ENDPOINT_DESTINATION_DIR = Path(__file__).parent.parent.joinpath("endpoints") # <- The Endpoint files are created in the endpoints dir in the project root 24 | 25 | 26 | class EndpointGenerator(): 27 | """ 28 | A class to Generate Endpoint module using Jinja2 template 29 | """ 30 | 31 | 32 | def __init__(self, logger_obj: logger): 33 | """ 34 | Initialize Endpoint Generator class 35 | """ 36 | self.endpoint_template_filename = ENDPOINT_TEMPLATE_NAME.name 37 | self.jinja_template_dir = ENDPOINT_TEMPLATE_NAME.parent.absolute() 38 | self.logger = logger_obj 39 | self.jinja_environment = Environment(loader=FileSystemLoader(self.jinja_template_dir), 40 | autoescape=True) 41 | 42 | 43 | def endpoint_class_content_generator(self, 44 | endpoint_class_name: str, 45 | endpoint_class_content: dict) -> str: 46 | """ 47 | Create Jinja2 template content 48 | """ 49 | content = None 50 | template = self.jinja_environment.get_template(self.endpoint_template_filename) 51 | content = template.render(class_name=endpoint_class_name, class_content=endpoint_class_content) 52 | self.logger.info(f"Rendered content for {endpoint_class_name} class using Jinja2 template") 53 | return content 54 | 55 | 56 | def generate_endpoint_file(self, 57 | endpoint_filename: str, 58 | endpoint_class_name: str, 59 | endpoint_class_content: dict): 60 | """ 61 | Create an Endpoint file 62 | """ 63 | try: 64 | endpoint_filename = ENDPOINT_DESTINATION_DIR.joinpath(endpoint_filename+'.py') 65 | endpoint_content = self.endpoint_class_content_generator(endpoint_class_name, 66 | endpoint_class_content) 67 | with open(endpoint_filename, 'w', encoding='utf-8') as endpoint_f: 68 | endpoint_f.write(endpoint_content) 69 | except TemplateNotFound: 70 | self.logger.error(f"Unable to find {ENDPOINT_TEMPLATE_NAME.absolute()}") 71 | except Exception as endpoint_creation_err: 72 | self.logger.error(f"Unable to generate Endpoint file - {endpoint_filename} due to {endpoint_creation_err}") 73 | else: 74 | self.logger.success(f"Successfully generated Endpoint file - {endpoint_filename.name}") 75 | 76 | 77 | if __name__ == "__main__": 78 | arg_parser = ArgumentParser(prog="GenerateEndpointFile", 79 | description="Generate Endpoint.py file from OpenAPI spec") 80 | arg_parser.add_argument("--spec", 81 | dest="spec_file", 82 | required=True, 83 | help="Pass the location to the OpenAPI spec file, Passing this param alone will run a dry run of endpoint content generation with actually creating the endpoint") 84 | arg_parser.add_argument("--generate-endpoints", 85 | dest='if_generate_endpoints', 86 | action='store_true', 87 | help="This param will create _Endpoint.py file for Path objects from the OpenAPI spec") 88 | 89 | args = arg_parser.parse_args() 90 | try: 91 | parser = OpenAPISpecParser(args.spec_file, logger) 92 | if args.if_generate_endpoints: 93 | endpoint_generator = EndpointGenerator(logger) 94 | for module_name, file_content in parser.parsed_dict.items(): 95 | for class_name, class_content in file_content.items(): 96 | endpoint_generator.generate_endpoint_file(module_name, 97 | class_name, 98 | class_content) 99 | except Exception as ep_generation_err: 100 | raise ep_generation_err 101 | -------------------------------------------------------------------------------- /api_auto_generator/endpoint_name_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to generate: 3 | 1. Module name 4 | 2. Class name 5 | 3. Method name 6 | """ 7 | 8 | 9 | import re 10 | from typing import Union 11 | from packaging.version import Version, InvalidVersion 12 | 13 | 14 | class NameGenerator(): 15 | "Base class for generating names" 16 | 17 | 18 | def __init__(self, 19 | endpoint_url: str, 20 | if_query_param: bool, 21 | path_params: list, 22 | requestbody_type: str): 23 | "Init NameGen object" 24 | self.endpoint_split, self.api_version_num = self.split_endpoint_string(endpoint_url) 25 | self.common_base = self.endpoint_split[0] 26 | self.endpoints_in_a_file = [ ep for ep in re.split("-|_", self.common_base)] 27 | self.if_query_param = if_query_param 28 | self.path_params = path_params 29 | self.requestbody_type = requestbody_type 30 | 31 | 32 | @property 33 | def module_name(self) -> str : 34 | "Module name for an Endpoint" 35 | return "_".join(self.endpoints_in_a_file) + "_" + "endpoint" 36 | 37 | 38 | @property 39 | def class_name(self) -> str : 40 | "Class name for Endpoint" 41 | capitalized_endpoints_in_a_file = [ ep.capitalize() for ep in self.endpoints_in_a_file] 42 | return "".join(capitalized_endpoints_in_a_file) + "Endpoint" 43 | 44 | 45 | @property 46 | def url_method_name(self) -> str : 47 | "URL method name for endpoint" 48 | return self.common_base.lower().replace('-', '_') + "_" + "url" 49 | 50 | 51 | @property 52 | def base_api_param_string(self) -> str : 53 | "Base API method parameter string" 54 | param_string = "" 55 | if self.if_query_param: 56 | param_string += ", params=params" 57 | if self.requestbody_type == "json": 58 | param_string += ", json=json" 59 | if self.requestbody_type == "data": 60 | param_string += ", data=data" 61 | param_string += ", headers=headers" 62 | return param_string 63 | 64 | 65 | @property 66 | def instance_method_param_string(self) -> str : 67 | "Instance method parameter string" 68 | param_string = "self" 69 | if self.if_query_param: 70 | param_string += ", params" 71 | for param in self.path_params: 72 | param_string += f", {param[0]}" 73 | if self.requestbody_type == "json": 74 | param_string += ", json" 75 | if self.requestbody_type == "data": 76 | param_string += ", data" 77 | param_string += ', headers' 78 | return param_string 79 | 80 | 81 | def get_instance_method_name(self, http_method: str) -> str : 82 | "Generate Instance method name" 83 | endpoint_split = [ ep.lower().replace('-','_') for ep in self.endpoint_split ] 84 | return http_method + "_" + "_".join(endpoint_split) 85 | 86 | 87 | def split_endpoint_string(self, endpoint_url: str) -> tuple[list[str], Union[str,None]]: 88 | """ 89 | Split the text in the endpoint, clean it up & return a list of text 90 | """ 91 | version_num = None 92 | if endpoint_url == "/": # <- if the endpoint is only / 93 | endpoint_split = ["home_base"] # <- make it /home_base (it needs to be unique) 94 | else: 95 | endpoint_split = endpoint_url.split("/") 96 | # remove {} from path paramters in endpoints 97 | endpoint_split = [ re.sub("{|}","",text) for text in endpoint_split if text ] 98 | for split_values in endpoint_split: 99 | try: 100 | if_api_version = Version(split_values) # <- check if version number present 101 | version_num = [ str(num) for num in if_api_version.release ] 102 | version_num = '_'.join(version_num) 103 | endpoint_split.remove(split_values) 104 | except InvalidVersion: 105 | if split_values == "api": 106 | endpoint_split.remove(split_values) 107 | return (endpoint_split, version_num,) 108 | -------------------------------------------------------------------------------- /api_auto_generator/templates/endpoint_template.jinja2: -------------------------------------------------------------------------------- 1 | {#- This template is used to generate Endpoints file for the API Test Automation Framework -#} 2 | """ 3 | This Endpoint file is generated using the api_auto_generator/endpoint_module_generator.py module 4 | """ 5 | from .base_api import BaseAPI 6 | 7 | 8 | class {{class_name}}(BaseAPI): 9 | 10 | 11 | def {{class_content['url_method_name']}}(self, suffix=''): 12 | "Append endpoint to base URI" 13 | return self.base_url + suffix 14 | 15 | {% for function in class_content['instance_methods'] -%} {#- No need to enclose paths in {{}} in for step -#} 16 | {%- for function_name, function_value in function.items() %} 17 | def {{function_name}}({{function_value['instance_method_param_string']}}): 18 | """ 19 | Run {{function_value['http_method']}} request against {{function_value['endpoint']}} 20 | :parameters: 21 | {%- if function_value['params']['query_params'] %} 22 | :params: dict 23 | {%- for query_param in function_value['params']['query_params'] %} 24 | :{{query_param[0]}}: {{query_param[1]}} 25 | {%- endfor %} 26 | {%- endif %} 27 | {%- if function_value['params']['path_params'] %} 28 | {%- for path_param in function_value['params']['path_params'] %} 29 | :{{path_param[0]}}: {{path_param[1]}} 30 | {%- endfor %} 31 | {%- endif %} 32 | {%- if function_value['params']['json_params'] %} 33 | :json: dict 34 | {%- if function_value['params']['content_schema_type'] == 'array' %} 35 | :list: 36 | {%- endif %} 37 | {%- for json_param in function_value['params']['json_params'] %} 38 | {%- if json_param is mapping %} 39 | {%- for json_key, json_value in json_param.items() %} 40 | :{{json_key}}: dict 41 | {%- for nested_json_value in json_value %} 42 | :{{nested_json_value[0]}}: {{nested_json_value[1]}} 43 | {%- endfor %} 44 | {%- endfor %} 45 | {%- else %} 46 | :{{json_param[0]}}: {{json_param[1]}} 47 | {%- endif %} 48 | {%- endfor %} 49 | {%- endif %} 50 | {%- if function_value['params']['data_params'] %} 51 | :data: dict 52 | {%- if function_value['params']['content_schema_type'] == 'array' %} 53 | :list: 54 | {%- endif %} 55 | {%- for data_param in function_value['params']['data_params'] %} 56 | {%- if data_param is mapping %} 57 | {%- for data_key, data_value in data_param.items() %} 58 | :{{data_key}}: dict 59 | {%- for nested_data_value in data_value %} 60 | :{{nested_data_value[0]}}: {{nested_data_value[1]}} 61 | {%- endfor %} 62 | {%- endfor %} 63 | {%- else %} 64 | :{{data_param[0]}}: {{data_param[1]}} 65 | {%- endif %} 66 | {%- endfor %} 67 | {%- endif %} 68 | """ 69 | url = self.{{class_content['url_method_name']}}(f"{{function_value['endpoint']}}") 70 | response = self.make_request(method='{{function_value["http_method"]}}', url=url{{function_value['base_api_param_string']}}) 71 | return { 72 | 'url' : url, 73 | 'response' : response 74 | } 75 | {% endfor %} 76 | {% endfor %} 77 | -------------------------------------------------------------------------------- /bandit.yml: -------------------------------------------------------------------------------- 1 | # Skip flagging assert statement inclusion in test during Codacy check 2 | assert_used: 3 | skips: ['*/test_*.py'] -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/conf/__init__.py -------------------------------------------------------------------------------- /conf/api_example_conf.py: -------------------------------------------------------------------------------- 1 | #add_car 2 | car_details = {'name':'figo','brand':'ford','price_range':'5-8 lacs','car_type':'hatchback'} 3 | 4 | #get_car_details 5 | car_name_1 = 'Swift' 6 | brand = 'Maruti' 7 | 8 | #update 9 | car_name_2 = 'figo' 10 | update_car = {'name':'figo','brand':'Ford','price_range':'2-3lacs','car_type':'hatchback'} 11 | 12 | #register_car 13 | customer_details = {'customer_name':'Rohan','city':'Ponda'} 14 | 15 | #authentication details 16 | user_name = 'eric' 17 | password = 'testqxf2' 18 | 19 | #invalid auth details 20 | invalid_user_name = 'unknown' 21 | invalid_password = 'unknown' 22 | -------------------------------------------------------------------------------- /conf/base_url_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file for base_url 3 | """ 4 | ui_base_url = "https://qxf2.com/" 5 | api_base_url= "https://cars-app.qxf2.com" 6 | 7 | -------------------------------------------------------------------------------- /conf/browser_os_name_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to generate the cross browser cross platform test run configuration 3 | """ 4 | 5 | import os 6 | 7 | 8 | #Conf list for local 9 | default_browser = ["chrome"] #default browser for the tests to run against when -B option is not used 10 | local_browsers = ["firefox","chrome"] #local browser list against which tests would run if no -M Y and -B all is used 11 | 12 | 13 | #Conf list for Browserstack/Sauce Labs 14 | #change this depending on your client 15 | 16 | browsers = ["firefox","chrome","safari"] #browsers to generate test run configuration to run on Browserstack/Sauce Labs 17 | firefox_versions = ["121","122"] #firefox versions for the tests to run against on Browserstack/Sauce Labs 18 | chrome_versions = ["121","122"] #chrome versions for the tests to run against on Browserstack/Sauce Labs 19 | safari_versions = ["15.3"] #safari versions for the tests to run against on Browserstack/Sauce Labs 20 | os_list = ["windows","OS X"] #list of os for the tests to run against on Browserstack/Sauce Labs 21 | windows_versions = ["10","11"] #list of windows versions for the tests to run against on Browserstack/Sauce Labs 22 | os_x_versions = ["Monterey"] #list of os x versions for the tests to run against on Browserstack/Sauce Labs 23 | sauce_labs_os_x_versions = ["10.10"] #Set if running on sauce_labs instead of "yosemite" 24 | default_config_list = [("chrome","latest","windows","11")] #default configuration against which the test would run if no -B all option is used 25 | # Define default os versions based on os 26 | default_os_versions = { 27 | "windows": "11", 28 | "os x": "sequoia" 29 | } 30 | 31 | def generate_configuration(browsers=browsers,firefox_versions=firefox_versions,chrome_versions=chrome_versions,safari_versions=safari_versions, 32 | os_list=os_list,windows_versions=windows_versions,os_x_versions=os_x_versions): 33 | 34 | "Generate test configuration" 35 | if os.getenv('REMOTE_BROWSER_PLATFORM') == 'SL': 36 | os_x_versions = sauce_labs_os_x_versions 37 | test_config = [] 38 | for browser in browsers: 39 | if browser == "firefox": 40 | for firefox_version in firefox_versions: 41 | for os_name in os_list: 42 | if os_name == "windows": 43 | for windows_version in windows_versions: 44 | config = [browser,firefox_version,os_name,windows_version] 45 | test_config.append(tuple(config)) 46 | if os_name == "OS X": 47 | for os_x_version in os_x_versions: 48 | config = [browser,firefox_version,os_name,os_x_version] 49 | test_config.append(tuple(config)) 50 | if browser == "chrome": 51 | for chrome_version in chrome_versions: 52 | for os_name in os_list: 53 | if os_name == "windows": 54 | for windows_version in windows_versions: 55 | config = [browser,chrome_version,os_name,windows_version] 56 | test_config.append(tuple(config)) 57 | if os_name == "OS X": 58 | for os_x_version in os_x_versions: 59 | config = [browser,chrome_version,os_name,os_x_version] 60 | test_config.append(tuple(config)) 61 | if browser == "safari": 62 | for safari_version in safari_versions: 63 | for os_name in os_list: 64 | if os_name == "OS X": 65 | for os_x_version in os_x_versions: 66 | config = [browser,safari_version,os_name,os_x_version] 67 | test_config.append(tuple(config)) 68 | 69 | 70 | 71 | return test_config 72 | 73 | #variable to hold the configuration that can be imported in the conftest.py file 74 | cross_browser_cross_platform_config = generate_configuration() 75 | -------------------------------------------------------------------------------- /conf/clean_up_repo_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | The file will have relative paths for dir and respective files which clean_up_repo.py will delete. 3 | """ 4 | import os 5 | 6 | # Declaring directories as directory list 7 | # dir_list : list 8 | REPO_DIR = os.path.dirname(os.path.dirname(__file__)) 9 | CONF_DIR = os.path.join(REPO_DIR, 'conf') 10 | ENDPOINTS_DIR = os.path.join(REPO_DIR, 'endpoints') 11 | PAGE_OBJECTS_EXAMPLES_DIR = os.path.join(REPO_DIR, 'page_objects','examples') 12 | TEST_DIR = os.path.join(REPO_DIR, 'tests') 13 | dir_list = [CONF_DIR, ENDPOINTS_DIR, TEST_DIR] 14 | 15 | # Declaring files as file_list 16 | # file_list : list 17 | CONF_FILES_DELETE = ['api_example_conf.py', 18 | 'cars_api_openapi_spec.json', 19 | 'env_remote_enc', 20 | 'example_form_conf.py', 21 | 'example_table_conf.py', 22 | 'mobile_bitcoin_conf.py', 23 | 'mobile_weather_shopper_conf.py', 24 | 'successive_form_creation_conf.py', 25 | 'weather_shopper_mobile_conf.py'] 26 | ENDPOINTS_FILES_DELETE = ['cars_api_endpoints.py', 27 | 'registration_api_endpoints.py', 28 | 'user_api_endpoints.py'] 29 | TEST_FILES_DELETE = ['test_accessibility.py', 30 | 'test_api_async_example.py', 31 | 'test_api_endpoint_auto_generation.py', 32 | 'test_example_table.py', 33 | 'test_api_example.py', 34 | 'test_mobile_bitcoin_price.py', 35 | 'test_successive_form_creation.py', 36 | 'test_example_form.py', 37 | 'test_weather_shopper_app_menu_options.py', 38 | 'test_weather_shopper_app.py', 39 | 'test_weather_shopper_payment_app.py'] 40 | 41 | file_list = [CONF_FILES_DELETE, ENDPOINTS_FILES_DELETE, TEST_FILES_DELETE] 42 | -------------------------------------------------------------------------------- /conf/copy_framework_template_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This conf file would have the relative paths of the files & folders. 3 | """ 4 | import os 5 | 6 | #Files from src: 7 | src_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','__init__.py')) 8 | src_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','conftest.py')) 9 | src_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','Readme.md')) 10 | src_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','requirements.txt')) 11 | src_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','.gitignore')) 12 | src_file6 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','env_conf')) 13 | src_file7 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','env_remote')) 14 | src_file8 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','env_ssh_conf')) 15 | src_file9 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','pytest.ini')) 16 | 17 | #src file list: 18 | src_files_list = [src_file1,src_file2,src_file3,src_file4,src_file5,src_file6,src_file7,src_file8, 19 | src_file9] 20 | 21 | #CONF 22 | #files from src conf: 23 | src_conf_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'testrail_caseid_conf.py')) 24 | src_conf_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'base_url_conf.py')) 25 | src_conf_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'browser_os_name_conf.py')) 26 | src_conf_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'clean_up_repo_conf.py')) 27 | src_conf_file5 = os.path.abspath(os.path.join(os.path.dirname(__file__),'gpt_summarization_prompt.py')) 28 | src_conf_file6 = os.path.abspath(os.path.join(os.path.dirname(__file__),'locators_conf.py')) 29 | src_conf_file7 = os.path.abspath(os.path.join(os.path.dirname(__file__),'ports_conf.py')) 30 | src_conf_file8 = os.path.abspath(os.path.join(os.path.dirname(__file__),'remote_url_conf.py')) 31 | src_conf_file9 = os.path.abspath(os.path.join(os.path.dirname(__file__),'screenshot_conf.py')) 32 | src_conf_file10 = os.path.abspath(os.path.join(os.path.dirname(__file__),'snapshot_dir_conf.py')) 33 | src_conf_file11 = os.path.abspath(os.path.join(os.path.dirname(__file__),'__init__.py')) 34 | 35 | #src Conf file list: 36 | src_conf_files_list = [src_conf_file1,src_conf_file2,src_conf_file3,src_conf_file4,src_conf_file5, 37 | src_conf_file6,src_conf_file7,src_conf_file8,src_conf_file9,src_conf_file10, 38 | src_conf_file11] 39 | 40 | #Page_Objects 41 | #files from src page_objects: 42 | src_page_objects_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','zero_page.py')) 43 | src_page_objects_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','zero_mobile_page.py')) 44 | src_page_objects_file3 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','PageFactory.py')) 45 | src_page_objects_file4 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','page_objects','__init__.py')) 46 | 47 | #src page_objects file list: 48 | src_page_objects_files_list = [src_page_objects_file1,src_page_objects_file2,src_page_objects_file3,src_page_objects_file4] 49 | 50 | #tests 51 | #files from tests: 52 | src_tests_file1 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','tests','__init__.py')) 53 | src_tests_file2 = os.path.abspath(os.path.join(os.path.dirname(__file__),'..','tests','test_boilerplate.py')) 54 | 55 | #src tests file list: 56 | src_tests_files_list = [src_tests_file1, src_tests_file2] 57 | -------------------------------------------------------------------------------- /conf/env_remote_enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/conf/env_remote_enc -------------------------------------------------------------------------------- /conf/example_form_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file for test_example_form 3 | """ 4 | 5 | name = "knight" 6 | email = "knight@qxf2.com" 7 | phone_no = 1111111111 8 | gender = "Male" -------------------------------------------------------------------------------- /conf/example_table_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file for test_example_table 3 | """ 4 | 5 | name = "Michael" -------------------------------------------------------------------------------- /conf/gpt_summarization_prompt.py: -------------------------------------------------------------------------------- 1 | summarization_prompt = """ 2 | You are a helpful assistant who specializes in providing technical solutions or recommendations to errors in Python. You will receive the contents of a consolidated log file containing results of automation tests run using Pytest. These tests were conducted using a Python Selenium framework. 3 | Carefully analyze the contents of the log file and summarize the results focussing on providing technical recommendations for any errors encountered. Provide the response in JSON format. Follow the below instructions: 4 | 5 | - Start with a heading that says "SummaryOfTestResults". 6 | 7 | - Use the "PassedTests" key to list the names of the tests that have passed along with the number of checks passed within each test. In the log file, for each test, check for the presence of information regarding the total number of checks and the number of checks passed. If this information is present, extract the number of checks passed and include it in the summary by indicating the test name followed by the number of checks passed, separated by a colon. Avoid providing detailed information about individual checks and about mini-checks too. 8 | 9 | - Use the "FailedTests" key to list the names of the failed tests. 10 | - For each failed test, provide the following information: 11 | - Use the "test_name" key to indicate the name of the failed test. 12 | - Use the "reasons_for_failure" key to provide the specific failure message encountered during the test execution. This should include the error message or relevant failure information. Look for messages categorized as DEBUG, ERROR, or CRITICAL to extract the reasons for failure. 13 | - Use the "recommendations" key to provide suggestions on how to fix the errors encountered for that test. The recommendations should be based on the failures listed in the "reasons_for_failure" key, ensuring that the suggestions are contextually relevant to the specific issues identified during the test execution. 14 | 15 | - Do not give general recommendations to improve tests. 16 | - Exclude any information related to assertion errors, and do not provide recommendations specific to assertion failures. 17 | - Before providing recommendations, review the list of predefined reasons_for_failure outlined below. While analyzing the log file, if any of these predefined reasons_for_failure are encountered, include the corresponding recommendations provided for that specific failure. 18 | 19 | - List of reasons_for_failure: 20 | 1. reasons_for_failure: 21 | * Browser console error on url 22 | * Failed to load resource: the server responded with a status of 404 () 23 | recommendations: 24 | * Verify the URL and check for typos. 25 | * Ensure the requested resource exists on the server or hasn't been moved/deleted. 26 | 27 | 2. reasons_for_failure: 28 | * Multiple or all locators of test has 'no such element: Unable to locate element:' error. 29 | recommendations: 30 | * Verify the URL and check for typos. 31 | * Review for any recent changes in the web page that could affect element visibility. 32 | 33 | 3. reasons_for_failure: 34 | * 'Unknown error: net::ERR_NAME_NOT_RESOLVED' error for all tests 35 | recommendations: 36 | * Check if the base URL is correct. 37 | * Ensure that the hostname is resolvable. 38 | * Check for network issues or DNS problems that might be preventing the browser from resolving the domain name. 39 | 40 | 4. reasons_for_failure: 41 | * Browser driver executable needs to be in PATH. 42 | recommendations: 43 | * Ensure that the executable for the specific browser driver (eg. 'chromedriver', 'geckodriver' etc.) is installed and its location is added to the system PATH. 44 | 45 | The response should be in JSON format like this: 46 | """ -------------------------------------------------------------------------------- /conf/locators_conf.py: -------------------------------------------------------------------------------- 1 | #Common locator file for all locators 2 | #Locators are ordered alphabetically 3 | 4 | ############################################ 5 | #Selectors we can use 6 | #ID 7 | #NAME 8 | #css selector 9 | #CLASS_NAME 10 | #LINK_TEXT 11 | #PARTIAL_LINK_TEXT 12 | #XPATH 13 | ########################################### 14 | 15 | #Locators for the footer object(footer_object.py) 16 | 17 | footer_menu = "xpath,//ul[contains(@class,'nav-justified')]/descendant::a[text()='%s']" 18 | copyright_text = "xpath,//p[contains(@class,'qxf2_copyright')]" 19 | #---- 20 | 21 | #Locators for the form object(form_object.py) 22 | name_field = "id,name" 23 | email_field = "name,email" 24 | phone_no_field = "css selector,#phone" 25 | click_me_button = "xpath,//button[text()='Click me!']" 26 | gender_dropdown = "xpath,//button[@data-toggle='dropdown']" 27 | gender_option = "xpath,//a[text()='%s']" 28 | tac_checkbox = "xpath,//input[@type='checkbox']" 29 | #---- 30 | 31 | #Locators for hamburger menu object(hamburg_menu_object.py) 32 | menu_icon = "xpath,//img[@alt='Menu']" 33 | menu_link = "xpath,//ul[contains(@class,'dropdown-menu')]/descendant::a[text()='%s']" 34 | menu_item = "xpath,//ul[contains(@class,'dropdown-menu')]/descendant::a[@data-toggle='dropdown' and text()='%s']" 35 | #---- 36 | 37 | #Locators for header object(header_object.py) 38 | qxf2_logo = "xpath,//img[contains(@src,'qxf2_logo.png')]" 39 | qxf2_tagline_part1 = "xpath,//h1[contains(@class,'banner-brown') and text()='SOFTWARE TESTING SERVICES']" 40 | qxf2_tagline_part2 = "xpath,//h1[contains(@class,'banner-grey') and text()='for startups']" 41 | #---- 42 | 43 | #Locators for table object(table_object.py) 44 | table_xpath = "xpath,//table[@name='Example Table']" 45 | rows_xpath = "xpath,//table[@name='Example Table']//tbody/descendant::tr" 46 | cols_xpath = "xpath,//table[@name='Example Table']//tbody/descendant::td" 47 | cols_relative_xpath = "xpath,//table[@name='Example Table']//tbody/descendant::tr[%d]/descendant::td" 48 | cols_header = "xpath,//table[@name='Example Table']//thead/descendant::th" 49 | #---- 50 | 51 | #Locators for tutorial redirect page(tutorial_redirect_page.py) 52 | heading = "xpath,//h2[contains(@class,'grey_text') and text()='Selenium for beginners: Practice page 2']" 53 | 54 | #Locators for Contact Object(contact_object.py) 55 | contact_name_field = "id,name" 56 | 57 | #Locators for mobile application - Bitcoin Info(bitcoin_price_page.py) 58 | bitcoin_real_time_price_button = "xpath,//android.widget.TextView[@resource-id='com.dudam.rohan.bitcoininfo:id/current_price']" 59 | bitcoin_price_page_heading = "xpath,//android.widget.TextView[@text='Real Time Price of Bitcoin']" 60 | bitcoin_price_in_usd = "xpath,//android.widget.TextView[@resource-id='com.dudam.rohan.bitcoininfo:id/doller_value']" 61 | 62 | #Weather Shopper 63 | temperature = "id,textHome" 64 | moisturizers = "xpath,//android.widget.TextView[@text='Moisturizers']" 65 | sunscreens = "xpath,//android.widget.TextView[@text='Sunscreens']" 66 | cart = "xpath,//android.widget.TextView[@text='Cart']" 67 | recycler_view = "id,recyclerView" 68 | product_price = "id,text_product_price" 69 | product_name = "id,text_product_name" 70 | add_to_cart = "xpath,//android.widget.TextView[@text='{}']/parent::*/android.widget.Button[@text='ADD TO CART']" 71 | total_amount = "id,totalAmountTextView" 72 | edit_quantity = "xpath,//android.widget.TextView[@text='{}']/following-sibling::android.widget.LinearLayout/android.widget.EditText" 73 | refresh_button = "id,refreshButton" 74 | checkbox = "xpath,//android.widget.TextView[@text='{}']/ancestor::android.widget.LinearLayout/preceding-sibling::android.widget.CheckBox" 75 | delete_from_cart_button = "id,deleteSelectedButton" 76 | checkout_button = "id,checkoutButton" 77 | payment_method_dropdown = "id,payment_method_spinner" 78 | payment_card_type = "xpath,//android.widget.CheckedTextView[@text='{}']" 79 | payment_email = "id,email_edit_text" 80 | payment_card_number = "id,card_number_edit_text" 81 | payment_card_expiry = "id,expiration_date_edit_text" 82 | payment_card_cvv = "id,cvv_edit_text" 83 | pay_button = "id,pay_button" 84 | payment_success = "xpath,//android.widget.TextView[@text='Payment Successful']" 85 | image_of_moisturizer = "xpath,//android.widget.TextView[@text='Wilhelm Aloe Hydration Lotion']/parent::*/android.widget.ImageView" 86 | image_of_sunscreen = "xpath,//android.widget.TextView[@text='Robert Herbals Sunblock SPF-40']/parent::*/android.widget.ImageView" 87 | 88 | menu_option = "xpath,//android.widget.ImageView[@content-desc='More options']" 89 | developed_by = "xpath,//android.widget.TextView[@text='Developed by Qxf2 Services']" 90 | about_app = "xpath,//android.widget.TextView[@text='About This App']" 91 | framework = "xpath,//android.widget.TextView[@text='Qxf2 Automation Framework']" 92 | privacy_policy = "xpath,//android.widget.TextView[@text='Privacy Policy']" 93 | contact_us = "xpath,//android.widget.TextView[@text='Contact us']" 94 | chrome_welcome_dismiss = "xpath,//android.widget.Button[@resource-id='com.android.chrome:id/signin_fre_dismiss_button']" 95 | turn_off_sync_button = "xpath,//android.widget.Button[@resource-id='com.android.chrome:id/negative_button']" 96 | 97 | 98 | image_of_moisturizer = "xpath,//android.widget.TextView[@text='Wilhelm Aloe Hydration Lotion']/parent::*/android.widget.ImageView" 99 | image_of_sunscreen = "xpath,//android.widget.TextView[@text='Robert Herbals Sunblock SPF-40']/parent::*/android.widget.ImageView" 100 | -------------------------------------------------------------------------------- /conf/mobile_bitcoin_conf.py: -------------------------------------------------------------------------------- 1 | # Conf for bitcoin example 2 | 3 | 4 | # text for bitcoin real time price in usd 5 | expected_bitcoin_price_page_heading = "Real Time Price of Bitcoin" 6 | -------------------------------------------------------------------------------- /conf/mobile_weather_shopper_conf.py: -------------------------------------------------------------------------------- 1 | # Conf for Weather shopper mobile app 2 | menu_option = { 3 | "developed_by_label" : "Developed by Qxf2 Services", 4 | "developed_by_url" : "https://qxf2.com/?utm_source=menu&utm_medium=click&utm_campaign=mobile WeatherShopper app", 5 | "about_app_label" : "About This App", 6 | "about_app_url" : "https://qxf2.com/blog/weather-shopper-learn-appium/?utm_source=menu&utm_medium=click&utm_campaign=mobile WeatherShopper app", 7 | "framework_label" : "Qxf2 Automation Framework", 8 | "framework_url" : "https://github.com/qxf2/qxf2-page-object-model", 9 | "privacy_policy_label" : "Privacy Policy", 10 | "contact_us_label" : "Contact us", 11 | "contact_us_url" : "https://qxf2.com/contact?utm_source=menu&utm_medium=click&utm_campaign=mobile WeatherShopper app" 12 | } 13 | -------------------------------------------------------------------------------- /conf/ports_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Specify the port in which you want to run your local mobile tests 3 | """ 4 | port = 4723 5 | -------------------------------------------------------------------------------- /conf/remote_url_conf.py: -------------------------------------------------------------------------------- 1 | #The URLs for connecting to BrowserStack and Sauce Labs. 2 | 3 | browserstack_url = "http://hub-cloud.browserstack.com/wd/hub" 4 | browserstack_app_upload_url = "https://api-cloud.browserstack.com/app-automate/upload" 5 | browserstack_api_server_url = "https://api.browserstack.com/automate" 6 | browserstack_cloud_api_server_url = "https://api-cloud.browserstack.com" 7 | saucelabs_url = "https://ondemand.eu-central-1.saucelabs.com:443/wd/hub" 8 | saucelabs_app_upload_url = 'https://api.eu-central-1.saucelabs.com/v1/storage/upload' 9 | lambdatest_url = "http://{}:{}@hub.lambdatest.com/wd/hub" 10 | lambdatest_api_server_url = "https://api.lambdatest.com/automation/api/v1" 11 | -------------------------------------------------------------------------------- /conf/screenshot_conf.py: -------------------------------------------------------------------------------- 1 | BS_ENABLE_SCREENSHOTS = False 2 | overwrite_flag = False -------------------------------------------------------------------------------- /conf/snapshot_dir_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file for snapshot directory 3 | """ 4 | import os 5 | 6 | snapshot_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),'conf', 'snapshot') 7 | page_names = ["main", "redirect", "contact"] -------------------------------------------------------------------------------- /conf/successive_form_creation_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file for test_successive_form_creation 3 | """ 4 | 5 | form1 = {'NAME':'rook','EMAIL':'rook@qxf2.com','PHONE_NO':'1111111111','GENDER':'Male'} 6 | form2 = {'NAME':'pawn','EMAIL':'pawn@qxf2.com','PHONE_NO':'2222222222','GENDER':'Male'} 7 | form3 = {'NAME':'bishop','EMAIL':'bishop@qxf2.com','PHONE_NO':'3333333333','GENDER':'Male'} 8 | form4 = {'NAME':'queen','EMAIL':'queen@qxf2.com','PHONE_NO':'4444444444','GENDER':'Female'} 9 | form_list = [form1,form2,form3,form4] -------------------------------------------------------------------------------- /conf/testrail_caseid_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conf file to hold the testcase id 3 | """ 4 | 5 | test_example_form = 125 6 | test_example_table = 126 7 | test_example_form_name = 127 8 | test_example_form_email = 128 9 | test_example_form_phone = 129 10 | test_example_form_gender = 130 11 | test_example_form_footer_contact = 131 12 | test_bitcoin_price_page_header = 234 13 | test_bitcoin_real_time_price = 235 14 | test_successive_form_creation = 132 15 | test_accessibility = 140 16 | 17 | -------------------------------------------------------------------------------- /conf/weather_shopper_mobile_conf.py: -------------------------------------------------------------------------------- 1 | valid_payment_details = { 2 | "card_type": "Debit Card", 3 | "email": "qxf2tester@example.com", 4 | "card_number": "1234567890123456", 5 | "card_expiry": "12/25", 6 | "card_cvv": "123" 7 | } 8 | 9 | # Defining dictionaries with invalid field entries 10 | 11 | invalid_email_in_payment_details = { 12 | "card_type": "Debit Card", 13 | "email": "qxf2tester", 14 | "card_number": "1234567890123456", 15 | "card_expiry": "12/25", 16 | "card_cvv": "123", 17 | "image_name": "navigate_to_email_field", 18 | "validation_message": "Invalid email address" 19 | } 20 | 21 | invalid_cardnum_in_payment_details = { 22 | "card_type": "Credit Card", 23 | "email": "qxf2tester@example.com", 24 | "card_number": "1234567890", 25 | "card_expiry": "12/25", 26 | "card_cvv": "123", 27 | "image_name": "navigate_to_cardnumber_field", 28 | "validation_message": "Invalid card number" 29 | } 30 | 31 | -------------------------------------------------------------------------------- /core_helpers/custom_pytest_plugins.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module houses custom pytest plugins implemented 3 | Plugins added: 4 | - CustomTerminalReporter: Print a prettytable failure summary using pytest 5 | """ 6 | 7 | from _pytest.terminal import TerminalReporter 8 | from .prettytable_object import FailureSummaryTable # pylint: disable=relative-beyond-top-level 9 | 10 | class CustomTerminalReporter(TerminalReporter): # pylint: disable=subclassed-final-class 11 | "A custom pytest TerminalReporter plugin" 12 | def __init__(self, config): 13 | self.failed_scenarios = {} 14 | super().__init__(config) 15 | 16 | # Overwrite the summary_failures method to print the prettytable summary 17 | def summary_failures(self): 18 | if self.failed_scenarios: 19 | table = FailureSummaryTable() 20 | # Print header 21 | self.write_sep(sep="=", title="Failure Summary", red=True) 22 | # Print table 23 | table.print_table(self.failed_scenarios) 24 | -------------------------------------------------------------------------------- /core_helpers/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/core_helpers/drivers/__init__.py -------------------------------------------------------------------------------- /core_helpers/drivers/local_options.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the webrivers for local browsers. 3 | """ 4 | from selenium.webdriver.chrome.options import Options 5 | import sys 6 | from selenium import webdriver 7 | 8 | class LocalOptions(): 9 | """Class contains methods for getting webfrivers for various browsers.""" 10 | 11 | @staticmethod 12 | def firefox_local(browser_version): 13 | """Get webdriver for firefox.""" 14 | options = webdriver.FirefoxOptions() 15 | options.browser_version = browser_version 16 | local_driver = webdriver.Firefox(options=options) 17 | 18 | return local_driver 19 | 20 | @staticmethod 21 | def edge_local(browser_version): 22 | """Get webdriver for Edge.""" 23 | options = webdriver.EdgeOptions() 24 | options.browser_version = browser_version 25 | local_driver = webdriver.Edge(options=options) 26 | 27 | return local_driver 28 | 29 | @staticmethod 30 | def chrome_local(browser_version): 31 | """Get webdriver for chrome.""" 32 | options = webdriver.ChromeOptions() 33 | options.browser_version = browser_version 34 | local_driver = webdriver.Chrome(options=options) 35 | 36 | return local_driver 37 | 38 | @staticmethod 39 | def safari_local(): 40 | """Get webdriver for safari.""" 41 | local_driver = webdriver.Safari() 42 | 43 | return local_driver 44 | 45 | @staticmethod 46 | def headless_chrome(browser_version): 47 | """Set up headless chrome driver options and get webdriver for headless chrome.""" 48 | options = Options() 49 | options.headless = True 50 | options.browser_version = browser_version 51 | options.add_argument("--window-size=1920,1080") 52 | options.add_argument("--disable-extensions") 53 | options.add_argument("--proxy-server='direct://'") 54 | options.add_argument("--proxy-bypass-list=*") 55 | options.add_argument("--start-maximized") 56 | options.add_argument('--headless') 57 | options.add_argument('--disable-gpu') 58 | options.add_argument('--disable-dev-shm-usage') 59 | options.add_argument('--no-sandbox') 60 | options.add_argument('--ignore-certificate-errors') 61 | local_driver = webdriver.Chrome(options=options) 62 | 63 | return local_driver 64 | 65 | def app_details(self, desired_capabilities, app_package, app_activity): 66 | desired_capabilities['appPackage'] = app_package 67 | desired_capabilities['appActivity'] = app_activity 68 | return desired_capabilities 69 | 70 | def app_name(self, desired_capabilities, app_path, app_name): 71 | import os 72 | desired_capabilities['app'] = os.path.join(app_path, app_name) 73 | return desired_capabilities 74 | 75 | def ios_capabilities(self, desired_capabilities, app_package, no_reset_flag, ud_id, org_id, signing_id): 76 | desired_capabilities['bundleId'] = app_package 77 | desired_capabilities['noReset'] = no_reset_flag 78 | if ud_id is not None: 79 | desired_capabilities['udid'] = ud_id 80 | desired_capabilities['xcodeOrgId'] = org_id 81 | desired_capabilities['xcodeSigningId'] = signing_id 82 | return desired_capabilities 83 | 84 | def set_mobile_device(self, mobile_os_name, mobile_os_version, device_name, orientation): 85 | """Setup the mobile device.""" 86 | desired_capabilities = {} 87 | desired_capabilities['platformName'] = mobile_os_name 88 | desired_capabilities['platformVersion'] = mobile_os_version 89 | desired_capabilities['deviceName'] = device_name 90 | desired_capabilities['orientation'] = orientation 91 | 92 | return desired_capabilities 93 | -------------------------------------------------------------------------------- /core_helpers/gif_maker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: This utility is for creating a GIF of all the screenshots captured during current test run 3 | 4 | """ 5 | import imageio.v2 as imageio 6 | import os 7 | 8 | 9 | def make_gif(screenshot_dir_path,name = "test_recap",suffix=".gif",frame_duration=0.5): 10 | "Creates gif of the screenshots" 11 | gif_name = None 12 | images = [] 13 | 14 | if "/" in name: 15 | name=name.split("/")[-1] 16 | 17 | filenames = sorted(os.listdir(screenshot_dir_path)) 18 | if len(filenames) != 0: 19 | gif_name = os.path.join(screenshot_dir_path, name + suffix) 20 | for files in sorted(filenames): 21 | images.append(imageio.imread(os.path.join(screenshot_dir_path, files))) 22 | imageio.mimwrite(gif_name, images, duration=frame_duration) 23 | 24 | return gif_name -------------------------------------------------------------------------------- /core_helpers/logging_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper class for Logging Objects 3 | """ 4 | 5 | from utils.Base_Logging import Base_Logging 6 | from utils.stop_test_exception_util import Stop_Test_Exception 7 | import logging 8 | 9 | class Logging_Objects: 10 | def __init__(self): 11 | self.msg_list = [] 12 | self.exceptions = [] 13 | 14 | @staticmethod 15 | def color_text(text, color ="red"): 16 | colors = { 17 | "red": "\033[91m", 18 | "green": "\033[92m", 19 | "yellow": "\033[93m", 20 | "blue": "\033[94m", 21 | "magenta": "\033[95m", 22 | "cyan": "\033[96m", 23 | "reset": "\033[0m" 24 | } 25 | return f"{colors.get(color, colors['reset'])}{text}{colors['reset']}" 26 | 27 | def write_test_summary(self): 28 | "Print out a useful, human readable summary" 29 | if self.result_counter==self.pass_counter: 30 | level = "success" 31 | else: 32 | level = "error" 33 | self.write('\n\n************************\n--------RESULT--------\n',level=level) 34 | self.write('Total number of checks=%d'%self.result_counter,level=level) 35 | self.write('Total number of checks passed=%d\n----------------------\n************************\n\n'%self.pass_counter,level=level) 36 | if self.mini_check_counter > 0: 37 | self.write('Total number of mini-checks=%d'%self.mini_check_counter,level=level) 38 | self.write('Total number of mini-checks passed=%d'%self.mini_check_pass_counter,level=level) 39 | self.make_gif() 40 | if self.gif_file_name is not None: 41 | self.write("Screenshots & GIF created at %s"%self.screenshot_dir) 42 | if len(self.exceptions) > 0: 43 | self.exceptions = list(set(self.exceptions)) 44 | self.write('\n--------USEFUL EXCEPTION--------\n',level="critical") 45 | for (i,msg) in enumerate(self.exceptions,start=1): 46 | self.write(str(i)+"- " + msg,level="critical") 47 | 48 | def write(self,msg,level='info', trace_back=None): 49 | "Log the message" 50 | msg = str(msg) 51 | self.msg_list.append('%-8s: '%level.upper() + msg) 52 | self.log_obj.write(msg,level,trace_back) 53 | 54 | def success(self,msg,level='success',pre_format='PASS: '): 55 | "Write out a success message" 56 | self.log_obj.write(pre_format + msg,level) 57 | self.result_counter += 1 58 | self.pass_counter += 1 59 | 60 | def set_log_file(self): 61 | "set the log file" 62 | self.log_name = self.testname + '.log' 63 | self.log_obj = Base_Logging(log_file_name=self.log_name,level=logging.DEBUG) 64 | 65 | def log_result(self,flag,positive,negative,level='info'): 66 | "Write out the result of the test" 67 | if level.lower() == "inverse": 68 | if flag is True: 69 | self.failure(positive,level="error") 70 | # Collect the failed scenarios for prettytable summary 71 | self.failed_scenarios.append(positive) 72 | else: 73 | self.success(negative,level="success") 74 | else: 75 | if flag is True: 76 | self.success(positive,level="success") 77 | else: 78 | self.failure(negative,level="error") 79 | # Collect the failed scenarios for prettytable summary 80 | self.failed_scenarios.append(negative) 81 | 82 | def get_failure_message_list(self): 83 | "Return the failure message list" 84 | return self.failure_message_list 85 | 86 | def failure(self,msg,level='error',pre_format='FAIL: '): 87 | "Write out a failure message" 88 | self.log_obj.write(pre_format + msg,level) 89 | self.result_counter += 1 90 | self.failure_message_list.append(pre_format + msg) 91 | if level.lower() == 'critical': 92 | raise Stop_Test_Exception("Stopping test because: "+ msg) 93 | 94 | def set_rp_logger(self,rp_pytest_service): 95 | "Set the reportportal logger" 96 | self.rp_logger = self.log_obj.setup_rp_logging(rp_pytest_service) 97 | -------------------------------------------------------------------------------- /core_helpers/prettytable_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module to house prettytable custom objects 3 | This tail made objects are used by pytest to print table output of failure summary 4 | """ 5 | from prettytable.colortable import ColorTable, Theme, Themes 6 | 7 | #pylint: disable=too-few-public-methods 8 | class PrettyTableTheme(Themes): 9 | "A custom color theme object" 10 | Failure = Theme(default_color="31", 11 | vertical_color="31", 12 | horizontal_color="31", 13 | junction_color="31") 14 | 15 | class FailureSummaryTable(): 16 | "Failure Summary Table to be printed in the pytest result summary" 17 | def __init__(self) -> None: 18 | """ 19 | Initializer 20 | """ 21 | # Create a pretty color table in red to mark failures 22 | self.table = ColorTable(theme=PrettyTableTheme.Failure) 23 | self.table.field_names = ["Tests Failed"] 24 | self.table.align = "l" # <- Align the content of the table left 25 | 26 | def print_table(self, testsummary: dict) -> None: 27 | """ 28 | Print the Failure Summary table 29 | :param: 30 | :testsummary: A dict with testname as key and failed scenarios as a list 31 | """ 32 | try: 33 | for testname, testscenarios in testsummary.items(): 34 | self.table.add_row([f"{testname}:"]) 35 | for scenario in testscenarios: 36 | self.table.add_row([f"\u2717 {scenario}"]) # <- Add unicode x mark 37 | self.table.add_row([""]) # Add a empty row for spacing after a testname 38 | if testsummary: 39 | print(self.table) 40 | except Exception as err: # pylint: disable=broad-except 41 | print("Unable to print prettytable failure summary") 42 | raise err 43 | -------------------------------------------------------------------------------- /core_helpers/remote_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper class for Remote Objects 3 | """ 4 | 5 | class Remote_Objects: 6 | def __init__(self): 7 | self.image_url_list = [] 8 | self.msg_list = [] 9 | self.testrail_flag = False 10 | self.tesults_flag = False 11 | self.test_run_id = None 12 | self.images = [] 13 | 14 | def register_testrail(self): 15 | "Register TestRail with Page" 16 | from integrations.reporting_tools.Test_Rail import Test_Rail # pylint: disable=import-error,import-outside-toplevel 17 | self.testrail_flag = True 18 | self.testrail_object = Test_Rail() 19 | self.write('Automation registered with TestRail',level='debug') 20 | 21 | def register_tesults(self): 22 | "Register Tesults with Page" 23 | self.tesults_flag = True 24 | from integrations.reporting_tools import Tesults # pylint: disable=import-error,import-outside-toplevel 25 | self.tesult_object = Tesults 26 | 27 | def add_tesults_case(self, name, desc, suite, result_flag, msg='', files=None, params=None, custom=None): 28 | "Update Tesults with test results" 29 | import os # pylint: disable=import-error,import-outside-toplevel 30 | if files is None: 31 | files = [] 32 | if params is None: 33 | params = {} 34 | if custom is None: 35 | custom = {} 36 | if self.tesults_flag is True: 37 | result = "unknown" 38 | failReason = "" 39 | if result_flag == True: 40 | result = "pass" 41 | if result_flag == False: 42 | result = "fail" 43 | failReason = msg 44 | for image in self.images: 45 | files.append(self.screenshot_dir + os.sep + image + '.png') 46 | self.images = [] 47 | caseObj = {'name': name, 'suite': suite, 'desc': desc, 'result': result, 'reason': failReason, 'files': files, 'params': params} 48 | for key, value in custom.items(): 49 | caseObj[key] = str(value) 50 | self.tesult_object.add_test_case(caseObj) 51 | 52 | def report_to_testrail(self,case_id,test_run_id,result_flag,msg=''): 53 | "Update Test Rail" 54 | if self.testrail_flag is True: 55 | msg += '\n'.join(self.msg_list) 56 | msg = msg + "\n" 57 | if self.session_url is not None: 58 | msg += '\n\n' + '[' + 'Watch Replay On Cloud: ' + ']('+ self.session_url +')' 59 | self.testrail_object.update_testrail(case_id,test_run_id,result_flag,msg=msg) 60 | self.msg_list = [] 61 | 62 | def set_test_run_id(self,test_run_id): 63 | "Set TestRail's test run id" 64 | self.test_run_id = test_run_id 65 | -------------------------------------------------------------------------------- /core_helpers/screenshot_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper class for Screenshot Objects 3 | """ 4 | import os 5 | import shutil 6 | from core_helpers import gif_maker 7 | import conf.screenshot_conf as conf 8 | 9 | class Screenshot_Objects: 10 | def __init__(self): 11 | self.tesults_flag = False 12 | self.images = [] 13 | 14 | def save_screenshot(self,screenshot_name,pre_format=" #Debug screenshot: "): 15 | "Take a screenshot" 16 | if os.path.exists(self.screenshot_dir + os.sep + screenshot_name+'.png'): 17 | for i in range(1,100): 18 | if os.path.exists(self.screenshot_dir + os.sep +screenshot_name+'_'+str(i)+'.png'): 19 | continue 20 | else: 21 | os.rename(self.screenshot_dir + os.sep +screenshot_name+'.png',self.screenshot_dir + os.sep +screenshot_name+'_'+str(i)+'.png') 22 | break 23 | screenshot_name = self.screenshot_dir + os.sep + screenshot_name+'.png' 24 | self.driver.get_screenshot_as_file(screenshot_name) 25 | if self.rp_logger: 26 | self.save_screenshot_reportportal(screenshot_name) 27 | if self.tesults_flag is True: 28 | self.images.append(screenshot_name) 29 | 30 | def save_screenshot_reportportal(self,image_name): 31 | "Method to save image to ReportPortal" 32 | try: 33 | with open(image_name, "rb") as fh: 34 | image = fh.read() 35 | screenshot_name = os.path.basename(image_name) 36 | self.rp_logger.info( 37 | screenshot_name, 38 | attachment={ 39 | "name": screenshot_name, 40 | "data": image, 41 | "mime": "image/png" 42 | }, 43 | ) 44 | except Exception as e: 45 | self.write("Exception when trying to get rplogger",'critical') 46 | self.write(str(e),'critical') 47 | self.exceptions.append("Error when trying to get reportportal logger") 48 | 49 | def make_gif(self): 50 | "Create a gif of all the screenshots within the screenshots directory" 51 | self.gif_file_name = gif_maker.make_gif(self.screenshot_dir,name=self.calling_module) 52 | 53 | return self.gif_file_name 54 | 55 | def set_directory_structure(self): 56 | "Setup the required directory structure if it is not already present" 57 | try: 58 | self.screenshots_parent_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)),'screenshots')) 59 | if not os.path.exists(self.screenshots_parent_dir): 60 | os.makedirs(self.screenshots_parent_dir) 61 | self.logs_parent_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)),'log')) 62 | if not os.path.exists(self.logs_parent_dir): 63 | os.makedirs(self.logs_parent_dir) 64 | except Exception as e: 65 | # Using print as log_obj wouldnt be created to write this exception to log file 66 | print("Exception when trying to set screenshot directory due to error:",str(e)) 67 | 68 | def get_test_name(self): 69 | "Returns the name of the test case by extracting it from the calling module." 70 | self.testname = self.get_calling_module() 71 | self.testname = self.testname.replace('<','') 72 | self.testname = self.testname.replace('>','') 73 | return self.testname 74 | 75 | def screenshot_directory(self, testname): 76 | "It checks for an existing screenshot directory, handles overwriting, and returns the path of the saved screenshot directory." 77 | overwrite_flag=conf.overwrite_flag 78 | self.screenshot_dir = self.screenshots_parent_dir + os.sep + testname 79 | if os.path.exists(self.screenshot_dir): 80 | if overwrite_flag is False: 81 | for i in range(1,4096): 82 | if os.path.exists(self.screenshot_dir + '_'+str(i)): 83 | continue 84 | else: 85 | os.rename(self.screenshot_dir,self.screenshot_dir +'_'+str(i)) 86 | break 87 | else: 88 | try: 89 | shutil.rmtree(self.screenshot_dir) 90 | except OSError as e: 91 | self.write("Error: %s - %s." % (e.filename, e.strerror),'critical') 92 | return self.screenshot_dir 93 | 94 | def create_screenshot_dir(self, screenshot_dir): 95 | "Create the screenshot directory if it doesn't exists already" 96 | try: 97 | if not os.path.exists(screenshot_dir): 98 | os.makedirs(screenshot_dir) 99 | return screenshot_dir 100 | except Exception as e: 101 | self.write("Exception when trying to set screenshot directory",'critical') 102 | self.write(str(e),'critical') 103 | self.exceptions.append("Error when setting up the screenshot directory") 104 | -------------------------------------------------------------------------------- /endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/endpoints/__init__.py -------------------------------------------------------------------------------- /endpoints/api_interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | A composed Interface for all the Endpoint abstraction objects: 3 | * Cars API Endpoints 4 | * Registration API Endpoints 5 | * User API Endpoints 6 | The APIPlayer Object interacts only to the Interface to access the Endpoint 7 | """ 8 | 9 | from .cars_api_endpoints import CarsAPIEndpoints 10 | from .registration_api_endpoints import RegistrationAPIEndpoints 11 | from .user_api_endpoints import UserAPIEndpoints 12 | 13 | class APIInterface(CarsAPIEndpoints, RegistrationAPIEndpoints, UserAPIEndpoints): 14 | "A composed interface for the API objects" 15 | 16 | def __init__(self, url): 17 | "Initialize the Interface" 18 | self.base_url = url 19 | -------------------------------------------------------------------------------- /endpoints/cars_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """ 2 | API methods for /cars endpoint 3 | """ 4 | from .base_api import BaseAPI 5 | 6 | class CarsAPIEndpoints(BaseAPI): 7 | "Class for cars endpoints" 8 | 9 | def cars_url(self,suffix=''): 10 | """Append API end point to base URL""" 11 | return self.base_url+'/cars'+suffix 12 | 13 | def add_car(self,data,headers): 14 | "Adds a new car" 15 | try: 16 | url = self.cars_url('/add') 17 | json_response = self.post(url,json=data,headers=headers) 18 | except Exception as err: # pylint: disable=broad-exception-caught 19 | print(f"Python says: {err}") 20 | json_response = None 21 | return { 22 | 'url':url, 23 | 'response':json_response.json() 24 | } 25 | 26 | def get_cars(self,headers): 27 | "gets list of cars" 28 | try: 29 | url = self.cars_url() 30 | json_response = self.get(url,headers=headers) 31 | except Exception as err: # pylint: disable=broad-exception-caught 32 | print(f"Python says: {err}") 33 | json_response = None 34 | return { 35 | 'url':url, 36 | 'response':json_response.json() 37 | } 38 | 39 | def get_car(self,url_params,headers): 40 | "gets given car details" 41 | try: 42 | url = self.cars_url('/find?')+url_params 43 | json_response = self.get(url,headers=headers) 44 | except Exception as err: # pylint: disable=broad-exception-caught 45 | print(f"Python says: {err}") 46 | json_response = None 47 | return { 48 | 'url':url, 49 | 'response':json_response.json() 50 | } 51 | 52 | def update_car(self,car_name,json,headers): 53 | "updates a given car" 54 | try: 55 | url = self.cars_url(f"/update/{car_name}") 56 | json_response =self.put(url,json=json,headers=headers) 57 | except Exception as err: # pylint: disable=broad-exception-caught 58 | print(f"Python says: {err}") 59 | json_response = None 60 | return { 61 | 'url':url, 62 | 'response':json_response.json() 63 | } 64 | 65 | def remove_car(self,car_name,headers): 66 | "deletes a car entry" 67 | try: 68 | url =self.cars_url(f"/remove/{car_name}") 69 | json_response = self.delete(url,headers=headers) 70 | except Exception as err: # pylint: disable=broad-exception-caught 71 | print(f"Python says: {err}") 72 | json_response = None 73 | return{ 74 | 'url':url, 75 | 'response':json_response.json() 76 | } 77 | 78 | # Async methods 79 | async def get_cars_async(self,headers): 80 | "Get the list of cars" 81 | url = self.cars_url() 82 | response = await self.async_get(url,headers=headers) 83 | return response 84 | 85 | async def add_car_async(self,data,headers): 86 | "Add a new car" 87 | url = self.cars_url("/add") 88 | response = await self.async_post(url,json=data,headers=headers) 89 | return response 90 | 91 | async def get_car_async(self,url_params,headers): 92 | "Get car using URL params" 93 | url = self.cars_url("/find?")+url_params 94 | response = await self.async_get(url,headers=headers) 95 | return response 96 | -------------------------------------------------------------------------------- /endpoints/registration_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """ 2 | API endpoint abstraction for /registration endpoint 3 | """ 4 | from .base_api import BaseAPI 5 | 6 | class RegistrationAPIEndpoints(BaseAPI): 7 | "Class for registration endpoints" 8 | 9 | def registration_url(self,suffix=''): 10 | """Append API end point to base URL""" 11 | return self.base_url+'/register/'+suffix 12 | 13 | def register_car(self,url_params,json,headers): 14 | "register car " 15 | url = self.registration_url('car?')+url_params 16 | json_response = self.post(url,params=url_params,json=json,headers=headers) 17 | return { 18 | 'url':url, 19 | 'response':json_response.json() 20 | } 21 | 22 | def get_registered_cars(self,headers): 23 | "gets registered cars" 24 | url = self.registration_url('') 25 | json_response = self.get(url,headers=headers) 26 | return { 27 | 'url':url, 28 | 'response':json_response.json() 29 | } 30 | 31 | def delete_registered_car(self,headers): 32 | "deletes registered car" 33 | url = self.registration_url('car/delete/') 34 | json_response = self.delete(url,headers) 35 | return { 36 | 'url':url, 37 | 'response':json_response.json() 38 | } 39 | 40 | # Async methods 41 | async def get_registered_cars_async(self,headers): 42 | "Get registered cars" 43 | url = self.registration_url('') 44 | response = await self.async_get(url,headers=headers) 45 | return response 46 | -------------------------------------------------------------------------------- /endpoints/user_api_endpoints.py: -------------------------------------------------------------------------------- 1 | """ 2 | API endpoints for Registration 3 | """ 4 | from .base_api import BaseAPI 5 | 6 | class UserAPIEndpoints(BaseAPI): 7 | "Class for user endpoints" 8 | def user_url(self,suffix=''): 9 | """Append API end point to base URL""" 10 | return self.base_url+'/users'+suffix 11 | def get_user_list(self,headers): 12 | "get users list" 13 | try: 14 | url = self.user_url('') 15 | json_response = self.get(url,headers=headers) 16 | except Exception as err: # pylint: disable=broad-exception-caught 17 | print(f"Python says: {err}") 18 | json_response = None 19 | return { 20 | 'url':url, 21 | 'response':json_response.status_code, 22 | 'user_list':json_response.json() 23 | } 24 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DISPLAY=:20 3 | Xvfb :20 -screen 0 1366x768x16 & 4 | 5 | # Start x11vnc 6 | x11vnc -passwd TestVNC -display :20 -N -forever & 7 | 8 | # Run CMD command 9 | exec "$@" -------------------------------------------------------------------------------- /env_conf: -------------------------------------------------------------------------------- 1 | # Tesults Configuration 2 | tesults_target_token_default = "" 3 | 4 | #TestRail url and credentials 5 | 6 | testrail_url = "Add your testrail url" 7 | testrail_user = 'TESTRAIL_USERNAME' 8 | testrail_password = 'TESTRAIL_PASSWORD' 9 | 10 | #Details needed for the Gmail 11 | #Fill out the email details over here 12 | imaphost ="imap.gmail.com" #Add imap hostname of your email client 13 | app_username ='USERNAME' 14 | 15 | #Login has to use the app password because of Gmail security configuration 16 | # 1. Setup 2 factor authentication 17 | # 2. Follow the 2 factor authentication setup wizard to enable an app password 18 | #Src: https://support.google.com/accounts/answer/185839?hl=en 19 | #Src: https://support.google.com/mail/answer/185833?hl=en 20 | app_password = 'APP_PASSWORD' 21 | 22 | #Details for sending pytest report 23 | smtp_ssl_host = 'smtp.gmail.com' # Add smtp ssl host of your email client 24 | smtp_ssl_port = 465 # Add smtp ssl port number of your email client 25 | sender = 'abc@xyz.com' #Add senders email address here 26 | targets = ['asd@xyz.com','qwe@xyz.com'] # Add recipients email address in a list 27 | 28 | #REPORT PORTAL 29 | report_portal_api_key = "Enter your report portal api key here" 30 | report_portal_endpoint = "Enter your endpoint here" 31 | report_portal_project = "Enter your Project here" 32 | report_portal_launch = "Enter your project launch here" 33 | 34 | #Slack channel incoming webhook 35 | #To generate incoming webhook url ref: https://qxf2.com/blog/post-pytest-test-results-on-slack/ 36 | slack_incoming_webhook_url = "Add Slack incomming webhook url here" 37 | # To generate pytest_report.log file add ">pytest_report.log" at end of py.test command 38 | # for e.g. pytest -k example_form --slack_flag y -v > log/pytest_report.log 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /env_remote: -------------------------------------------------------------------------------- 1 | #Set REMOTE_BROWSER_PLATFROM TO BS TO RUN ON BROWSERSTACK else 2 | #SET REMOTE_BROWSER_PLATFORM TO SL TO RUN ON SAUCELABS 3 | #SET REMOTE_BROWSER_PLATFORM TO LT TO RUN ON LAMBDATEST 4 | REMOTE_BROWSER_PLATFORM = "BS" 5 | 6 | REMOTE_USERNAME = "Enter your username" 7 | REMOTE_ACCESS_KEY = "Enter your access key" -------------------------------------------------------------------------------- /env_ssh_conf: -------------------------------------------------------------------------------- 1 | #Server credential details needed for ssh 2 | 3 | HOST = 'Enter your host details here' 4 | USERNAME = 'USERNAME' 5 | PASSWORD = 'PASSWORD' 6 | PORT = 22 7 | TIMEOUT = 10 8 | 9 | #.pem file details 10 | PKEY = 'Enter your key filename here' 11 | 12 | #Sample commands to execute(Add your commands here) 13 | COMMANDS = ['ls;mkdir sample'] 14 | 15 | #Sample file locations to upload and download 16 | UPLOADREMOTEFILEPATH = '/etc/example/filename.txt' 17 | UPLOADLOCALFILEPATH = 'home/filename.txt' 18 | DOWNLOADREMOTEFILEPATH = '/etc/sample/data.txt' 19 | DOWNLOADLOCALFILEPATH = 'home/data.txt' 20 | -------------------------------------------------------------------------------- /integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/integrations/__init__.py -------------------------------------------------------------------------------- /integrations/cross_browsers/BrowserStack_Library.py: -------------------------------------------------------------------------------- 1 | """ 2 | First version of a library to interact with BrowserStack's artifacts. 3 | 4 | For now, this is useful for: 5 | a) Obtaining the session URL 6 | b) Obtaining URLs of screenshots 7 | 8 | To do: 9 | a) Handle expired sessions better 10 | """ 11 | import os 12 | import requests 13 | from conf import remote_url_conf 14 | 15 | class BrowserStack_Library(): 16 | "BrowserStack library to interact with BrowserStack artifacts" 17 | def __init__(self): 18 | "Constructor for the BrowserStack library" 19 | self.browserstack_api_server_url = remote_url_conf.browserstack_api_server_url 20 | self.browserstack_cloud_api_server_url = remote_url_conf.browserstack_cloud_api_server_url 21 | self.auth = self.get_auth() 22 | 23 | 24 | def get_auth(self): 25 | "Set up the auth object for the Requests library" 26 | USERNAME = os.getenv('REMOTE_USERNAME') 27 | PASSWORD = os.getenv('REMOTE_ACCESS_KEY') 28 | auth = (USERNAME,PASSWORD) 29 | 30 | return auth 31 | 32 | 33 | def get_build_id(self,timeout=10): 34 | "Get the build ID" 35 | build_url = self.browserstack_api_server_url + "/builds.json?status=running" 36 | builds = requests.get(build_url, auth=self.auth, timeout=timeout).json() 37 | build_id = builds[0]['automation_build']['hashed_id'] 38 | 39 | return build_id 40 | 41 | 42 | def get_sessions(self,timeout=10): 43 | "Get a JSON object with all the sessions" 44 | build_id = self.get_build_id() 45 | sessions= requests.get(f'{self.browserstack_api_server_url}/builds/{build_id}/sessions.json?browserstack_status=running', auth=self.auth, timeout=timeout).json() 46 | 47 | return sessions 48 | 49 | 50 | def get_active_session_details(self): 51 | "Return the session ID of the first active session" 52 | session_details = None 53 | sessions = self.get_sessions() 54 | for session in sessions: 55 | #Get session id of the first session with status = running 56 | if session['automation_session']['status']=='running': 57 | session_details = session['automation_session'] 58 | #session_id = session['automation_session']['hashed_id'] 59 | #session_url = session['automation_session']['browser_url'] 60 | break 61 | 62 | return session_details 63 | 64 | def extract_session_id(self, session_url): 65 | "Extract session id from session url" 66 | import re 67 | # Use regex to match the session ID, which is a 40-character hexadecimal string 68 | match = re.search(r'/sessions/([a-f0-9]{40})', session_url) 69 | if match: 70 | return match.group(1) 71 | else: 72 | return None 73 | 74 | def upload_terminal_logs(self, file_path, session_id = None, appium_test = False, timeout=30): 75 | "Upload the terminal log to BrowserStack" 76 | try: 77 | # Get session ID if not provided 78 | if session_id is None: 79 | session_details = self.get_active_session_details() 80 | session_id = session_details['hashed_id'] 81 | if not session_id: 82 | raise ValueError("Session ID could not be retrieved. Check active session details.") 83 | 84 | # Determine the URL based on the type of test 85 | if appium_test: 86 | url = f'{self.browserstack_cloud_api_server_url}/app-automate/sessions/{session_id}/terminallogs' 87 | else: 88 | url = f'{self.browserstack_cloud_api_server_url}/automate/sessions/{session_id}/terminallogs' 89 | 90 | # Open the file using a context manager to ensure it is properly closed 91 | with open(file_path, 'rb') as file: 92 | files = {'file': file} 93 | # Make the POST request to upload the file 94 | response = requests.post(url, auth=self.auth, files=files, timeout=timeout) 95 | 96 | # Check if the request was successful 97 | if response.status_code == 200: 98 | print("Log file uploaded to BrowserStack session successfully.") 99 | else: 100 | print(f"Failed to upload log file. Status code: {response.status_code}") 101 | print(response.text) 102 | 103 | return response 104 | 105 | except FileNotFoundError as e: 106 | print(f"Error: Log file '{file_path}' not found.") 107 | return {"error": "Log file not found.", "details": str(e)} 108 | 109 | except ValueError as e: 110 | print(f"Error: {str(e)}") 111 | return {"error": "Invalid session ID.", "details": str(e)} 112 | 113 | except requests.exceptions.RequestException as e: 114 | # Handle network-related errors 115 | print(f"Error: Failed to upload log file to BrowserStack. Network error: {str(e)}") 116 | return {"error": "Network error during file upload.", "details": str(e)} 117 | 118 | except Exception as e: 119 | # Catch any other unexpected errors 120 | print(f"An unexpected error occurred: {str(e)}") 121 | return {"error": "Unexpected error occurred during file upload.", "details": str(e)} 122 | -------------------------------------------------------------------------------- /integrations/cross_browsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/integrations/cross_browsers/__init__.py -------------------------------------------------------------------------------- /integrations/cross_browsers/lambdatest_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the webdriver for LambdaTest browsers. 3 | """ 4 | import os 5 | import time 6 | import requests 7 | from selenium import webdriver 8 | from integrations.cross_browsers.remote_options import RemoteOptions 9 | from conf import remote_url_conf 10 | 11 | class LambdaTestRunner(RemoteOptions): 12 | """Configure and get the webdriver for the LambdaTest""" 13 | def __init__(self): 14 | self.username = os.getenv('REMOTE_USERNAME') 15 | self.password = os.getenv('REMOTE_ACCESS_KEY') 16 | self.lambdatest_url = remote_url_conf.lambdatest_url.format(self.username, self.password) 17 | self.lambdatest_api_server_url = remote_url_conf.lambdatest_api_server_url 18 | self.session_id = None 19 | self.session_url = None 20 | 21 | def lambdatest_credentials(self, lambdatest_options): 22 | """Set LambdaTest credentials.""" 23 | lambdatest_options['user'] = self.username 24 | lambdatest_options['accessKey'] = self.password 25 | return lambdatest_options 26 | 27 | def set_lambdatest_capabilities(self,remote_project_name, remote_build_name, testname): 28 | """Set LambdaTest Capabilities""" 29 | lambdatest_options = {} 30 | lambdatest_options = self.lambdatest_credentials(lambdatest_options) 31 | lambdatest_options["build"] = remote_build_name 32 | lambdatest_options["project"] = remote_project_name 33 | lambdatest_options["name"] = testname 34 | lambdatest_options["video"] = True 35 | lambdatest_options["visual"] = True 36 | lambdatest_options["network"] = True 37 | lambdatest_options["w3c"] = True 38 | lambdatest_options["console"] = True 39 | lambdatest_options["plugin"] = "python-pytest" 40 | 41 | return lambdatest_options 42 | 43 | def get_lambdatest_webdriver(self, os_name, os_version, browser, browser_version, 44 | remote_project_name, remote_build_name, testname): 45 | """Run the test in LambdaTest when remote flag is 'Y'.""" 46 | options = self.get_browser(browser, browser_version) 47 | 48 | if options is None: 49 | raise ValueError(f"Unsupported browser: {browser}") 50 | 51 | # Set LambdaTest platform 52 | options.platformName = f"{os_name} {os_version}" 53 | 54 | lambdatest_options = self.set_lambdatest_capabilities(remote_project_name,remote_build_name, 55 | testname) 56 | 57 | options.set_capability('LT:options', lambdatest_options) 58 | web_driver = webdriver.Remote(command_executor=self.lambdatest_url, options=options) 59 | 60 | # Get the session ID and session URL and print it 61 | self.session_id = web_driver.session_id 62 | self.session_url = self.get_session_url_with_retries(self.session_id) 63 | 64 | return web_driver,self.session_url 65 | 66 | def get_session_url_with_retries(self, session_id, retries=5, delay=2, timeout=30): 67 | """Fetch the session URL using the LambdaTest API with retries.""" 68 | api_url = f"{self.lambdatest_api_server_url}/sessions/{session_id}" 69 | time.sleep(2) 70 | for _ in range(retries): 71 | response = requests.get(api_url, auth=(self.username, self.password),timeout=timeout) 72 | if response.status_code == 200: 73 | session_data = response.json() 74 | test_id = session_data['data']['test_id'] 75 | session_url = f"https://automation.lambdatest.com/test?testID={test_id}" 76 | 77 | return session_url 78 | 79 | else: 80 | print(f"Retrying... Status code: {response.status_code}, Response: {response.text}") 81 | time.sleep(delay) 82 | raise Exception(f"Failed to fetch session details after {retries} retries.") 83 | -------------------------------------------------------------------------------- /integrations/cross_browsers/remote_options.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set the desired option for running the test on a remote platform. 3 | """ 4 | from selenium.webdriver.firefox.options import Options as FirefoxOptions 5 | from selenium.webdriver.edge.options import Options as EdgeOptions 6 | from selenium.webdriver.chrome.options import Options as ChromeOptions 7 | from selenium.webdriver.safari.options import Options as SafariOptions 8 | from appium.options.android import UiAutomator2Options 9 | from appium import webdriver as mobile_webdriver 10 | 11 | class RemoteOptions(): 12 | """Class contains methods for various remote options for browserstack and saucelab.""" 13 | 14 | @staticmethod 15 | def firefox(browser_version): 16 | """Set web browser as firefox.""" 17 | options = FirefoxOptions() 18 | options.browser_version = browser_version 19 | 20 | return options 21 | 22 | @staticmethod 23 | def edge(browser_version): 24 | """Set web browser as Edge.""" 25 | options = EdgeOptions() 26 | options.browser_version = browser_version 27 | 28 | return options 29 | 30 | @staticmethod 31 | def chrome(browser_version): 32 | """Set web browser as Chrome.""" 33 | options = ChromeOptions() 34 | options.browser_version = browser_version 35 | 36 | return options 37 | 38 | @staticmethod 39 | def safari(browser_version): 40 | """Set web browser as Safari.""" 41 | options = SafariOptions() 42 | options.browser_version = browser_version 43 | 44 | return options 45 | 46 | def get_browser(self, browser, browser_version): 47 | """Select the browser.""" 48 | if browser.lower() == 'ff' or browser.lower() == 'firefox': 49 | desired_capabilities = self.firefox(browser_version) 50 | elif browser.lower() == 'edge': 51 | desired_capabilities = self.edge(browser_version) 52 | elif browser.lower() == 'chrome': 53 | desired_capabilities = self.chrome(browser_version) 54 | elif browser.lower() == 'safari': 55 | desired_capabilities = self.safari(browser_version) 56 | else: 57 | print(f"\nDriverFactory does not know the browser\t{browser}\n") 58 | desired_capabilities = None 59 | 60 | return desired_capabilities 61 | 62 | def remote_project_name(self, desired_capabilities, remote_project_name): 63 | """Set remote project name for browserstack.""" 64 | desired_capabilities['projectName'] = remote_project_name 65 | 66 | return desired_capabilities 67 | 68 | def remote_build_name(self, desired_capabilities, remote_build_name): 69 | """Set remote build name for browserstack.""" 70 | from datetime import datetime 71 | desired_capabilities['buildName'] = remote_build_name+"_"+str(datetime.now().strftime("%c")) 72 | 73 | return desired_capabilities 74 | 75 | def set_capabilities_options(self, desired_capabilities, url): 76 | """Set the capabilities options for the mobile driver.""" 77 | capabilities_options = UiAutomator2Options().load_capabilities(desired_capabilities) 78 | mobile_driver = mobile_webdriver.Remote(command_executor=url,options=capabilities_options) 79 | 80 | return mobile_driver 81 | -------------------------------------------------------------------------------- /integrations/cross_browsers/saucelab_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get the webdriver and mobiledriver for SauceLab. 3 | """ 4 | import os 5 | from selenium import webdriver 6 | from integrations.cross_browsers.remote_options import RemoteOptions 7 | from conf import remote_url_conf 8 | 9 | class SauceLabRunner(RemoteOptions): 10 | """Configure and get the webdriver and the mobiledriver for SauceLab""" 11 | def __init__(self): 12 | self.username = os.getenv('REMOTE_USERNAME') 13 | self.password = os.getenv('REMOTE_ACCESS_KEY') 14 | self.saucelabs_url = remote_url_conf.saucelabs_url 15 | self.saucelabs_app_upload_url = remote_url_conf.saucelabs_app_upload_url 16 | 17 | def saucelab_credentials(self, sauce_options): 18 | """Set saucelab credentials.""" 19 | sauce_options['username'] = self.username 20 | sauce_options['accessKey'] = self.password 21 | 22 | return sauce_options 23 | 24 | def saucelab_capabilities(self, desired_capabilities, app_name): 25 | """Set saucelab capabilities""" 26 | desired_capabilities['appium:app'] = 'storage:filename='+app_name 27 | desired_capabilities['autoAcceptAlerts'] = 'true' 28 | sauce_mobile_options = {} 29 | sauce_mobile_options = self.saucelab_credentials(sauce_mobile_options) 30 | desired_capabilities['sauce:options'] = sauce_mobile_options 31 | 32 | return desired_capabilities 33 | 34 | def saucelab_platform(self, options, os_name, os_version): 35 | """Set platform for saucelab.""" 36 | options.platform_name = os_name + ' '+os_version 37 | 38 | return options 39 | 40 | def sauce_upload(self,app_path, app_name, timeout=30): 41 | """Upload the apk to the sauce temperory storage.""" 42 | import requests 43 | from requests.auth import HTTPBasicAuth 44 | 45 | result_flag = False 46 | try: 47 | apk_file_path = os.path.join(app_path, app_name) 48 | 49 | # Open the APK file in binary mode 50 | with open(apk_file_path, 'rb') as apk_file: 51 | files = {'payload': (app_name, apk_file, 'application/vnd.android.package-archive')} 52 | params = {'name': app_name, 'overwrite': 'true'} 53 | # Perform the upload request 54 | response = requests.post(self.saucelabs_app_upload_url, 55 | auth=HTTPBasicAuth(self.username, self.password), 56 | files=files, params=params, timeout=timeout) 57 | # Check the response 58 | if response.status_code == 201: 59 | result_flag = True 60 | print('App successfully uploaded to sauce storage') 61 | #print('Response:', response.json()) 62 | else: 63 | print('App upload failed!') 64 | print('Status code:', response.status_code) 65 | print('Response:', response.text) 66 | raise Exception("Failed to upload APK file." + 67 | f"Status code: {response.status_code}") 68 | 69 | except Exception as exception: 70 | print(str(exception)) 71 | 72 | return result_flag 73 | 74 | def get_saucelab_mobile_driver(self, app_path, app_name, desired_capabilities): 75 | """Setup mobile driver to run the test in Saucelab.""" 76 | #Saucelabs expects the app to be uploaded to Sauce storage everytime the test is run 77 | result_flag = self.sauce_upload(app_path, app_name) 78 | 79 | if result_flag: 80 | desired_capabilities = self.saucelab_capabilities(desired_capabilities, app_name) 81 | mobile_driver = self.set_capabilities_options(desired_capabilities, 82 | url=self.saucelabs_url) 83 | else: 84 | print("Failed to upload an app file") 85 | raise Exception("Failed to upload APK file.") 86 | 87 | return mobile_driver 88 | 89 | def get_saucelab_webdriver(self, os_name, os_version, browser, browser_version): 90 | """Setup webdriver to run the test in Saucelab.""" 91 | #set browser 92 | options = self.get_browser(browser, browser_version) 93 | 94 | #set saucelab platform 95 | options = self.saucelab_platform(options, os_name, os_version) 96 | sauce_options = {} 97 | sauce_options = self.saucelab_credentials(sauce_options) 98 | options.set_capability('sauce:options', sauce_options) 99 | web_driver = webdriver.Remote(command_executor=self.saucelabs_url, options=options) 100 | 101 | return web_driver 102 | -------------------------------------------------------------------------------- /integrations/reporting_channels/gmail/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | 5 | GMail! Woo! 6 | 7 | """ 8 | 9 | __title__ = 'gmail' 10 | __version__ = '0.1' 11 | __author__ = 'Charlie Guo' 12 | __build__ = 0x0001 13 | __license__ = 'Apache 2.0' 14 | __copyright__ = 'Copyright 2013 Charlie Guo' 15 | 16 | from .gmail import Gmail 17 | from .mailbox import Mailbox 18 | from .message import Message 19 | from .exceptions import GmailException, ConnectionError, AuthenticationError 20 | from .utils import login, authenticate 21 | 22 | -------------------------------------------------------------------------------- /integrations/reporting_channels/gmail/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | gmail.exceptions 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module contains the set of Gmails' exceptions. 8 | 9 | """ 10 | 11 | 12 | class GmailException(RuntimeError): 13 | """There was an ambiguous exception that occurred while handling your 14 | request.""" 15 | 16 | class ConnectionError(GmailException): 17 | """A Connection error occurred.""" 18 | 19 | class AuthenticationError(GmailException): 20 | """Gmail Authentication failed.""" 21 | 22 | class Timeout(GmailException): 23 | """The request timed out.""" 24 | -------------------------------------------------------------------------------- /integrations/reporting_channels/gmail/gmailtest.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example automated test to check gmail utils 3 | Our automated test will do the following: 4 | #login to gmail and fetch mailboxes 5 | #After fetching the mail box ,select and fetch messages and print the number of messages 6 | #and the subject of the messages 7 | 8 | Prerequisites: 9 | - Gmail account with app password 10 | """ 11 | import sys 12 | import os 13 | import io 14 | from dotenv import load_dotenv 15 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))) 16 | from integrations.reporting_channels.gmail.gmail import Gmail, AuthenticationError 17 | from integrations.reporting_channels.gmail.mailbox import Mailbox 18 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') 19 | 20 | load_dotenv() 21 | 22 | def gmail_test(): 23 | "Run the Gmail utility test" 24 | try: 25 | # 1. Initialize Gmail object and connect 26 | gmail = Gmail() 27 | gmail.connect() 28 | print("Connected to Gmail") 29 | 30 | # 2. Login to Gmail 31 | username = os.getenv('app_username') 32 | password = os.getenv('app_password') 33 | 34 | try: 35 | gmail.login(username, password) 36 | print("Login successful") 37 | except AuthenticationError as e: 38 | print(f"Authentication failed: Check for the login credentials {str(e)}") 39 | return 40 | 41 | # 3. Fetch mailboxes 42 | mailboxes = gmail.fetch_mailboxes() 43 | if mailboxes: 44 | print(f"Fetched mailboxes: {mailboxes}") 45 | else: 46 | raise ValueError("Failed to fetch mailboxes!") 47 | 48 | # 4. Select and fetch messages from SPAM mailbox 49 | inbox_mailbox = gmail.use_mailbox("[Gmail]/Spam") 50 | if isinstance(inbox_mailbox, Mailbox): 51 | print("SPAM mailbox selected successfully") 52 | else: 53 | raise TypeError(f"Error: Expected Mailbox instance, got {type(inbox_mailbox)}") 54 | 55 | # 5. Fetch and print messages from SPAM 56 | messages = inbox_mailbox.mail() 57 | print(f"Number of messages in SPAM: {len(messages)}") 58 | 59 | # 6. Fetch and print message subjects 60 | if messages: 61 | msg = messages[0] 62 | fetched_msg = msg.fetch() 63 | print(f"Fetching Message subject: {fetched_msg.get('subject')}") 64 | 65 | # Fetch multiple messages and log subjects 66 | messages_dict = {msg.uid.decode('utf-8'): msg for msg in messages} 67 | fetched_messages = gmail.fetch_multiple_messages(messages_dict) 68 | for uid, message in fetched_messages.items(): 69 | subject = getattr(message, 'subject', 'No subject') 70 | print(f"UID: {uid}, Subject: {subject.encode('utf-8', errors='replace').decode('utf-8')}") 71 | 72 | else: 73 | print("No messages found in SPAM") 74 | 75 | # 7. Logout 76 | gmail.logout() 77 | print("Logged out successfully") 78 | 79 | except Exception as e: 80 | print(f"Exception encountered: {str(e)}") 81 | raise 82 | 83 | if __name__ == "__main__": 84 | gmail_test() 85 | -------------------------------------------------------------------------------- /integrations/reporting_channels/gmail/utf.py: -------------------------------------------------------------------------------- 1 | """ 2 | # The contents of this file has been derived code from the Twisted project 3 | # (http://twistedmatrix.com/). The original author is Jp Calderone. 4 | 5 | # Twisted project license follows: 6 | 7 | # Permission is hereby granted, free of charge, to any person obtaining 8 | # a copy of this software and associated documentation files (the 9 | # "Software"), to deal in the Software without restriction, including 10 | # without limitation the rights to use, copy, modify, merge, publish, 11 | # distribute, sublicense, and/or sell copies of the Software, and to 12 | # permit persons to whom the Software is furnished to do so, subject to 13 | # the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | """ 26 | TEXT_TYPE = str 27 | BINARY_TYPE = bytes 28 | 29 | PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7f)) 30 | 31 | def encode(s): 32 | """Encode a folder name using IMAP modified UTF-7 encoding. 33 | 34 | Despite the function's name, the output is still a unicode string. 35 | """ 36 | if not isinstance(s, TEXT_TYPE): 37 | return s 38 | 39 | r = [] 40 | _in = [] 41 | 42 | def extend_result_if_chars_buffered(): 43 | if _in: 44 | r.extend(['&', modified_utf7(''.join(_in)), '-']) 45 | del _in[:] 46 | 47 | for c in s: 48 | if ord(c) in PRINTABLE: 49 | extend_result_if_chars_buffered() 50 | r.append(c) 51 | elif c == '&': 52 | extend_result_if_chars_buffered() 53 | r.append('&-') 54 | else: 55 | _in.append(c) 56 | 57 | extend_result_if_chars_buffered() 58 | 59 | return ''.join(r) 60 | 61 | def decode(s): 62 | """Decode a folder name from IMAP modified UTF-7 encoding to unicode. 63 | 64 | Despite the function's name, the input may still be a unicode 65 | string. If the input is bytes, it's first decoded to unicode. 66 | """ 67 | if isinstance(s, BINARY_TYPE): 68 | s = s.decode('latin-1') 69 | if not isinstance(s, TEXT_TYPE): 70 | return s 71 | 72 | r = [] 73 | _in = [] 74 | for c in s: 75 | if c == '&' and not _in: 76 | _in.append('&') 77 | elif c == '-' and _in: 78 | if len(_in) == 1: 79 | r.append('&') 80 | else: 81 | r.append(modified_deutf7(''.join(_in[1:]))) 82 | _in = [] 83 | elif _in: 84 | _in.append(c) 85 | else: 86 | r.append(c) 87 | if _in: 88 | r.append(modified_deutf7(''.join(_in[1:]))) 89 | 90 | return ''.join(r) 91 | 92 | def modified_utf7(s): 93 | "Convert a string to modified UTF-7 encoding." 94 | # encode to utf-7: '\xff' => b'+AP8-', decode from latin-1 => '+AP8-' 95 | s_utf7 = s.encode('utf-7').decode('latin-1') 96 | return s_utf7[1:-1].replace('/', ',') 97 | 98 | def modified_deutf7(s): 99 | "Convert a modified UTF-7 encoded string back to UTF-8." 100 | s_utf7 = '+' + s.replace(',', '/') + '-' 101 | # encode to latin-1: '+AP8-' => b'+AP8-', decode from utf-7 => '\xff' 102 | return s_utf7.encode('latin-1').decode('utf-7') 103 | -------------------------------------------------------------------------------- /integrations/reporting_channels/gmail/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .gmail import Gmail 4 | 5 | def login(username, password): 6 | gmail = Gmail() 7 | gmail.login(username, password) 8 | return gmail 9 | 10 | def authenticate(username, access_token): 11 | gmail = Gmail() 12 | gmail.authenticate(username, access_token) 13 | return gmail -------------------------------------------------------------------------------- /integrations/reporting_channels/post_test_reports_to_slack.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A Simple script which used to post test reports on Slack Channel. 3 | 4 | Steps to Use: 5 | 1. Generate Slack incoming webhook url by reffering our blog: 6 | https://qxf2.com/blog/post-pytest-test-results-on-slack/ & add url in our code 7 | 2. Generate test report log file by adding ">log/pytest_report.log" command 8 | at end of pytest command 9 | for e.g. pytest -k example_form --slack_flag y -v > log/pytest_report.log 10 | Note: Your terminal must be pointed to root address of our POM while generating 11 | test report file using above command 12 | 3. Check you are calling correct report log file or not 13 | ''' 14 | import os 15 | import json 16 | import requests 17 | 18 | def post_reports_to_slack(timeout=30): 19 | "Post report to Slack" 20 | #To generate incoming webhook url ref: https://qxf2.com/blog/post-pytest-test-results-on-slack/ 21 | url= os.getenv('slack_incoming_webhook_url') 22 | 23 | # To generate pytest_report.log file add "> log/pytest_report.log" at end of pytest command 24 | # for e.g. pytest -k example_form --slack_flag y -v > log/pytest_report.log 25 | # Update report file name & address here as per your requirement 26 | test_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 27 | '..','..','log','pytest_report.log')) 28 | 29 | with open(test_report_file, "r") as in_file: 30 | testdata = "" 31 | for line in in_file: 32 | testdata = testdata + '\n' + line 33 | 34 | # Set Slack Pass Fail bar indicator color according to test results 35 | if 'FAILED' in testdata: 36 | bar_color = "#ff0000" 37 | else: 38 | bar_color = "#36a64f" 39 | 40 | data = {"attachments":[ 41 | {"color": bar_color, 42 | "title": "Test Report", 43 | "text": testdata} 44 | ]} 45 | json_params_encoded = json.dumps(data) 46 | slack_response = requests.post(url=url,data=json_params_encoded, 47 | headers={"Content-type":"application/json"}, 48 | timeout=timeout) 49 | if slack_response.text == 'ok': 50 | print('\n Successfully posted pytest report on Slack channel') 51 | else: 52 | print('\n Something went wrong. Unable to post pytest report on Slack channel.' 53 | 'Slack Response:', slack_response) 54 | 55 | #---USAGE EXAMPLES 56 | if __name__=='__main__': 57 | post_reports_to_slack() 58 | -------------------------------------------------------------------------------- /integrations/reporting_tools/Tesults.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tesults 3 | 4 | cases = [] 5 | 6 | def add_test_case(data): 7 | cases.append(data) 8 | 9 | def post_results_to_tesults (): 10 | " This method is to post the results into the tesults" 11 | # uses default token unless otherwise specified 12 | token = os.getenv('tesults_target_token_default') 13 | 14 | if not token: 15 | solution =("It looks like you are trying to use tesults to run your test." 16 | "Please make sure you have updated .env with the right credentials .") 17 | print(f"\033[92m\nSOLUTION: {solution}\n\033[0m") 18 | else: 19 | data = { 20 | 'target': token, 21 | 'results': { 'cases': cases } 22 | } 23 | print ('-----Tesults output-----') 24 | if len(data['results']['cases']) > 0: 25 | print (data) 26 | print('Uploading results to Tesults...') 27 | ret = tesults.results(data) 28 | print ('success: ' + str(ret['success'])) 29 | print ('message: ' + str(ret['message'])) 30 | print ('warnings: ' + str(ret['warnings'])) 31 | print ('errors: ' + str(ret['errors'])) 32 | else: 33 | print ('No test results.') 34 | -------------------------------------------------------------------------------- /integrations/reporting_tools/testrail_client.py: -------------------------------------------------------------------------------- 1 | # 2 | # TestRail API binding for Python 3.x (API v2, available since 3 | # TestRail 3.0) 4 | # Compatible with TestRail 3.0 and later. 5 | # 6 | # Learn more: 7 | # 8 | # http://docs.gurock.com/testrail-api2/start 9 | # http://docs.gurock.com/testrail-api2/accessing 10 | # 11 | # Copyright Gurock Software GmbH. See license.md for details. 12 | # 13 | 14 | import requests 15 | import json 16 | import base64 17 | 18 | 19 | class APIClient: 20 | def __init__(self, base_url): 21 | self.user = '' 22 | self.password = '' 23 | if not base_url.endswith('/'): 24 | base_url += '/' 25 | self.__url = base_url + 'index.php?/api/v2/' 26 | 27 | # 28 | # Send Get 29 | # 30 | # Issues a GET request (read) against the API and returns the result 31 | # (as Python dict) or filepath if successful file download 32 | # 33 | # Arguments: 34 | # 35 | # uri The API method to call including parameters 36 | # (e.g. get_case/1) 37 | # 38 | # filepath The path and file name for attachment download 39 | # Used only for 'get_attachment/:attachment_id' 40 | # 41 | def send_get(self, uri, filepath=None): 42 | return self.__send_request('GET', uri, filepath) 43 | 44 | # 45 | # Send POST 46 | # 47 | # Issues a POST request (write) against the API and returns the result 48 | # (as Python dict). 49 | # 50 | # Arguments: 51 | # 52 | # uri The API method to call including parameters 53 | # (e.g. add_case/1) 54 | # data The data to submit as part of the request (as 55 | # Python dict, strings must be UTF-8 encoded) 56 | # If adding an attachment, must be the path 57 | # to the file 58 | # 59 | def send_post(self, uri, data): 60 | return self.__send_request('POST', uri, data) 61 | 62 | def __send_request(self, method, uri, data): 63 | url = self.__url + uri 64 | 65 | auth = str( 66 | base64.b64encode( 67 | bytes('%s:%s' % (self.user, self.password), 'utf-8') 68 | ), 69 | 'ascii' 70 | ).strip() 71 | headers = {'Authorization': 'Basic ' + auth} 72 | 73 | if method == 'POST': 74 | if uri[:14] == 'add_attachment': # add_attachment API method 75 | files = {'attachment': (open(data, 'rb'))} 76 | response = requests.post(url, headers=headers, files=files) 77 | files['attachment'].close() 78 | else: 79 | headers['Content-Type'] = 'application/json' 80 | payload = bytes(json.dumps(data), 'utf-8') 81 | response = requests.post(url, headers=headers, data=payload) 82 | else: 83 | headers['Content-Type'] = 'application/json' 84 | response = requests.get(url, headers=headers) 85 | 86 | if response.status_code > 201: 87 | try: 88 | error = response.json() 89 | except: # response.content not formatted as JSON 90 | error = str(response.content) 91 | raise APIError('TestRail API returned HTTP %s (%s)' % (response.status_code, error)) 92 | else: 93 | if uri[:15] == 'get_attachment/': # Expecting file, not JSON 94 | try: 95 | open(data, 'wb').write(response.content) 96 | return (data) 97 | except: 98 | return ("Error saving attachment.") 99 | else: 100 | return response.json() 101 | 102 | 103 | class APIError(Exception): 104 | pass 105 | -------------------------------------------------------------------------------- /page_objects/PageFactory.py: -------------------------------------------------------------------------------- 1 | """ 2 | PageFactory uses the factory design pattern. 3 | get_page_object() returns the appropriate page object. 4 | Add elif clauses as and when you implement new pages. 5 | Pages implemented so far: 6 | 1. Tutorial main page 7 | 2. Tutorial redirect page 8 | 3. Contact Page 9 | 4. Bitcoin main page 10 | 5. Bitcoin price page 11 | """ 12 | # pylint: disable=import-outside-toplevel, E0401 13 | from conf import base_url_conf as url_conf 14 | 15 | class PageFactory(): 16 | "PageFactory uses the factory design pattern." 17 | @staticmethod 18 | def get_page_object(page_name,base_url=url_conf.ui_base_url): 19 | "Return the appropriate page object based on page_name" 20 | test_obj = None 21 | page_name = page_name.lower() 22 | if page_name in ["zero","zero page","agent zero"]: 23 | from page_objects.zero_page import Zero_Page 24 | test_obj = Zero_Page(base_url=base_url) 25 | elif page_name in ["zero mobile","zero mobile page"]: 26 | from page_objects.zero_mobile_page import Zero_Mobile_Page 27 | test_obj = Zero_Mobile_Page() 28 | elif page_name in ["main","main page"]: 29 | from page_objects.examples.selenium_tutorial_webpage.tutorial_main_page import Tutorial_Main_Page 30 | test_obj = Tutorial_Main_Page(base_url=base_url) 31 | elif page_name == "redirect": 32 | from page_objects.examples.selenium_tutorial_webpage.tutorial_redirect_page import Tutorial_Redirect_Page 33 | test_obj = Tutorial_Redirect_Page(base_url=base_url) 34 | elif page_name in ["contact","contact page"]: 35 | from page_objects.examples.selenium_tutorial_webpage.contact_page import Contact_Page 36 | test_obj = Contact_Page(base_url=base_url) 37 | elif page_name == "bitcoin main page": 38 | from page_objects.examples.bitcoin_mobile_app.bitcoin_main_page import Bitcoin_Main_Page 39 | test_obj = Bitcoin_Main_Page() 40 | elif page_name == "bitcoin price page": 41 | from page_objects.examples.bitcoin_mobile_app.bitcoin_price_page import Bitcoin_Price_Page 42 | test_obj = Bitcoin_Price_Page() 43 | #"New pages added needs to be updated in the get_all_page_names method too" 44 | elif page_name == "weathershopper home page": 45 | from page_objects.examples.weather_shopper_mobile_app.weather_shopper_home_page import WeatherShopperHomePage 46 | test_obj = WeatherShopperHomePage() 47 | elif page_name == "weathershopper products page": 48 | from page_objects.examples.weather_shopper_mobile_app.weather_shopper_product_page import WeatherShopperProductPage 49 | test_obj = WeatherShopperProductPage() 50 | elif page_name == "weathershopper payment page": 51 | from page_objects.examples.weather_shopper_mobile_app.weather_shopper_payment_page import WeatherShopperPaymentPage 52 | test_obj = WeatherShopperPaymentPage() 53 | elif page_name == "weathershopper cart page": 54 | from page_objects.examples.weather_shopper_mobile_app.weather_shopper_cart_page import WeatherShopperCartPage 55 | test_obj = WeatherShopperCartPage() 56 | elif page_name == "webview": 57 | from page_objects.examples.weather_shopper_mobile_app.webview_chrome import WebviewChrome 58 | test_obj = WebviewChrome() 59 | return test_obj 60 | -------------------------------------------------------------------------------- /page_objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/page_objects/__init__.py -------------------------------------------------------------------------------- /page_objects/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/page_objects/examples/__init__.py -------------------------------------------------------------------------------- /page_objects/examples/bitcoin_mobile_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/page_objects/examples/bitcoin_mobile_app/__init__.py -------------------------------------------------------------------------------- /page_objects/examples/bitcoin_mobile_app/bitcoin_main_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | Page object for Bitcoin main Page. 3 | """ 4 | import os 5 | import sys 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | import conf.locators_conf as locators 8 | from utils.Wrapit import Wrapit 9 | from core_helpers.mobile_app_helper import Mobile_App_Helper 10 | 11 | 12 | class Bitcoin_Main_Page(Mobile_App_Helper): 13 | "Page object bitcoin main page." 14 | 15 | #Locators of the mobile page elements. 16 | bitcoin_real_time_price_button = locators.bitcoin_real_time_price_button 17 | bitcoin_price_page_heading = locators.bitcoin_price_page_heading 18 | 19 | 20 | @Wrapit._screenshot 21 | def click_on_price_button(self): 22 | "This method is to click on real time price page button." 23 | try: 24 | # Click on real time price page button. 25 | result_flag = None 26 | if self.click_element(self.bitcoin_real_time_price_button): 27 | result_flag = True 28 | else: 29 | result_flag = False 30 | 31 | self.conditional_write(result_flag, 32 | positive='Click on the bitcoin real time price page button.', 33 | negative='Failed to click on the bitcoin real time price page button.', 34 | level='debug') 35 | 36 | except Exception as e: 37 | self.write("Exception while clicking on the bitcoin real time price button.") 38 | self.write(str(e)) 39 | 40 | return result_flag 41 | 42 | 43 | @Wrapit._screenshot 44 | def check_redirect(self, expected_bitcoin_price_page_heading): 45 | "This method is to check if we have been redirected to the bitcoin real time price page." 46 | result_flag = False 47 | bitcoin_price_page_heading = self.get_text_by_locator(self.bitcoin_price_page_heading) 48 | if bitcoin_price_page_heading.decode('utf-8') == expected_bitcoin_price_page_heading: 49 | result_flag = True 50 | self.switch_page("bitcoin price page") 51 | 52 | return result_flag 53 | 54 | 55 | @Wrapit._screenshot 56 | def click_on_real_time_price_button(self, expected_bitcoin_price_page_heading): 57 | "This method is to visit bitcoin real time price page and verify page heading." 58 | result_flag = self.click_on_price_button() 59 | result_flag &= self.check_redirect(expected_bitcoin_price_page_heading) 60 | 61 | return result_flag 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /page_objects/examples/bitcoin_mobile_app/bitcoin_price_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | Page object for Bitcoin price Page. 3 | """ 4 | import os 5 | import sys 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | import conf.locators_conf as locators 8 | from utils.Wrapit import Wrapit 9 | from core_helpers.mobile_app_helper import Mobile_App_Helper 10 | 11 | 12 | class Bitcoin_Price_Page(Mobile_App_Helper): 13 | "Page object for real time prices of bitcoin page." 14 | 15 | # Locators of the bitcoin real time price page elements. 16 | bitcoin_price_in_usd = locators.bitcoin_price_in_usd 17 | 18 | 19 | @Wrapit._screenshot 20 | def get_bitcoin_real_time_price(self): 21 | "This method is to get the real time price of the bitcoin." 22 | # Final result flag is to return False if expected price and real time price is different. 23 | try: 24 | # Get real time price of the bitcoin in usd. 25 | result_flag = None 26 | bitcoin_price_in_usd = self.get_text_by_locator(self.bitcoin_price_in_usd) 27 | if bitcoin_price_in_usd is not None: 28 | result_flag = True 29 | else: 30 | result_flag = False 31 | 32 | self.conditional_write(result_flag, 33 | positive='Get the bitcoin real time price in usd.', 34 | negative='Failed to get the bitcoin real time price in usd.', 35 | level='debug') 36 | 37 | except Exception as e: 38 | self.write("Exception while getting real time price of the bitcoin.") 39 | self.write(str(e)) 40 | 41 | return result_flag 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/page_objects/examples/selenium_tutorial_webpage/__init__.py -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/contact_form_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the form on contact page 3 | The form consists of some input fields. 4 | """ 5 | import conf.locators_conf as locators 6 | from utils.Wrapit import Wrapit 7 | 8 | class Contact_Form_Object: 9 | "Page object for the contact Form" 10 | 11 | #locators 12 | contact_name_field = locators.contact_name_field 13 | 14 | @Wrapit._exceptionHandler 15 | def set_name(self,name): 16 | "Set the name on the Kick start form" 17 | result_flag = self.set_text(self.contact_name_field,name) 18 | self.conditional_write(result_flag, 19 | positive='Set the name to: %s'%name, 20 | negative='Failed to set the name in the form', 21 | level='debug') 22 | 23 | return result_flag 24 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/contact_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the Contact page. 3 | URL: contact 4 | The page consists of a header, footer and form object. 5 | """ 6 | from core_helpers.web_app_helper import Web_App_Helper 7 | from .contact_form_object import Contact_Form_Object 8 | from .header_object import Header_Object 9 | from .footer_object import Footer_Object 10 | 11 | class Contact_Page(Web_App_Helper,Contact_Form_Object,Header_Object,Footer_Object): 12 | "Page Object for the contact page" 13 | 14 | def start(self): 15 | "Use this method to go to specific URL -- if needed" 16 | url = 'contact' 17 | self.open(url) 18 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/footer_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the footer object on qxf2.com 3 | We model it as two parts: 4 | 1. The menu 5 | 2. The copyright 6 | """ 7 | from datetime import datetime 8 | import conf.locators_conf as locators 9 | from utils.Wrapit import Wrapit 10 | 11 | 12 | class Footer_Object: 13 | "Page object for the footer class" 14 | 15 | #locators 16 | footer_menu = locators.footer_menu 17 | copyright_text = locators.copyright_text 18 | copyright_start_year = "2013" 19 | 20 | 21 | @Wrapit._exceptionHandler 22 | def goto_footer_link(self,link_name,expected_url_string=None): 23 | "Go to the link in the footer" 24 | #Format for a link: string separated by a '>' 25 | #E.g.: 'About > Our name' 26 | result_flag = True 27 | split_link = link_name.split('>') 28 | for link in split_link: 29 | result_flag &= self.click_element(self.footer_menu%link.strip()) 30 | 31 | #Additional check to see if we went to the right page 32 | if expected_url_string is not None: 33 | result_flag &= True if expected_url_string in self.get_current_url() else False 34 | 35 | return result_flag 36 | 37 | @Wrapit._exceptionHandler 38 | def get_copyright(self): 39 | "Get the current copyright" 40 | copyright_slug = str(self.get_text(self.copyright_text)) 41 | copyright_slug = copyright_slug.strip() 42 | #NOTE: We strip out the special '© 43 | copyright_slug = 'Qxf2' + copyright_slug[:-1].split('Qxf2')[-1] 44 | 45 | return copyright_slug 46 | 47 | 48 | def get_current_year(self): 49 | "Return the current year in YYYY format" 50 | now = datetime.now() 51 | current_year = now.strftime('%Y') 52 | 53 | return current_year 54 | 55 | @Wrapit._exceptionHandler 56 | def check_copyright(self): 57 | "Check if the copyright is correct" 58 | result_flag = False 59 | actual_copyright = self.get_copyright() 60 | self.write('Copyright text: {}'.format(actual_copyright),'debug') 61 | #Note: We need to maintain this at 2015 because we promised to not change this page 62 | expected_copyright = "Qxf2 Services " + self.copyright_start_year + " - 2015" 63 | 64 | if actual_copyright == expected_copyright: 65 | result_flag = True 66 | 67 | return result_flag 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/form_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the form on the Selenium tutorial page 3 | The form consists of some input fields, a dropdown, a checkbox and a button 4 | """ 5 | import conf.locators_conf as locators 6 | from utils.Wrapit import Wrapit 7 | 8 | 9 | class Form_Object: 10 | "Page object for the Form" 11 | 12 | #locators 13 | name_field = locators.name_field 14 | email_field = locators.email_field 15 | phone_no_field = locators.phone_no_field 16 | click_me_button = locators.click_me_button 17 | gender_dropdown = locators.gender_dropdown 18 | gender_option = locators.gender_option 19 | tac_checkbox = locators.tac_checkbox 20 | redirect_title = "redirect" 21 | 22 | @Wrapit._exceptionHandler 23 | @Wrapit._screenshot 24 | def set_name(self,name): 25 | "Set the name on the form" 26 | result_flag = self.set_text(self.name_field,name) 27 | self.conditional_write(result_flag, 28 | positive='Set the name to: %s'%name, 29 | negative='Failed to set the name in the form', 30 | level='debug') 31 | 32 | return result_flag 33 | 34 | 35 | @Wrapit._exceptionHandler 36 | @Wrapit._screenshot 37 | def set_email(self,email): 38 | "Set the email on the form" 39 | result_flag = self.set_text(self.email_field,email) 40 | self.conditional_write(result_flag, 41 | positive='Set the email to: %s'%email, 42 | negative='Failed to set the email in the form', 43 | level='debug') 44 | 45 | return result_flag 46 | 47 | 48 | @Wrapit._exceptionHandler 49 | @Wrapit._screenshot 50 | def set_phone(self,phone): 51 | "Set the phone on the form" 52 | result_flag = self.set_text(self.phone_no_field,phone) 53 | self.conditional_write(result_flag, 54 | positive='Set the phone to: %s'%phone, 55 | negative='Failed to set the phone in the form', 56 | level='debug') 57 | 58 | return result_flag 59 | 60 | 61 | @Wrapit._exceptionHandler 62 | @Wrapit._screenshot 63 | def set_gender(self,gender,wait_seconds=1): 64 | "Set the gender on the form" 65 | result_flag = self.click_element(self.gender_dropdown) 66 | self.wait(wait_seconds) 67 | result_flag &= self.click_element(self.gender_option%gender) 68 | self.conditional_write(result_flag, 69 | positive='Set the gender to: %s'%gender, 70 | negative='Failed to set the gender in the form', 71 | level='debug') 72 | 73 | return result_flag 74 | 75 | 76 | @Wrapit._exceptionHandler 77 | @Wrapit._screenshot 78 | def click_me(self): 79 | "Click on 'Click Me' button" 80 | result_flag = self.click_element(self.click_me_button) 81 | self.conditional_write(result_flag, 82 | positive='Clicked on the "click me" button', 83 | negative='Failed to click on "click me" button', 84 | level='debug') 85 | 86 | return result_flag 87 | 88 | 89 | @Wrapit._exceptionHandler 90 | @Wrapit._screenshot 91 | def accept_terms(self): 92 | "Accept the terms and conditions" 93 | result_flag = self.select_checkbox(self.tac_checkbox) 94 | self.conditional_write(result_flag, 95 | positive='Accepted the terms and conditions', 96 | negative='Failed to accept the terms and conditions', 97 | level='debug') 98 | 99 | return result_flag 100 | 101 | 102 | @Wrapit._exceptionHandler 103 | @Wrapit._screenshot 104 | def check_redirect(self): 105 | "Check if we have been redirected to the redirect page" 106 | result_flag = False 107 | if self.redirect_title in self.driver.title: 108 | result_flag = True 109 | self.switch_page("redirect") 110 | 111 | return result_flag 112 | 113 | 114 | @Wrapit._exceptionHandler 115 | @Wrapit._screenshot 116 | def submit_form(self,username,email,phone,gender): 117 | "Submit the form" 118 | result_flag = self.set_name(username) 119 | result_flag &= self.set_email(email) 120 | result_flag &= self.set_phone(phone) 121 | result_flag &= self.set_gender(gender) 122 | result_flag &= self.accept_terms() 123 | result_flag &= self.click_me() 124 | result_flag &= self.check_redirect() 125 | 126 | return result_flag 127 | 128 | 129 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/hamburger_menu_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the hamburger menu object as a Page Object 3 | The hamburger menu has a bunch of options that can be: 4 | a) Clicked 5 | b) Hovered over 6 | """ 7 | import conf.locators_conf as locators 8 | from utils.Wrapit import Wrapit 9 | 10 | 11 | class Hamburger_Menu_Object: 12 | "Page Object for the hamburger menu" 13 | 14 | #locators 15 | menu_icon = locators.menu_icon 16 | menu_link = locators.menu_link 17 | menu_item = locators.menu_item 18 | 19 | @Wrapit._exceptionHandler 20 | def goto_menu_link(self,my_link,expected_url_string=None): 21 | "Navigate to a link: Hover + Click or just Click" 22 | #Format for link: string separated by '>' 23 | #E.g.: 'Approach > Where we start' 24 | split_link = my_link.split('>') 25 | hover_list = split_link[:-1] 26 | self.click_hamburger_menu() 27 | for element in hover_list: 28 | self.hover(self.menu_item%element.strip()) 29 | result_flag = self.click_element(self.menu_link%split_link[-1].strip()) 30 | 31 | #Additional check to see if we went to the right page 32 | if expected_url_string is not None: 33 | result_flag &= True if expected_url_string in self.get_current_url() else False 34 | 35 | #If the action failed, close the Hamburger menu 36 | if result_flag is False: 37 | self.click_hamburger_menu() 38 | 39 | return result_flag 40 | 41 | 42 | def click_hamburger_menu(self): 43 | "Click on the hamburger menu icon" 44 | return self.click_element(self.menu_icon) 45 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/header_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the Qxf2.com header as a Page Object. 3 | The header consists of the Qxf2 logo, Qxf2 tag-line and the hamburger menu 4 | Since the hanburger menu is complex, we will model it as a separate object 5 | """ 6 | 7 | from .hamburger_menu_object import Hamburger_Menu_Object 8 | import conf.locators_conf as locators 9 | from utils.Wrapit import Wrapit 10 | 11 | 12 | class Header_Object(Hamburger_Menu_Object): 13 | "Page Object for the header class" 14 | 15 | #locators 16 | qxf2_logo = locators.qxf2_logo 17 | qxf2_tagline_part1 = locators.qxf2_tagline_part1 18 | qxf2_tagline_part2 = locators.qxf2_tagline_part2 19 | 20 | @Wrapit._exceptionHandler 21 | def check_logo_present(self): 22 | "Check if a logo is present" 23 | return self.check_element_present(self.qxf2_logo) 24 | 25 | @Wrapit._exceptionHandler 26 | def check_tagline_present(self): 27 | "Check if the tagline is present" 28 | return self.check_element_present(self.qxf2_tagline_part1) and self.check_element_present(self.qxf2_tagline_part2) 29 | 30 | 31 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/table_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the table on the Selenium tutorial page 3 | """ 4 | import conf.locators_conf as locators 5 | from utils.Wrapit import Wrapit 6 | 7 | 8 | class Table_Object: 9 | "Page Object for the table" 10 | 11 | #locators 12 | table_xpath = locators.table_xpath 13 | rows_xpath = locators.rows_xpath 14 | cols_xpath = locators.cols_xpath 15 | cols_relative_xpath = locators.cols_relative_xpath 16 | cols_header = locators.cols_header 17 | 18 | #Column numbers (makes code more readable) 19 | COL_NAME = 0 20 | COL_EMAIL = 1 21 | COL_PHONE = 2 22 | COL_GENDER = 3 23 | 24 | @Wrapit._check_browser_console_log 25 | @Wrapit._exceptionHandler 26 | @Wrapit._screenshot 27 | def get_all_text(self): 28 | "Get the text within the table" 29 | table_text = [] 30 | row_doms = self.get_elements(self.rows_xpath) 31 | for index in range(0,len(row_doms)): 32 | row_text = [] 33 | cell_doms = self.get_elements(self.cols_relative_xpath%(index+1)) 34 | for cell_dom in cell_doms: 35 | row_text.append(self.get_dom_text(cell_dom).decode('utf-8')) 36 | table_text.append(row_text) 37 | 38 | return table_text 39 | 40 | 41 | def get_num_rows(self): 42 | "Get the total number of rows in the table" 43 | #NOTE: We do not count the header row 44 | row_doms = self.get_elements(self.rows_xpath) 45 | 46 | return len(row_doms) 47 | 48 | 49 | def get_num_cols(self): 50 | "Return the number of columns" 51 | #NOTE: We just count the columns in the header row 52 | col_doms = self.get_elements(self.cols_header) 53 | 54 | return len(col_doms) 55 | 56 | 57 | def get_column_text(self,column_name): 58 | "Get the text within a column" 59 | column_text = [] 60 | col_index = -1 61 | if column_name.lower()=='name': 62 | col_index = self.COL_NAME 63 | if column_name.lower()=='email': 64 | col_index = self.COL_EMAIL 65 | if column_name.lower()=='phone': 66 | col_index = self.COL_PHONE 67 | if column_name.lower()=='gender': 68 | col_index = self.COL_GENDER 69 | 70 | if col_index > -1: 71 | table_text = self.get_all_text() 72 | #Transpose the matrix since you want the column 73 | column_text = list(zip(*table_text))[col_index] 74 | 75 | return column_text 76 | 77 | @Wrapit._exceptionHandler 78 | def get_column_names(self): 79 | "Return a list with the column names" 80 | column_names = [] 81 | col_doms = self.get_elements(self.cols_header) 82 | for col_dom in col_doms: 83 | column_names.append(self.get_dom_text(col_dom).decode('utf-8')) 84 | 85 | return column_names 86 | 87 | @Wrapit._exceptionHandler 88 | def check_cell_text_present(self,text,column_name='all'): 89 | "Check if the text you want is present in a cell" 90 | result_flag = False 91 | if column_name == 'all': 92 | table_text = self.get_all_text() 93 | 94 | else: 95 | table_text = [self.get_column_text(column_name)] 96 | 97 | for row in table_text: 98 | for col in row: 99 | if col == text: 100 | result_flag = True 101 | break 102 | if result_flag is True: 103 | break 104 | 105 | return result_flag 106 | 107 | @Wrapit._exceptionHandler 108 | def check_name_present(self,name): 109 | "Check if the supplied name is present anywhere in the table" 110 | return self.check_cell_text_present(name,column_name='name') 111 | 112 | 113 | @Wrapit._exceptionHandler 114 | def print_table_text(self): 115 | "Print out the table text neatly" 116 | result_flag = False 117 | column_names = self.get_column_names() 118 | table_text = self.get_all_text() 119 | self.write('||'.join(column_names)) 120 | if table_text is not None: 121 | for row in table_text: 122 | self.write('|'.join(row)) 123 | result_flag = True 124 | 125 | return result_flag -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/tutorial_main_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the main Selenium tutorial page. 3 | URL: selenium-tutorial-main 4 | The page consists of a header, footer, form and table objects 5 | """ 6 | from core_helpers.web_app_helper import Web_App_Helper 7 | from .form_object import Form_Object 8 | from .header_object import Header_Object 9 | from .table_object import Table_Object 10 | from .footer_object import Footer_Object 11 | 12 | 13 | class Tutorial_Main_Page(Web_App_Helper,Form_Object,Header_Object,Table_Object,Footer_Object): 14 | "Page Object for the tutorial's main page" 15 | 16 | def start(self): 17 | "Use this method to go to specific URL -- if needed" 18 | url = 'selenium-tutorial-main' 19 | self.open(url) 20 | -------------------------------------------------------------------------------- /page_objects/examples/selenium_tutorial_webpage/tutorial_redirect_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the redirect page of the Selenium tutorial 3 | URL: selenium-tutorial-redirect 4 | The page consists of a header, footer and some text 5 | """ 6 | from core_helpers.web_app_helper import Web_App_Helper 7 | from .header_object import Header_Object 8 | from .footer_object import Footer_Object 9 | import conf.locators_conf as locators 10 | from utils.Wrapit import Wrapit 11 | 12 | 13 | class Tutorial_Redirect_Page(Web_App_Helper,Header_Object,Footer_Object): 14 | "Page Object for the tutorial's redirect page" 15 | 16 | #locators 17 | heading = locators.heading 18 | 19 | def start(self): 20 | "Use this method to go to specific URL -- if needed" 21 | url = 'selenium-tutorial-redirect' 22 | self.open(url) 23 | 24 | @Wrapit._exceptionHandler 25 | def check_heading(self): 26 | "Check if the heading exists" 27 | result_flag = self.check_element_present(self.heading) 28 | self.conditional_write(result_flag, 29 | positive='Correct heading present on redirect page', 30 | negative='Heading on redirect page is INCORRECT!!', 31 | level='debug') 32 | 33 | return result_flag 34 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/cart_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the objects of the cart in Weathershopper application. 3 | """ 4 | import conf.locators_conf as locators 5 | from utils.Wrapit import Wrapit 6 | import re 7 | 8 | class CartObjects: 9 | """ 10 | Page Objects for the cart in Weather Shopper application 11 | """ 12 | 13 | @Wrapit._exceptionHandler 14 | @Wrapit._screenshot 15 | def change_quantity(self, item_to_change, quantity=2): 16 | "This method is to change the quantity of an item in the cart" 17 | result_flag = self.set_text(locators.edit_quantity.format(item_to_change), quantity) 18 | self.conditional_write(result_flag, 19 | positive=f"Successfully changed quantity of {item_to_change} to {quantity}", 20 | negative=f"Failed to change quantity of {item_to_change} to {quantity}", 21 | level='debug') 22 | return result_flag 23 | 24 | @Wrapit._exceptionHandler 25 | @Wrapit._screenshot 26 | def checkout(self): 27 | "This method is to go to the Checkout page" 28 | result_flag = self.click_element(locators.checkout_button) 29 | self.conditional_write(result_flag, 30 | positive="Successfully clicked on checkout button", 31 | negative="Failed to click on checkout button", 32 | level='debug') 33 | self.switch_page("weathershopper payment page") 34 | return result_flag 35 | 36 | @Wrapit._exceptionHandler 37 | @Wrapit._screenshot 38 | def delete_from_cart(self, item_to_delete): 39 | "This method is to delete an item from the cart" 40 | result_flag = self.click_element(locators.checkbox.format(item_to_delete)) 41 | result_flag &= self.click_element(locators.delete_from_cart_button) 42 | self.conditional_write(result_flag, 43 | positive=f"Successfully deleted {item_to_delete} from cart", 44 | negative="Failed to delete item from cart", 45 | level='debug') 46 | return result_flag 47 | 48 | @Wrapit._exceptionHandler 49 | @Wrapit._screenshot 50 | def get_cart_total(self): 51 | "This method is to get the total price in the cart." 52 | cart_total = self.get_text(locators.total_amount) 53 | # Extracting the numeric part using regular expression 54 | match = re.search(r'\d+\.\d+', cart_total.decode()) 55 | total_amount = float(match.group()) if match else None 56 | 57 | if total_amount is None: 58 | self.write("Total amount is None", level='debug') 59 | else: 60 | self.write(f"Total amount is {total_amount}", level='debug') 61 | return total_amount 62 | 63 | @Wrapit._exceptionHandler 64 | @Wrapit._screenshot 65 | def refresh_total_amount(self): 66 | "This method is to refresh the total amount in the cart." 67 | result_flag = self.click_element(locators.refresh_button) 68 | self.conditional_write(result_flag, 69 | positive="Successfully clicked on refresh button", 70 | negative="Failed to click on refresh button", 71 | level='debug') 72 | return result_flag 73 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/homepage_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the objects of the home screen in Weathershopper application. 3 | """ 4 | import conf.locators_conf as locators 5 | from utils.Wrapit import Wrapit 6 | import secrets 7 | 8 | class HomepageObjects: 9 | "Page object for the home screen in Weathershopper application." 10 | 11 | @Wrapit._exceptionHandler 12 | def visit_product_page(self, temperature): 13 | "Visit the product page" 14 | product = None 15 | result_flag = False 16 | if temperature < 19: 17 | result_flag = self.view_moisturizers() 18 | product = "Moisturizers" 19 | 20 | elif temperature > 32: 21 | result_flag = self.view_sunscreens() 22 | product = "Sunscreens" 23 | 24 | else: 25 | skin_product = secrets.choice(['Moisturizers', 'Sunscreens']) 26 | if skin_product == 'Moisturizers': 27 | result_flag = self.view_moisturizers() 28 | product = "Moisturizers" 29 | else: 30 | result_flag = self.view_sunscreens() 31 | product = "Sunscreens" 32 | 33 | return product, result_flag 34 | 35 | @Wrapit._exceptionHandler 36 | @Wrapit._screenshot 37 | def get_temperature(self): 38 | "This method is to get the temperature in the Weather Shopper application." 39 | temperature = locators.temperature 40 | current_temperature = self.get_text(temperature) 41 | self.write(f"Current temperature is {int(current_temperature)}", level='debug') 42 | return int(current_temperature) 43 | 44 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/navigation_menu_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the navigation menu in Weathershopper application. 3 | """ 4 | import conf.locators_conf as locators 5 | from utils.Wrapit import Wrapit 6 | 7 | class NavigationMenuObjects: 8 | "Page objects for the navigation menu in Weathershopper application." 9 | 10 | @Wrapit._exceptionHandler 11 | @Wrapit._screenshot 12 | def view_moisturizers(self): 13 | "This method is to click on Moisturizer tab in the Weather Shopper application." 14 | moisturizers = locators.moisturizers 15 | result_flag = self.click_element(moisturizers) 16 | self.conditional_write(result_flag, 17 | positive='Successfully clicked on Moisturizer tab', 18 | negative='Failed to click on Moisturizer tab', 19 | level='debug') 20 | self.switch_page("weathershopper products page") 21 | return result_flag 22 | 23 | @Wrapit._exceptionHandler 24 | @Wrapit._screenshot 25 | def view_sunscreens(self): 26 | "This method is to click on Sunscreen tab in the Weather Shopper application." 27 | sunscreens = locators.sunscreens 28 | result_flag = self.click_element(sunscreens) 29 | self.conditional_write(result_flag, 30 | positive='Successfully clicked on Sunscreen tab', 31 | negative='Failed to click on Sunscreen tab', 32 | level='debug') 33 | self.switch_page("weathershopper products page") 34 | return result_flag 35 | 36 | @Wrapit._exceptionHandler 37 | @Wrapit._screenshot 38 | def view_cart(self): 39 | "This method is to click on Cart button in the Weather Shopper application." 40 | cart = locators.cart 41 | result_flag = self.click_element(cart) 42 | self.switch_page("weathershopper cart page") 43 | return result_flag 44 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/product_page_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the page objects for the products in Weathershopper application. 3 | """ 4 | import conf.locators_conf as locators 5 | from utils.Wrapit import Wrapit 6 | 7 | class ProductPageObjects: 8 | "Page objects for the products in Weathershopper application." 9 | @Wrapit._exceptionHandler 10 | @Wrapit._screenshot 11 | def add_to_cart(self,item): 12 | "This method is to click on Add to cart button in the Weather Shopper application." 13 | result_flag = self.scroll_to_bottom() 14 | result_flag &= self.swipe_to_element(locators.recycler_view, 15 | locators.add_to_cart.format(item['name']), 16 | direction="down") 17 | result_flag &= self.click_element(locators.add_to_cart.format(item['name'])) 18 | self.conditional_write(result_flag, 19 | positive=f"Successfully added {item['name']} to cart", 20 | negative=f"Failed to add {item['name']} to cart", 21 | level='debug') 22 | return result_flag 23 | 24 | @Wrapit._exceptionHandler 25 | @Wrapit._screenshot 26 | def get_all_products(self): 27 | "This method is to get all items from product page." 28 | all_products = [] 29 | max_scrolls = 50 30 | for attempt in range(max_scrolls): 31 | # Get product names and prices 32 | product_names = self.get_elements(locators.product_name) 33 | product_prices = self.get_elements(locators.product_price) 34 | 35 | # Store the product names and prices in a list 36 | all_products = self.store_products_in_list(all_products, product_names, product_prices) 37 | 38 | # Scroll forward 39 | result_flag = self.scroll_forward() 40 | 41 | # If products are the same as the previous scroll, break the loop 42 | products_after_scroll = self.get_elements(locators.product_name) 43 | if products_after_scroll == product_names: 44 | break 45 | 46 | # If it's the last attempt and we haven't reached the end, set result_flag to False 47 | if attempt == max_scrolls - 1: 48 | result_flag &= False 49 | 50 | self.conditional_write(result_flag, 51 | positive='Successfully scrolled to the end of the page', 52 | negative='Failed to scroll to the end of the page', 53 | level='debug') 54 | return all_products 55 | 56 | def store_products_in_list(self, all_products, product_names, product_prices): 57 | "This method is to store the products in view to a list." 58 | for name, price in zip(product_names, product_prices): 59 | product_name = self.get_text(name, dom_element_flag=True) 60 | product_price = self.get_text(price, dom_element_flag=True) 61 | 62 | # Append the product name and price to the list if it is not already in the list 63 | if {"name": product_name.decode(), 64 | "price": float(product_price)} not in all_products: 65 | all_products.append({"name": product_name.decode(), 66 | "price": float(product_price)}) 67 | return all_products 68 | 69 | @Wrapit._exceptionHandler 70 | @Wrapit._screenshot 71 | def zoom_in_product_image(self, product_type): 72 | "This method is to zoom in the product image in the Weather Shopper application." 73 | if product_type == "Moisturizers": 74 | product_image = locators.image_of_moisturizer 75 | result_flag = self.click_element(product_image) 76 | else: 77 | product_image = locators.image_of_sunscreen 78 | result_flag = self.click_element(product_image) 79 | 80 | self.conditional_write(result_flag, 81 | positive='Successfully zoomed in product image', 82 | negative='Failed to zoom in product image', 83 | level='debug') 84 | return result_flag 85 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/weather_shopper_cart_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the cart page in Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401 5 | from utils.Wrapit import Wrapit 6 | from core_helpers.mobile_app_helper import Mobile_App_Helper 7 | from .cart_objects import CartObjects 8 | 9 | class WeatherShopperCartPage(Mobile_App_Helper, CartObjects): 10 | "Page objects for the cart page in Weathershopper application." 11 | 12 | @Wrapit._exceptionHandler 13 | def change_quantity_and_verify(self, least_expensive_item, 14 | most_expensive_item, quantity): 15 | "Change quantity of item and verify cart total" 16 | # Change quantity of least expensive item 17 | result_flag = self.change_quantity(least_expensive_item['name'], quantity=quantity) 18 | self.conditional_write(result_flag, 19 | positive="Successfully changed quantity of item", 20 | negative="Failed to change quantity of item") 21 | 22 | # Refresh cart total 23 | result_flag = self.refresh_total_amount() 24 | self.conditional_write(result_flag, 25 | positive="Successfully refreshed total", 26 | negative="Failed to refresh total") 27 | 28 | # Verify cart total after change in quantity 29 | cart_total_after_change = self.get_cart_total() 30 | item_prices = [least_expensive_item['price'] * quantity, most_expensive_item['price']] 31 | result_flag = self.verify_total(cart_total_after_change, item_prices) 32 | 33 | return result_flag 34 | 35 | @Wrapit._exceptionHandler 36 | def delete_item_and_verify(self, least_expensive_item, most_expensive_item, quantity): 37 | "Delete item from cart and verify cart total" 38 | # Delete item from cart 39 | result_flag = self.delete_from_cart(most_expensive_item['name']) 40 | self.conditional_write(result_flag, 41 | positive="Successfully deleted item from cart", 42 | negative="Failed to delete item from cart") 43 | 44 | # Verify cart total after deletion 45 | cart_total_after_deletion = self.get_cart_total() 46 | item_prices = [least_expensive_item['price'] * quantity] 47 | result_flag = self.verify_total(cart_total_after_deletion, item_prices) 48 | 49 | return result_flag 50 | 51 | @Wrapit._exceptionHandler 52 | @Wrapit._screenshot 53 | def verify_total(self, cart_total, cart_items): 54 | "This method is to verify the total price in the cart." 55 | if cart_total == sum(cart_items): 56 | return True 57 | return False 58 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/weather_shopper_home_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the home page in Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401 5 | from core_helpers.mobile_app_helper import Mobile_App_Helper 6 | from .homepage_objects import HomepageObjects 7 | from .navigation_menu_objects import NavigationMenuObjects 8 | import conf.locators_conf as locators 9 | from utils.Wrapit import Wrapit 10 | 11 | class WeatherShopperHomePage(Mobile_App_Helper, HomepageObjects, NavigationMenuObjects): 12 | "Page objects for home page in Weathershopper application." 13 | @Wrapit._exceptionHandler 14 | @Wrapit._screenshot 15 | def click_on_menu_option(self): 16 | "Click on Menu option" 17 | result_flag = self.click_element(locators.menu_option) 18 | return result_flag 19 | 20 | @Wrapit._exceptionHandler 21 | @Wrapit._screenshot 22 | def get_developed_by_label(self): 23 | "Get the developed by label" 24 | label = self.get_text(locators.developed_by) 25 | return label.decode("utf-8") 26 | 27 | @Wrapit._exceptionHandler 28 | @Wrapit._screenshot 29 | def get_about_app_label(self): 30 | "Get the about app label" 31 | label = self.get_text(locators.about_app) 32 | return label.decode("utf-8") 33 | 34 | @Wrapit._exceptionHandler 35 | @Wrapit._screenshot 36 | def get_automation_framework_label(self): 37 | "Get the automation framework label" 38 | label = self.get_text(locators.framework) 39 | return label.decode("utf-8") 40 | 41 | @Wrapit._exceptionHandler 42 | @Wrapit._screenshot 43 | def get_privacy_policy_label(self): 44 | "Get the privacy policy label" 45 | label = self.get_text(locators.privacy_policy) 46 | return label.decode("utf-8") 47 | 48 | @Wrapit._exceptionHandler 49 | @Wrapit._screenshot 50 | def get_contact_us_label(self): 51 | "Get the contact us label" 52 | label = self.get_text(locators.contact_us) 53 | return label.decode("utf-8") 54 | 55 | @Wrapit._exceptionHandler 56 | @Wrapit._screenshot 57 | def click_get_developed_by_option(self): 58 | "Click developed by menu option" 59 | result_flag = self.click_element(locators.developed_by) 60 | return result_flag 61 | 62 | @Wrapit._exceptionHandler 63 | @Wrapit._screenshot 64 | def click_about_app_option(self): 65 | "Click about app menu option" 66 | result_flag = self.click_element(locators.about_app) 67 | return result_flag 68 | 69 | @Wrapit._exceptionHandler 70 | @Wrapit._screenshot 71 | def click_automation_framework_option(self): 72 | "Click automation framework menu option" 73 | result_flag = self.click_element(locators.framework) 74 | return result_flag 75 | 76 | @Wrapit._exceptionHandler 77 | @Wrapit._screenshot 78 | def click_contact_us_option(self): 79 | "Click contact us menu option" 80 | result_flag = self.click_element(locators.contact_us) 81 | return result_flag 82 | 83 | @Wrapit._exceptionHandler 84 | @Wrapit._screenshot 85 | def dismiss_chrome_welcome(self): 86 | "Dismiss Chrome welcome setting" 87 | self.handle_chrome_welcome_page(locators.chrome_welcome_dismiss, locators.turn_off_sync_button) 88 | 89 | @Wrapit._exceptionHandler 90 | @Wrapit._screenshot 91 | def switch_to_app_context(self): 92 | "Switch to app context to access mobile app elements" 93 | result_flag = self.switch_context("NATIVE_APP") 94 | self.conditional_write(result_flag, 95 | positive="Switched to NATIVE_APP context", 96 | negative="Unable to switch to NATIVE_APP context") 97 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/weather_shopper_payment_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | Page object for the payment page in Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401,W0104,R0913,R1710,W0718,E0402 5 | 6 | 7 | import conf.locators_conf as locators 8 | from utils.Wrapit import Wrapit 9 | from core_helpers.mobile_app_helper import Mobile_App_Helper 10 | 11 | class WeatherShopperPaymentPageObjects(Mobile_App_Helper): 12 | "Page objects for payment page in Weathershopper application." 13 | 14 | @Wrapit._exceptionHandler 15 | @Wrapit._screenshot 16 | def enter_card_cvv(self, card_cvv): 17 | "Enter the card CVV" 18 | result_flag = self.set_text(locators.payment_card_cvv, card_cvv) 19 | self.conditional_write(result_flag, 20 | positive=f'Successfully set the card CVV: {card_cvv}', 21 | negative=f'Failed to set the card CVV: {card_cvv}', 22 | level='debug') 23 | return result_flag 24 | 25 | @Wrapit._exceptionHandler 26 | @Wrapit._screenshot 27 | def enter_card_expiry(self, card_expiry): 28 | "Enter the card expiry date" 29 | result_flag = self.set_text(locators.payment_card_expiry, card_expiry) 30 | self.conditional_write(result_flag, 31 | positive=f'Successfully set the card expiry date: {card_expiry}', 32 | negative=f'Failed to set the card expiry date: {card_expiry}', 33 | level='debug') 34 | return result_flag 35 | 36 | @Wrapit._exceptionHandler 37 | @Wrapit._screenshot 38 | def enter_email(self, email): 39 | "Enter the email address" 40 | result_flag = self.set_text(locators.payment_email, email) 41 | self.conditional_write(result_flag, 42 | positive=f'Successfully set the email address: {email}', 43 | negative=f'Failed to set the email address: {email}', 44 | level='debug') 45 | return result_flag 46 | 47 | @Wrapit._exceptionHandler 48 | @Wrapit._screenshot 49 | def enter_card_number(self, card_number): 50 | "Enter the card number" 51 | result_flag = self.set_text(locators.payment_card_number, card_number) 52 | self.conditional_write(result_flag, 53 | positive=f'Successfully set the card number: {card_number}', 54 | negative=f'Failed to set the card number: {card_number}', 55 | level='debug') 56 | return result_flag 57 | 58 | @Wrapit._exceptionHandler 59 | @Wrapit._screenshot 60 | def select_payment_method(self, card_type): 61 | "Select the payment method" 62 | result_flag = self.click_element(locators.payment_method_dropdown) 63 | result_flag &= self.click_element(locators.payment_card_type.format(card_type)) 64 | self.conditional_write(result_flag, 65 | positive=f'Successfully selected the payment method: {card_type}', 66 | negative=f'Failed to select the payment method: {card_type}', 67 | level='debug') 68 | return result_flag 69 | 70 | @Wrapit._exceptionHandler 71 | @Wrapit._screenshot 72 | def submit_payment(self): 73 | "Click the pay button" 74 | result_flag = self.click_element(locators.pay_button) 75 | self.conditional_write(result_flag, 76 | positive='Successfully clicked on the pay button', 77 | negative='Failed to click on the pay button', 78 | level='debug') 79 | return result_flag 80 | 81 | @Wrapit._exceptionHandler 82 | @Wrapit._screenshot 83 | def verify_payment_success(self): 84 | "Verify if the payment was successful" 85 | result_flag = self.get_element(locators.payment_success, verbose_flag=False) is not None 86 | self.conditional_write(result_flag, 87 | positive='Payment was successful', 88 | negative='Payment failed', 89 | level='debug') 90 | return result_flag 91 | 92 | @Wrapit._exceptionHandler 93 | @Wrapit._screenshot 94 | def submit_payment_details(self,card_type,email,card_number,card_expiry,card_cvv): 95 | "Submit the form" 96 | result_flag = self.select_payment_method(card_type) 97 | result_flag &= self.enter_email(email) 98 | result_flag &= self.enter_card_number(card_number) 99 | result_flag &= self.enter_card_expiry(card_expiry) 100 | result_flag &= self.enter_card_cvv(card_cvv) 101 | result_flag &= self.submit_payment() 102 | return result_flag 103 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/weather_shopper_payment_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | The payment page in Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401,W0104,R0913,R1710,W0718,E0402 5 | 6 | import os 7 | from PIL import Image, ImageEnhance, ImageFilter 8 | import pytesseract 9 | import conf.locators_conf as locators 10 | from utils.Wrapit import Wrapit 11 | from .weather_shopper_payment_objects import WeatherShopperPaymentPageObjects 12 | 13 | 14 | class WeatherShopperPaymentPage(WeatherShopperPaymentPageObjects): 15 | 16 | "Page objects for payment page in Weathershopper application." 17 | 18 | @Wrapit._exceptionHandler 19 | def capture_payment_field_error(self,fieldname,screenshotname): 20 | """ 21 | Navigating the cursor to the payment error field 22 | and capture the screenshot of the error prompt. 23 | """ 24 | result_flag = self.click_payment_error_field(fieldname) 25 | self.hide_keyboard() 26 | self.save_screenshot(screenshot_name=screenshotname) 27 | 28 | return result_flag 29 | 30 | def click_payment_error_field(self,fieldname): 31 | """ 32 | This method will show the error message 33 | """ 34 | if fieldname == "email": 35 | result_flag = self.click_element(locators.payment_email) 36 | elif fieldname == "card number": 37 | result_flag = self.click_element(locators.payment_card_number) 38 | return result_flag 39 | 40 | @Wrapit._exceptionHandler 41 | def get_string_from_image(self, image_name): 42 | """ 43 | This method opens the image, enhances it, and 44 | extracts the text from the image using Tesseract. 45 | """ 46 | # Construct the full image path 47 | image_dir = self.screenshot_dir 48 | full_image_path = os.path.join(image_dir, f"{image_name}.png") 49 | result_flag = False 50 | # Check if the file exists 51 | if os.path.exists(full_image_path): 52 | # Load the image 53 | image = Image.open(full_image_path) 54 | # Enhance the image before OCR 55 | enhanced_image = self.preprocess_image(image) 56 | # Perform OCR on the enhanced image 57 | text = pytesseract.image_to_string(enhanced_image) 58 | result_flag = True 59 | else: 60 | text = "" 61 | return result_flag,text 62 | 63 | @Wrapit._exceptionHandler 64 | def compare_string(self,image_text,validation_string): 65 | """ 66 | Check if the substring is in the input string 67 | """ 68 | if validation_string in image_text: 69 | return True 70 | 71 | def preprocess_image(self, image): 72 | """ 73 | Pre-process the image. 74 | """ 75 | try: 76 | # Convert to grayscale 77 | grayscale_image = image.convert('L') 78 | # Enhance contrast 79 | enhancer = ImageEnhance.Contrast(grayscale_image) 80 | enhanced_image = enhancer.enhance(2) 81 | # Apply a filter to sharpen the image 82 | sharpened_image = enhanced_image.filter(ImageFilter.SHARPEN) 83 | return sharpened_image 84 | except Exception as preproc: 85 | print(f"Error during image preprocessing: {preproc}") 86 | return image # Return original image if preprocessing fails 87 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/weather_shopper_product_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | Page objects for the product page in Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401 5 | from utils.Wrapit import Wrapit 6 | from core_helpers.mobile_app_helper import Mobile_App_Helper 7 | from .product_page_objects import ProductPageObjects 8 | from .navigation_menu_objects import NavigationMenuObjects 9 | 10 | class WeatherShopperProductPage(Mobile_App_Helper, ProductPageObjects, NavigationMenuObjects): 11 | "Page objects for product page in Weathershopper application." 12 | 13 | @Wrapit._exceptionHandler 14 | def get_least_and_most_expensive_items(self, all_items): 15 | "Get least and most expensive item from the page" 16 | 17 | # Calculate least and most expensive item 18 | least_expensive_item = self.get_least_expensive_item(all_items) 19 | most_expensive_item = self.get_most_expensive_item(all_items) 20 | 21 | return least_expensive_item, most_expensive_item 22 | 23 | @Wrapit._exceptionHandler 24 | def add_items_to_cart(self, items): 25 | "Add items to cart" 26 | result_flag = True 27 | for item in items: 28 | result_flag &= self.add_to_cart(item) 29 | 30 | return result_flag 31 | 32 | @Wrapit._exceptionHandler 33 | @Wrapit._screenshot 34 | def get_least_expensive_item(self, all_items): 35 | "This method is to get the least expensive item from the given list of items" 36 | least_expensive_item = min(all_items, key=lambda x: x['price']) 37 | self.write(f"Least expensive item is {least_expensive_item}") 38 | return least_expensive_item 39 | 40 | @Wrapit._exceptionHandler 41 | @Wrapit._screenshot 42 | def get_most_expensive_item(self, all_items): 43 | "This method is to get the most expensive item from the given list of items" 44 | most_expensive_item = max(all_items, key=lambda x: x['price']) 45 | self.write(f"Most expensive item is {most_expensive_item}") 46 | return most_expensive_item 47 | -------------------------------------------------------------------------------- /page_objects/examples/weather_shopper_mobile_app/webview_chrome.py: -------------------------------------------------------------------------------- 1 | """ 2 | Page object to test Webview Chrome for Weathershopper application. 3 | """ 4 | # pylint: disable = W0212,E0401 5 | from utils.Wrapit import Wrapit 6 | from core_helpers.mobile_app_helper import Mobile_App_Helper 7 | from urllib.parse import unquote 8 | 9 | class WebviewChrome(Mobile_App_Helper): 10 | "Page object for ChromeView interaction" 11 | 12 | @Wrapit._exceptionHandler 13 | @Wrapit._screenshot 14 | def switch_to_chrome_context(self): 15 | "Switch to Chrome webview context to access the browser methods" 16 | result_flag = self.switch_context("WEBVIEW_chrome") 17 | self.conditional_write(result_flag, 18 | positive="Switched to WEBVIEW_chrome context", 19 | negative="Unable to switch to WEBVIEW_chrome context") 20 | 21 | @Wrapit._exceptionHandler 22 | @Wrapit._screenshot 23 | def get_current_url(self): 24 | "This method gets the current URL" 25 | url = unquote(self.driver.current_url) 26 | return url 27 | 28 | @Wrapit._exceptionHandler 29 | @Wrapit._screenshot 30 | def get_page_title(self): 31 | "Get the page title" 32 | return self.driver.title 33 | -------------------------------------------------------------------------------- /page_objects/zero_mobile_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the first dummy page needed by the framework to start. 3 | URL: None 4 | Please do not modify or delete this page 5 | """ 6 | from core_helpers.mobile_app_helper import Mobile_App_Helper 7 | 8 | class Zero_Mobile_Page(Mobile_App_Helper): 9 | "Page Object for the dummy page" 10 | 11 | def start(self): 12 | "Use this method to go to specific URL -- if needed" 13 | pass -------------------------------------------------------------------------------- /page_objects/zero_page.py: -------------------------------------------------------------------------------- 1 | """ 2 | This class models the first dummy page needed by the framework to start. 3 | URL: None 4 | Please do not modify or delete this page 5 | """ 6 | from core_helpers.web_app_helper import Web_App_Helper 7 | 8 | class Zero_Page(Web_App_Helper): 9 | "Page Object for the dummy page" 10 | 11 | def start(self): 12 | "Use this method to go to specific URL -- if needed" 13 | pass -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -s -rsxX --continue-on-collection-errors --tb=short --ignore=utils/Test_Rail.py --ignore=tests/test_boilerplate.py --ignore=utils/Test_Runner_Class.py -p no:cacheprovider 3 | norecursedirs = .svn _build tmp* log .vscode .git 4 | markers = 5 | GUI: mark a test as part of the GUI regression suite 6 | API: mark a test as part of the API regression suite 7 | MOBILE: mark a test as part of the MOBILE regression suite 8 | ACCESSIBILITY: mark a test as part of the ACCESSIBILITY suite 9 | API_AUTO_GEN_UNIT: unit tests to validate API auth gen feature 10 | junit_family=xunit2 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.0 2 | reportportal-client==5.5.4 3 | pytest==8.1.1 4 | selenium==4.12.0 5 | python_dotenv==0.16.0 6 | Appium_Python_Client==3.2.0 7 | pytest-xdist>=1.31 8 | pytest-html>=3.0.0 9 | pytest-rerunfailures>=9.1.1 10 | pytest_reportportal==5.4.0 11 | pillow>=6.2.0 12 | tesults==1.2.1 13 | boto3==1.33.0 14 | loguru 15 | imageio 16 | questionary>=1.9.0 17 | clear-screen>=0.1.14 18 | prompt-toolkit==2.0.10 19 | axe_selenium_python==2.1.6 20 | pytest-snapshot==0.9.0 21 | beautifulsoup4>=4.12.3 22 | openai==1.12.0 23 | pytesseract==0.3.10 24 | pytest-asyncio==0.23.7 25 | prettytable==3.10.2 26 | setuptools>=75.1.0; python_version >= '3.12' 27 | openapi3-parser==1.1.17 28 | jinja2==3.1.6 29 | deepdiff>=8.1.1 30 | requests_mock==1.12.1 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qxf2/qxf2-page-object-model/1f2ab32a363e2fd9c2621913094b40bc965d0d87/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_accessibility.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test file runs accessibility checks on multiple pages of the Selenium tutorial pages using Axe. 3 | It compares the Axe violation results to previously saved snapshots to identify new violations. 4 | If new violations are found, they are logged in a violation record file. 5 | Pages tested: 6 | 1. Selenium tutorial main page 7 | 2. Selenium tutorial redirect page 8 | 3. Selenium tutorial contact page 9 | 10 | Usage: 11 | - Run pytest to check for accessibility issues. 12 | - Use `--snapshot_update` to update the existing snapshots if changes are valid. 13 | """ 14 | import os 15 | import sys 16 | import pytest 17 | from utils.snapshot_util import Snapshotutil 18 | from page_objects.PageFactory import PageFactory 19 | import conf.snapshot_dir_conf 20 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 21 | sys.stdout.reconfigure(encoding='utf-8') 22 | 23 | @pytest.mark.ACCESSIBILITY 24 | def test_accessibility(test_obj, request): 25 | "Test accessibility using Axe and compare snapshot results and save if new violations found" 26 | try: 27 | 28 | #Initalize flags for tests summary 29 | expected_pass = 0 30 | actual_pass = -1 31 | 32 | #Get snapshot update flag from pytest options 33 | snapshot_update = request.config.getoption("--snapshot_update") 34 | #Create an instance of Snapshotutil 35 | snapshot_util = Snapshotutil(snapshot_update=snapshot_update) 36 | 37 | #Set up the violations log file 38 | violations_log_path = snapshot_util.initialize_violations_log() 39 | snapshot_dir = conf.snapshot_dir_conf.snapshot_dir 40 | 41 | #Get all pages 42 | page_names = conf.snapshot_dir_conf.page_names 43 | for page in page_names: 44 | test_obj = PageFactory.get_page_object(page,base_url=test_obj.base_url) 45 | #Inject Axe in every page 46 | test_obj.accessibility_inject_axe() 47 | #Check if Axe is run in every page 48 | axe_result = test_obj.accessibility_run_axe() 49 | #Extract the 'violations' section from the Axe result 50 | current_violations = axe_result.get('violations', []) 51 | # Log if no violations are found 52 | if not current_violations: 53 | test_obj.log_result( 54 | True, 55 | positive=f"No accessibility violations found on {page}.", 56 | negative="", 57 | level='info' 58 | ) 59 | 60 | #Load the existing snapshot for the current page (if available) 61 | existing_snapshot = snapshot_util.initialize_snapshot(snapshot_dir, page, current_violations=current_violations) 62 | if existing_snapshot is None: 63 | test_obj.log_result( 64 | True, 65 | positive=( 66 | f"No existing snapshot was found for {page} page. " 67 | "A new snapshot has been created in ../conf/snapshot dir. " 68 | "Please review the snapshot for violations before running the test again. " 69 | ), 70 | negative="", 71 | level='info' 72 | ) 73 | continue 74 | 75 | #Compare the current violations with the existing snapshot to find any new violations 76 | snapshots_match, new_violation_details = snapshot_util.compare_and_log_violation( 77 | current_violations, existing_snapshot, page, violations_log_path 78 | ) 79 | #For each new violation, log few details to the output display 80 | if new_violation_details: 81 | snapshot_util.log_new_violations(new_violation_details) 82 | #Log the result of the comparison (pass or fail) for the current page 83 | test_obj.log_result(snapshots_match, 84 | positive=f'Accessibility checks for {page} passed', 85 | negative=f'Accessibility checks for {page} failed', 86 | level='debug') 87 | 88 | #Print out the result 89 | test_obj.write_test_summary() 90 | expected_pass = test_obj.result_counter 91 | actual_pass = test_obj.pass_counter 92 | 93 | except Exception as e: 94 | test_obj.log_result( 95 | False, 96 | positive="", 97 | negative=f"Exception when trying to run test: {__file__}\nPython says: {str(e)}", 98 | level='error' 99 | ) 100 | 101 | assert expected_pass == actual_pass, f"Test failed: {__file__}" 102 | -------------------------------------------------------------------------------- /tests/test_api_async_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | API Async EXAMPLE TEST 3 | This test collects tasks using asyncio.TaskGroup object \ 4 | and runs these scenarios asynchronously: 5 | 1. Get the list of cars 6 | 2. Add a new car 7 | 3. Get a specifi car from the cars list 8 | 4. Get the registered cars 9 | """ 10 | 11 | import asyncio 12 | import os 13 | import sys 14 | import pytest 15 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 16 | from conf import api_example_conf 17 | 18 | @pytest.mark.asyncio 19 | # Skip running the test if Python version < 3.11 20 | @pytest.mark.skipif(sys.version_info < (3,11), 21 | reason="requires Python3.11 or higher") 22 | async def test_api_async_example(test_api_obj): 23 | "Run api test" 24 | try: 25 | expected_pass = 0 26 | actual_pass = -1 27 | 28 | # set authentication details 29 | username = api_example_conf.user_name 30 | password = api_example_conf.password 31 | auth_details = test_api_obj.set_auth_details(username, password) 32 | 33 | # Get an existing car detail from conf 34 | existing_car = api_example_conf.car_name_1 35 | brand = api_example_conf.brand 36 | # Get a new car detail from conf 37 | car_details = api_example_conf.car_details 38 | 39 | async with asyncio.TaskGroup() as group: 40 | get_cars = group.create_task(test_api_obj.async_get_cars(auth_details)) 41 | add_new_car = group.create_task(test_api_obj.async_add_car(car_details=car_details, 42 | auth_details=auth_details)) 43 | get_car = group.create_task(test_api_obj.async_get_car(auth_details=auth_details, 44 | car_name=existing_car, 45 | brand=brand)) 46 | get_reg_cars = group.create_task(test_api_obj.async_get_registered_cars(auth_details=auth_details)) 47 | 48 | test_api_obj.log_result(get_cars.result(), 49 | positive="Successfully obtained the list of cars", 50 | negative="Failed to get the cars") 51 | test_api_obj.log_result(add_new_car.result(), 52 | positive=f"Successfully added new car {car_details}", 53 | negative="Failed to add a new car") 54 | test_api_obj.log_result(get_car.result(), 55 | positive=f"Successfully obtained a car - {existing_car}", 56 | negative="Failed to add a new car") 57 | test_api_obj.log_result(get_reg_cars.result(), 58 | positive="Successfully obtained registered cars", 59 | negative="Failed to get registered cars") 60 | # write out test summary 61 | expected_pass = test_api_obj.total 62 | actual_pass = test_api_obj.passed 63 | test_api_obj.write_test_summary() 64 | # Assertion 65 | assert expected_pass == actual_pass,f"Test failed: {__file__}" 66 | 67 | except Exception as err: 68 | raise err 69 | -------------------------------------------------------------------------------- /tests/test_api_endpoint_auto_generation.py: -------------------------------------------------------------------------------- 1 | """ 2 | API Endpoint Auto generation test validates: 3 | 1. Endpoint Module name generation from an OpenAPI spec 4 | 2. Endpoint class name generation from the spec 5 | 3. URL method name generation for the Endpoint class from the spec 6 | 4. Base API method call string generation for Jinja template 7 | 5. Endoint object instance method call string generation for Jinja template 8 | 6. Values obtained for an endpoint after parsing the spec 9 | 7. JSON params for an endpoint obtained after parsing the spec 10 | 8. Query params for an endpoint after parsing the spec 11 | 9. Path params for an endpoint after parsing the spec 12 | """ 13 | 14 | import pytest 15 | import requests 16 | 17 | @pytest.mark.API_AUTO_GEN_UNIT 18 | def test_module_name_generation(name_generator): 19 | "Validate module name generation from spec" 20 | assert name_generator.module_name == "cars_endpoint" 21 | 22 | @pytest.mark.API_AUTO_GEN_UNIT 23 | def test_class_name_generation(name_generator): 24 | "Validate class name generation from spec" 25 | assert name_generator.class_name == "CarsEndpoint" 26 | 27 | @pytest.mark.API_AUTO_GEN_UNIT 28 | def test_url_method_name(name_generator): 29 | "Validate URL method name generation from spec" 30 | assert name_generator.url_method_name == "cars_url" 31 | 32 | @pytest.mark.API_AUTO_GEN_UNIT 33 | def test_base_api_param_string(name_generator): 34 | "Validate Base_API method call generation string" 35 | assert name_generator.base_api_param_string == ", headers=headers" 36 | 37 | @pytest.mark.API_AUTO_GEN_UNIT 38 | def test_instance_method_param_string(name_generator): 39 | "Validate Endpoint method call generation string" 40 | assert name_generator.instance_method_param_string == "self, name, headers" 41 | 42 | @pytest.mark.API_AUTO_GEN_UNIT 43 | def test_parsed_spec_get_method(parsed_spec, requests_mock): 44 | "Validate OpenAPI spec parsed values" 45 | http_method_in_test = parsed_spec[0]["get_cars"] 46 | endpoint = http_method_in_test["endpoint"] 47 | http_method = http_method_in_test["http_method"] 48 | requests_mock.get("http://stub-cars-api/cars", status_code=200) 49 | # Validate that the endpoint value was rightly parsed from the spec 50 | assert requests.request(method=http_method, url=f"http://stub-cars-api{endpoint}").status_code == 200 51 | 52 | @pytest.mark.API_AUTO_GEN_UNIT 53 | def test_parsed_spec_post_method(parsed_spec, requests_mock): 54 | "Validate OpenAPI spec parsed values for json params" 55 | http_method_in_test = parsed_spec[1]["post_cars_add"] 56 | endpoint = http_method_in_test["endpoint"] 57 | http_method = http_method_in_test["http_method"] 58 | json_keys = http_method_in_test["params"]["json_params"] 59 | json_content = {} 60 | for key in json_keys: 61 | json_content[key[0]] = "random_string" 62 | requests_mock.post("http://stub-cars-api/cars/add", json=json_content) 63 | json_response = requests.request(method=http_method, url=f"http://stub-cars-api{endpoint}").json() 64 | # Validate that the JSON keys were rightly parsed for the endpoint 65 | assert json_response.get("name") 66 | assert json_response.get("brand") 67 | assert json_response.get("price_range") 68 | assert json_response.get("car_type") 69 | 70 | @pytest.mark.API_AUTO_GEN_UNIT 71 | def test_parsed_spec_query_params(parsed_spec, requests_mock): 72 | "Validate OpenAPI spec parsed values for query params" 73 | http_method_in_test = parsed_spec[2]["get_cars_find"] 74 | endpoint = http_method_in_test["endpoint"] 75 | http_method = http_method_in_test["http_method"] 76 | query_keys = http_method_in_test["params"]["query_params"] 77 | query_params = {} 78 | for key in query_keys: 79 | query_params[key[0]] = "random_string" 80 | requests_mock.get(f"http://stub-cars-api/cars/find?name={query_params['name']}&brand={query_params['brand']}", status_code=200) 81 | response = requests.request(method=http_method, url=f"http://stub-cars-api{endpoint}", params=query_params) 82 | # Validat that the Query params were rightly parsed from the spec 83 | assert response.status_code==200 84 | 85 | @pytest.mark.API_AUTO_GEN_UNIT 86 | def test_parsed_spec_path_param(parsed_spec, requests_mock): 87 | "Validate OpenAPI spec parsed values for path params" 88 | http_method_in_test = parsed_spec[3]["get_cars_name"] 89 | http_method = http_method_in_test["http_method"] 90 | path_keys = http_method_in_test["params"]["path_params"][0] 91 | path_param = {path_keys[0]: "foo"} 92 | requests_mock.get("http://stub-cars-api/cars/foo", status_code=200) 93 | response = requests.request(method=http_method, url=f"http://stub-cars-api/cars/{path_param['name']}") 94 | # Validate that the path params were rightly parsed from the spec 95 | assert response.status_code==200 96 | -------------------------------------------------------------------------------- /tests/test_boilerplate.py: -------------------------------------------------------------------------------- 1 | """ 2 | This test file will help you get started in writing a new test using our framework 3 | """ 4 | import os,sys 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 6 | from page_objects.PageFactory import PageFactory 7 | import pytest 8 | 9 | 10 | @pytest.mark.GUI 11 | def test_boilerplate(test_obj): 12 | 13 | "Run the test" 14 | try: 15 | #Initalize flags for tests summary 16 | expected_pass = 0 17 | actual_pass = -1 18 | 19 | #This is the test object, you can change it to the desired page with relevance to the page factory 20 | test_obj = PageFactory.get_page_object("zero", base_url=test_obj.base_url) 21 | 22 | #Print out the result 23 | test_obj.write_test_summary() 24 | expected_pass = test_obj.result_counter 25 | actual_pass = test_obj.pass_counter 26 | 27 | except Exception as e: 28 | print("Exception when trying to run test: %s"%__file__) 29 | print("Python says:%s"%str(e)) 30 | 31 | assert expected_pass == actual_pass, "Test failed: %s"%__file__ 32 | -------------------------------------------------------------------------------- /tests/test_example_table.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example automated test to help you learn Qxf2's framework 3 | Our automated test will do the following: 4 | #Open Qxf2 selenium-tutorial-main page. 5 | #Print out the entire table 6 | #Verify if a certain name is present in the table 7 | """ 8 | 9 | #The import statements import: standard Python modules,conf,credential files 10 | import os,sys,time 11 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | from page_objects.PageFactory import PageFactory 13 | import conf.example_table_conf as conf 14 | import pytest 15 | 16 | 17 | @pytest.mark.GUI 18 | def test_example_table(test_obj): 19 | 20 | "Run the test" 21 | try: 22 | #Initalize flags for tests summary 23 | expected_pass = 0 24 | actual_pass = -1 25 | 26 | #1. Create a example table page object 27 | test_obj = PageFactory.get_page_object("Main Page", base_url=test_obj.base_url) 28 | 29 | #Set start_time with current time 30 | start_time = int(time.time()) 31 | 32 | #2. Get the test details from the conf file 33 | name = conf.name 34 | 35 | #3. Print out table text neatly 36 | result_flag = test_obj.print_table_text() 37 | test_obj.log_result(result_flag, 38 | positive="Completed printing table text", 39 | negative="Unable to print the table text") 40 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 41 | 42 | #4. Check if a name is present in the table 43 | result_flag = test_obj.check_name_present(name) 44 | test_obj.log_result(result_flag, 45 | positive="Located the name %s in the table"%name, 46 | negative="The name %s is not present under name column on the Page with url: %s"%(name,test_obj.get_current_url())) 47 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 48 | test_obj.add_tesults_case("Example table", "Verify if a certain name is present in the table", "test_example_table", result_flag,"\nFailed to Verify if a certain name is present in the table\n") 49 | 50 | #5. Print out the result 51 | test_obj.write_test_summary() 52 | expected_pass = test_obj.result_counter 53 | actual_pass = test_obj.pass_counter 54 | 55 | except Exception as e: 56 | print("Exception when trying to run test: %s"%__file__) 57 | print("Python says:%s"%str(e)) 58 | 59 | assert expected_pass == actual_pass, "Test failed: %s"%__file__ 60 | -------------------------------------------------------------------------------- /tests/test_mobile_bitcoin_price.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automated test will do the following: 3 | # Open Bitcoin Info application in emulator. 4 | # Click on the bitcoin real time price page button. 5 | # Compare expected bitcoin real time price page heading with current page heading. 6 | # Verify that the bitcoin real time price is displayed on the page. 7 | # Display the results. 8 | """ 9 | import os, sys, time 10 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 11 | from page_objects.PageFactory import PageFactory 12 | import conf.mobile_bitcoin_conf as conf 13 | import pytest 14 | 15 | @pytest.mark.MOBILE 16 | def test_mobile_bitcoin_price(test_mobile_obj): 17 | "Run the test." 18 | try: 19 | # Initalize flags for tests summary. 20 | expected_pass = 0 21 | actual_pass = -1 22 | 23 | #1. Create a test object. 24 | test_obj = PageFactory.get_page_object("bitcoin main page") 25 | 26 | #2. Setup and register a driver 27 | start_time = int(time.time()) 28 | 29 | #3. Get expected bitcoin price page header name 30 | expected_bitcoin_price_page_heading = conf.expected_bitcoin_price_page_heading 31 | 32 | #4. Click on real time price page button and verify the price page header name. 33 | result_flag = test_obj.click_on_real_time_price_button(expected_bitcoin_price_page_heading) 34 | test_obj.log_result(result_flag, 35 | positive="Successfully visited the bitcoin real time price page.", 36 | negative="Failed to visit the bitcoin real time price page.") 37 | 38 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 39 | 40 | #5. Verify bitcoin real time price is displayed. 41 | if result_flag is True: 42 | result_flag = test_obj.get_bitcoin_real_time_price() 43 | test_obj.log_result(result_flag, 44 | positive="Successfully got the bitcoin real time price in usd.", 45 | negative="Failed to get the bitcoin real time price in usd.") 46 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 47 | 48 | #6. Print out the results. 49 | test_obj.write_test_summary() 50 | 51 | #7. Teardown and Assertion. 52 | expected_pass = test_obj.result_counter 53 | actual_pass = test_obj.pass_counter 54 | 55 | except Exception as e: 56 | print("Exception when trying to run test:%s" % __file__) 57 | print("Python says:%s" % str(e)) 58 | 59 | assert expected_pass == actual_pass,"Test failed: %s"%__file__ -------------------------------------------------------------------------------- /tests/test_successive_form_creation.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example automated test to help you learn Qxf2's framework 3 | Our automated test will do the following action repeatedly to fill number of forms: 4 | #Open Qxf2 selenium-tutorial-main page. 5 | #Fill the example form 6 | #Click on Click me! button and check if its working fine 7 | """ 8 | #The import statements import: standard Python modules,conf,credential files 9 | import os,sys,time 10 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 11 | from page_objects.PageFactory import PageFactory 12 | import conf.successive_form_creation_conf as conf 13 | import pytest 14 | @pytest.mark.GUI 15 | 16 | def test_succesive_form_creation(test_obj): 17 | "Run the test" 18 | try: 19 | #Initalize flags for tests summary 20 | expected_pass = 0 21 | actual_pass = -1 22 | 23 | #1. Create a test object and fill the example form. 24 | test_obj = PageFactory.get_page_object("Main Page", base_url=test_obj.base_url) 25 | 26 | #Set start_time with current time 27 | start_time = int(time.time()) 28 | 29 | #2. Get the test details from the conf file and fill the forms 30 | form_list = conf.form_list 31 | form_number = 1 #Initalize form counter 32 | 33 | #3.Collect form data 34 | for form in form_list: 35 | name = form['NAME'] 36 | email = form['EMAIL'] 37 | phone = form['PHONE_NO'] 38 | gender = form['GENDER'] 39 | 40 | msg ="\nReady to fill form number %d"%form_number 41 | test_obj.write(msg) 42 | 43 | #a. Set and submit the form in one go 44 | result_flag = test_obj.submit_form(name,email,phone,gender) 45 | test_obj.log_result(result_flag, 46 | positive="Successfully submitted the form number %d\n"%form_number, 47 | negative="Failed to submit the form number %d \nOn url: %s"%(form_number,test_obj.get_current_url()), 48 | level="critical") 49 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 50 | test_obj.add_tesults_case("Set and submit form " + str(form_number), "Sets and submits the form in one go", "test_successive_form_creation", result_flag, "Failed to submit the form number %d \nOn url: %s"%(form_number,test_obj.get_current_url()), [test_obj.log_obj.log_file_dir + os.sep + test_obj.log_obj.log_file_name]) 51 | 52 | #b. Check the heading on the redirect page 53 | #Notice you don't need to create a new page object! 54 | if result_flag is True: 55 | result_flag = test_obj.check_heading() 56 | test_obj.log_result(result_flag, 57 | positive="Heading on the redirect page checks out!\n", 58 | negative="Fail: Heading on the redirect page is incorrect!") 59 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 60 | test_obj.add_tesults_case("Check redirect heading " + str(form_number), "Check the heading on the redirect page", "test_successive_form_creation", result_flag, "Fail: Heading on the redirect page is incorrect!", []) 61 | 62 | #c. Check the copyright 63 | result_flag = test_obj.check_copyright() 64 | test_obj.log_result(result_flag, 65 | positive="Copyright check was successful\n", 66 | negative="Copyright looks wrong.\nObtained the copyright: %s\n"%test_obj.get_copyright()) 67 | test_obj.write('Script duration: %d seconds\n'%(int(time.time()-start_time))) 68 | test_obj.add_tesults_case("Check copyright " + str(form_number), "Check the copyright", "test_successive_form_creation", result_flag, "Copyright looks wrong.\nObtained the copyright: %s\n"%test_obj.get_copyright(), []) 69 | 70 | #d. Visit main page again 71 | test_obj = PageFactory.get_page_object("Main Page", base_url=test_obj.base_url) 72 | form_number = form_number + 1 73 | 74 | #4.Print out the results 75 | test_obj.write_test_summary() 76 | expected_pass = test_obj.result_counter 77 | actual_pass = test_obj.pass_counter 78 | 79 | except Exception as e: 80 | print("Exception when trying to run test :%s"%__file__) 81 | print("Python says:%s"%str(e)) 82 | 83 | assert expected_pass == actual_pass ,"Test failed: %s"%__file__ 84 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = true 3 | 4 | 5 | [testenv] 6 | 7 | #Setting the dependency file 8 | deps = -r{toxinidir}/requirements.txt 9 | 10 | #used to not trigger the “not installed in virtualenv” warning message 11 | whitelist_externals=* 12 | 13 | #setting the environment 14 | setenv= app_path= {toxinidir}/weather-shopper-app-apk/app/ 15 | 16 | #Command to run the test 17 | commands = python -m pytest -s -v --app_path {env:app_path} --remote_flag Y -n 3 --remote_project_name Qxf2_Selenium_POM --remote_build_name Selenium_Tutorial --junitxml=test-reports/junit.xml --tb=native --ignore=tests/test_mobile_bitcoin_price.py -------------------------------------------------------------------------------- /utils/Image_Compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare images 3 | * Compare two images(actual and expected) smartly and generate a resultant image 4 | * Get the sum of colors in an image 5 | """ 6 | from PIL import Image, ImageChops 7 | import math, os 8 | 9 | def rmsdiff(im1,im2): 10 | "Calculate the root-mean-square difference between two images" 11 | 12 | h = ImageChops.difference(im1, im2).histogram() 13 | # calculate rms 14 | return math.sqrt(sum(h*(i**2) for i, h in enumerate(h)) / (float(im1.size[0]) * im1.size[1])) 15 | 16 | 17 | def is_equal(img_actual,img_expected,result): 18 | "Returns true if the images are identical(all pixels in the difference image are zero)" 19 | result_flag = False 20 | 21 | if not os.path.exists(img_actual): 22 | print('Could not locate the generated image: %s'%img_actual) 23 | 24 | if not os.path.exists(img_expected): 25 | print('Could not locate the baseline image: %s'%img_expected) 26 | 27 | if os.path.exists(img_actual) and os.path.exists(img_expected): 28 | actual = Image.open(img_actual) 29 | expected = Image.open(img_expected) 30 | result_image = ImageChops.difference(actual,expected) 31 | color_matrix = ([0] + ([255] * 255)) 32 | result_image = result_image.convert('L') 33 | result_image = result_image.point(color_matrix) 34 | result_image.save(result)#Save the result image 35 | 36 | if (ImageChops.difference(actual,expected).getbbox() is None): 37 | result_flag = True 38 | else: 39 | #Let's do some interesting processing now 40 | result_flag = analyze_difference_smartly(result) 41 | if result_flag is False: 42 | print("Since there is a difference in pixel value of both images, we are checking the threshold value to pass the images with minor difference") 43 | #Now with threshhold! 44 | result_flag = True if rmsdiff(actual,expected) < 958 else False 45 | #For temporary debug purposes 46 | print('RMS diff score: ',rmsdiff(actual,expected)) 47 | 48 | return result_flag 49 | 50 | 51 | def analyze_difference_smartly(img): 52 | "Make an evaluation of a difference image" 53 | result_flag = False 54 | if not os.path.exists(img): 55 | print('Could not locate the image to analyze the difference smartly: %s'%img) 56 | else: 57 | my_image = Image.open(img) 58 | #Not an ideal line, but we dont have any enormous images 59 | pixels = list(my_image.getdata()) 60 | pixels = [1 for x in pixels if x!=0] 61 | num_different_pixels = sum(pixels) 62 | print('Number of different pixels in the result image: %d'%num_different_pixels) 63 | #Rule 1: If the number of different pixels is <10, then pass the image 64 | #This is relatively safe since all changes to objects will be more than 10 different pixels 65 | if num_different_pixels < 10: 66 | result_flag = True 67 | 68 | return result_flag 69 | 70 | 71 | def get_color_sum(img): 72 | "Get the sum of colors in an image" 73 | sum_color_pixels = -1 74 | if not os.path.exists(img): 75 | print('Could not locate the image to sum the colors: %s'%actual) 76 | else: 77 | my_image = Image.open(img) 78 | color_matrix = ([0] + ([255] * 255)) 79 | my_image = my_image.convert('L') 80 | my_image = my_image.point(color_matrix) 81 | #Not an ideal line, but we don't have any enormous images 82 | pixels = list(my_image.getdata()) 83 | sum_color_pixels = sum(pixels) 84 | print('Sum of colors in the image %s is %d'%(img,sum_color_pixels)) 85 | 86 | return sum_color_pixels 87 | 88 | 89 | #--START OF SCRIPT 90 | if __name__=='__main__': 91 | # Please update below img1, img2, result_img values before running this script 92 | img1 = r'Add path of first image' 93 | img2 = r'Add path of second image' 94 | result_img= r'Add path of result image' #please add path along with resultant image name which you want 95 | 96 | # Compare images and generate a resultant difference image 97 | result_flag = is_equal(img1,img2,result_img) 98 | if (result_flag == True): 99 | print("Both images are matching") 100 | else: 101 | print("Images are not matching") 102 | 103 | # Get the sum of colors in an image 104 | get_color_sum(img1) 105 | -------------------------------------------------------------------------------- /utils/Wrapit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class to hold miscellaneous but useful decorators for our framework 3 | """ 4 | 5 | from inspect import getfullargspec 6 | import traceback 7 | 8 | 9 | class Wrapit(): 10 | 11 | "Wrapit class to hold decorator functions" 12 | def _exceptionHandler(f): 13 | "Decorator to handle exceptions" 14 | def inner(*args,**kwargs): 15 | try: 16 | return f(*args,**kwargs) 17 | except Exception as e: 18 | #args[0].write('You have this exception', level='error') 19 | trace = traceback.format_exc(limit=-1) 20 | # Create a message with the traceback details 21 | message = f"You have this exception: {str(e)}\n{trace}" 22 | args[0].write(message, level='error', trace_back=trace) 23 | 24 | #traceback.print_exc(limit=-1) 25 | #we denote None as failure case 26 | return None 27 | 28 | return inner 29 | 30 | 31 | def _screenshot(func): 32 | "Decorator for taking screenshots" 33 | #Usage: Make this the first decorator to a method (right above the 'def function_name' line) 34 | #Otherwise, we cannot name the screenshot with the name of the function that called it 35 | def wrapper(*args,**kwargs): 36 | result = func(*args,**kwargs) 37 | screenshot_name = '%003d'%args[0].screenshot_counter + '_' + func.__name__ 38 | args[0].screenshot_counter += 1 39 | args[0].save_screenshot(screenshot_name) 40 | 41 | return result 42 | 43 | return wrapper 44 | 45 | 46 | def _check_browser_console_log(func): 47 | "Decorator to check the browser's console log for errors" 48 | def wrapper(*args,**kwargs): 49 | #As IE driver does not support retrieval of any logs, 50 | #we are bypassing the read_browser_console_log() method 51 | result = func(*args, **kwargs) 52 | if "ie" not in str(args[0].driver): 53 | result = func(*args, **kwargs) 54 | log_errors = [] 55 | new_errors = [] 56 | log = args[0].read_browser_console_log() 57 | if log != None: 58 | for entry in log: 59 | if entry['level']=='SEVERE': 60 | log_errors.append(entry['message']) 61 | 62 | if args[0].current_console_log_errors != log_errors: 63 | #Find the difference 64 | new_errors = list(set(log_errors) - set(args[0].current_console_log_errors)) 65 | #Set current_console_log_errors = log_errors 66 | args[0].current_console_log_errors = log_errors 67 | 68 | if len(new_errors)>0: 69 | args[0].failure("\nBrowser console error on url: %s\nMethod: %s\nConsole error(s):%s"%(args[0].get_current_url(),func.__name__,'\n----'.join(new_errors))) 70 | 71 | return result 72 | 73 | return wrapper 74 | 75 | 76 | _exceptionHandler = staticmethod(_exceptionHandler) 77 | _screenshot = staticmethod(_screenshot) 78 | _check_browser_console_log = staticmethod(_check_browser_console_log) 79 | 80 | 81 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | "Check if dict item exist for given key else return none" 3 | 4 | def get_dict_item(from_this, get_this): 5 | """ get dic object item """ 6 | if not from_this: 7 | return None 8 | item = from_this 9 | if isinstance(get_this, str): 10 | if get_this in from_this: 11 | item = from_this[get_this] 12 | else: 13 | item = None 14 | else: 15 | for key in get_this: 16 | if isinstance(item, dict) and key in item: 17 | item = item[key] 18 | else: 19 | return None 20 | return item 21 | -------------------------------------------------------------------------------- /utils/accessibility_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Accessibility Integration 3 | * This is a class which extends the methods of Axe parent class 4 | """ 5 | 6 | import os 7 | from axe_selenium_python import Axe 8 | 9 | script_url=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "utils", "axe.min.js")) 10 | 11 | class Accessibilityutil(Axe): 12 | "Accessibility object to run accessibility test" 13 | def __init__(self, driver): 14 | super().__init__(driver, script_url) 15 | -------------------------------------------------------------------------------- /utils/clean_up_repo.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Qxf2 automation repository ships with example tests. 3 | Run this file to delete all the example files and start fresh with your example. 4 | Usage: python clean_up_repo.py 5 | """ 6 | import os 7 | import sys 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | from conf import clean_up_repo_conf as conf 10 | from utils.Base_Logging import Base_Logging 11 | 12 | class CleanUpRepo: 13 | """Utility for cleaning up example files.""" 14 | def __init__(self): 15 | """Initializes the CleanUpRepo class with a logger""" 16 | self.logger = Base_Logging(log_file_name="clean_up_repo.log", level="INFO") 17 | 18 | def delete_file(self, file_name): 19 | """The method will delete a particular file""" 20 | if os.path.exists(file_name): 21 | os.remove(file_name) 22 | self.logger.write(f'{file_name} deleted') 23 | 24 | def delete_directory(self, dir_name): 25 | """The method will delete a particular directory along with its content""" 26 | import shutil # pylint: disable=import-error,import-outside-toplevel 27 | if os.path.exists(dir_name) and os.path.isdir(dir_name): 28 | shutil.rmtree(dir_name) 29 | self.logger.write(f'{dir_name} deleted') 30 | 31 | def delete_files_in_dir(self, directory, files): 32 | """The method will delete files in a particular directory""" 33 | for file_name in files: 34 | self.delete_file(os.path.join(directory, file_name)) 35 | 36 | def delete_files_used_in_example(self): 37 | """The method will delete a set of files""" 38 | for every_dir_list, every_file_list in zip(conf.dir_list, conf.file_list): 39 | self.delete_files_in_dir(every_dir_list, every_file_list) 40 | 41 | def run_cleanup(self): 42 | """Runs the utility to delete example files and logs the operation.""" 43 | self.logger.write("Running utility to delete the files") 44 | self.delete_directory(conf.PAGE_OBJECTS_EXAMPLES_DIR) 45 | self.delete_files_used_in_example() 46 | self.logger.write( 47 | f'All the files related to the sample example from Page Object Model have been removed from {conf.dir_list} folders.\n' 48 | 'For next steps, please refer to the edit files section of this blog post: ' 49 | 'https://qxf2.com/blog/how-to-start-using-the-qxf2-framework-with-a-new-project/' 50 | ) 51 | 52 | if __name__ == "__main__": 53 | cleanup = CleanUpRepo() 54 | cleanup.run_cleanup() 55 | -------------------------------------------------------------------------------- /utils/csv_compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare two csv files. 3 | 4 | """ 5 | import csv,os 6 | 7 | class Csv_Compare(): 8 | def is_equal(self,csv_actual,csv_expected): 9 | "Method to compare the Actual and Expected csv file" 10 | result_flag = True 11 | 12 | if not os.path.exists(csv_actual): 13 | result_flag = False 14 | print('Could not locate the csv file: %s'%csv_actual) 15 | 16 | if not os.path.exists(csv_expected): 17 | result_flag = False 18 | print('Could not locate the csv file: %s'%csv_expected) 19 | 20 | if os.path.exists(csv_actual) and os.path.exists(csv_expected): 21 | #Open the csv file and put the content to list 22 | with open(csv_actual, 'r') as actual_csvfile, open(csv_expected, 'r') as exp_csvfile: 23 | reader = csv.reader(actual_csvfile) 24 | actual_file = [row for row in reader] 25 | reader = csv.reader(exp_csvfile) 26 | exp_file = [row for row in reader] 27 | 28 | if (len(actual_file)!= len(exp_file)): 29 | result_flag = False 30 | print("Mismatch in number of rows. The actual row count didn't match with expected row count") 31 | else: 32 | for actual_row, actual_col in zip(actual_file,exp_file): 33 | if actual_row == actual_col: 34 | pass 35 | else: 36 | print("Mismatch between actual and expected file at Row: ",actual_file.index(actual_row)) 37 | print("Row present only in Actual file: %s"%actual_row) 38 | print("Row present only in Expected file: %s"%actual_col) 39 | result_flag = False 40 | 41 | return result_flag 42 | 43 | 44 | #---USAGE EXAMPLES 45 | if __name__=='__main__': 46 | print("Start of %s"%__file__) 47 | 48 | #Fill in the file1 and file2 paths 49 | file1 = 'Add path for the first file here' 50 | file2 = 'Add path for the second file here' 51 | 52 | #Initialize the csv object 53 | csv_obj = Csv_Compare() 54 | 55 | #Sample code to compare csv files 56 | if csv_obj.is_equal(file1,file2) is True: 57 | print("Data matched in both the csv files\n") 58 | else: 59 | print("Data mismatch between the actual and expected csv files") 60 | -------------------------------------------------------------------------------- /utils/excel_compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qxf2 Services: Utility script to compare two excel files using openxl module 3 | 4 | """ 5 | import openpyxl 6 | import os 7 | 8 | class Excel_Compare(): 9 | def is_equal(self,xl_actual,xl_expected): 10 | "Method to compare the Actual and Expected xl file" 11 | result_flag = True 12 | if not os.path.exists(xl_actual): 13 | result_flag = False 14 | print('Could not locate the excel file: %s'%xl_actual) 15 | 16 | if not os.path.exists(xl_expected): 17 | result_flag = False 18 | print('Could not locate the excel file %s'%xl_expected) 19 | 20 | if os.path.exists(xl_actual) and os.path.exists(xl_expected): 21 | #Open the xl file and put the content to list 22 | actual_xlfile = openpyxl.load_workbook(xl_actual) 23 | xl_sheet = actual_xlfile.active 24 | actual_file = [] 25 | for row in xl_sheet.iter_rows(min_row=1, max_col=xl_sheet.max_column, max_row=xl_sheet.max_row): 26 | for cell in row: 27 | actual_file.append(cell.value) 28 | 29 | exp_xlfile = openpyxl.load_workbook(xl_expected) 30 | xl_sheet = exp_xlfile.active 31 | exp_file = [] 32 | for row in xl_sheet.iter_rows(min_row=1, max_col=xl_sheet.max_column, max_row=xl_sheet.max_row): 33 | for cell in row: 34 | exp_file.append(cell.value) 35 | 36 | #If there is row and column mismatch result_flag = False 37 | if (len(actual_file)!= len(exp_file)): 38 | result_flag = False 39 | print("Mismatch in number of rows or columns. The actual row or column count didn't match with expected row or column count") 40 | else: 41 | for actual_row, actual_col in zip(actual_file,exp_file): 42 | if actual_row == actual_col: 43 | pass 44 | else: 45 | print("Mismatch between actual and expected file at position(each row consists of 23 coordinates):",actual_file.index(actual_row)) 46 | print("Data present only in Actual file: %s"%actual_row) 47 | print("Data present only in Expected file: %s"%actual_col) 48 | result_flag = False 49 | 50 | return result_flag 51 | 52 | 53 | #---USAGE EXAMPLES 54 | if __name__=='__main__': 55 | print("Start of %s"%__file__) 56 | # Enter the path details of the xl files here 57 | file1 = 'Add path to the first xl file' 58 | file2 = 'Add path to the second xl file' 59 | 60 | #Initialize the excel object 61 | xl_obj = Excel_Compare() 62 | 63 | #Sample code to compare excel files 64 | if xl_obj.is_equal(file1,file2) is True: 65 | print("Data matched in both the excel files\n") 66 | else: 67 | print("Data mismatch between the actual and expected excel files") -------------------------------------------------------------------------------- /utils/qxf2_pom_essentials_dockerfiles/Readme.md: -------------------------------------------------------------------------------- 1 | # 🐳 Qxf2 Page Object Model (POM) Docker Setup with VNC Access 2 | 3 | This Docker image sets up a Selenium testing environment with: 4 | 5 | - Google Chrome 6 | - Firefox 7 | - XVFB (virtual display) 8 | - x11vnc (VNC access to the GUI) 9 | - Python 3.12 and virtual environment 10 | - Sample tests to run selenium tests on Chrome and Firefox 11 | 12 | ## Prerequisites 13 | 14 | - Docker installed 15 | - A VNC Viewer (e.g., TigerVNC, RealVNC, Remmina) 16 | 17 | --- 18 | 19 | ## Build the Docker Image 20 | 21 | Run the following command in the directory where your `Dockerfile` is located: 22 | 23 | ``` 24 | docker build -t qxf2-pom-essentials:python3.12 . 25 | ``` 26 | --- 27 | 28 | ## Pull image from Docker Hub: 29 | You can also skip the build step and pull the pre-built image directly: 30 | ``` 31 | docker pull qxf2rohand/qxf2-pom-essentials:python3.12 32 | ``` 33 | View image at https://hub.docker.com/r/qxf2rohand/qxf2_pom_essentials 34 | 35 | --- 36 | ## Run the Docker Container 37 | ``` 38 | docker run -it -p 5999:5999 qxf2-pom-essentials:python3.12 /bin/bash 39 | ``` 40 | Port 5999 exposed for VNC access. 41 | 42 | --- 43 | 44 | ## Connect via VNC 45 | 1. Open your VNC Viewer 46 | 47 | 2. Connect to: 48 | ```vncviewer localhost:99``` 49 | 50 | You’ll now see the browser UI inside your container. 51 | 52 | --- 53 | 54 | ## Run the Sample Test 55 | Inside the Docker container terminal: 56 | ``` 57 | python sample_test_chrome.py 58 | ``` 59 | You’ll now see VNC viewer, it opens the chrome browser and navigates to https://www.qxf2.com and prints title. 60 | 61 | --- 62 | 63 | ## Note: 64 | You can connect complete framework as a volume to container, install requirements, edit and run your tests. 65 | 66 | --- 67 | -------------------------------------------------------------------------------- /utils/qxf2_pom_essentials_dockerfiles/dockerfile: -------------------------------------------------------------------------------- 1 | # Use the latest Ubuntu version explicitly (current version 24.04) 2 | FROM ubuntu:latest 3 | LABEL maintainer="Qxf2 Services" 4 | 5 | # Set non-interactive mode for apt-get 6 | ENV DEBIAN_FRONTEND=noninteractive 7 | 8 | # Update package lists and install required dependencies 9 | RUN apt-get update && apt-get install -y --no-install-recommends \ 10 | wget \ 11 | ca-certificates \ 12 | unzip \ 13 | xvfb \ 14 | x11vnc \ 15 | x11-xkb-utils \ 16 | xkb-data \ 17 | python3.12 \ 18 | python3.12-venv \ 19 | python3.12-dev \ 20 | python3-pip \ 21 | vim-tiny \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | # Install Google Chrome and Firefox(latest stable) 25 | RUN wget -q -O /tmp/google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ 26 | && apt-get update \ 27 | && apt-get install -y /tmp/google-chrome.deb \ 28 | && rm /tmp/google-chrome.deb \ 29 | && apt-get update && apt-get install -y firefox \ 30 | && rm -rf /var/lib/apt/lists/* 31 | 32 | # Create a virtual environment and install Selenium (as the non-root user) 33 | RUN python3 -m venv /home/venv \ 34 | && /home/venv/bin/pip install --no-cache-dir selenium 35 | 36 | # Set environment variables 37 | ENV PATH="/home/venv/bin:$PATH" 38 | ENV DISPLAY=:99 39 | 40 | # Add files to qxf2_pom directory 41 | ADD . /qxf2_pom 42 | 43 | # Set working directory 44 | WORKDIR /qxf2_pom 45 | 46 | #Expose port for VNC 47 | EXPOSE 5999 48 | 49 | # Make entrypoint.sh executable and set it to run at start 50 | RUN chmod +x /qxf2_pom/entrypoint.sh 51 | ENTRYPOINT ["/qxf2_pom/entrypoint.sh"] 52 | CMD ["/bin/bash"] 53 | -------------------------------------------------------------------------------- /utils/qxf2_pom_essentials_dockerfiles/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DISPLAY=:99 3 | Xvfb :99 -screen 0 1366x768x16 2>/dev/null & 4 | 5 | # Start x11vnc 6 | x11vnc -display :99 -forever -nopw -quiet -rfbport 5999 & 7 | 8 | # Run CMD command 9 | exec "$@" -------------------------------------------------------------------------------- /utils/qxf2_pom_essentials_dockerfiles/sample_test_chrome.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | 3 | options = webdriver.ChromeOptions() 4 | options.add_argument("--no-sandbox") # Often needed in Docker/headless environments 5 | options.add_argument("--disable-dev-shm-usage") 6 | #options.add_argument("--headless") # Don't set it if you want to see test running on VNC Viewer 7 | driver = webdriver.Chrome(options=options) 8 | driver.get("http://www.qxf2.com") 9 | print(driver.title) 10 | driver.quit() 11 | -------------------------------------------------------------------------------- /utils/qxf2_pom_essentials_dockerfiles/sample_test_ff.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | 3 | driver = webdriver.Firefox() 4 | driver.get("http://www.qxf2.com") 5 | print(driver.title) 6 | driver.quit() -------------------------------------------------------------------------------- /utils/results.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tracks test results and logs them. 3 | Keeps counters of pass/fail/total. 4 | """ 5 | import logging 6 | from utils.Base_Logging import Base_Logging 7 | 8 | 9 | class Results(object): 10 | """ Base class for logging intermediate test outcomes """ 11 | 12 | def __init__(self, level=logging.DEBUG, log_file_path=None): 13 | self.logger = Base_Logging(log_file_name=log_file_path, level=level) 14 | self.total = 0 # Increment whenever success or failure are called 15 | self.passed = 0 # Increment everytime success is called 16 | self.written = 0 # Increment when conditional_write is called 17 | # Increment when conditional_write is called with True 18 | self.written_passed = 0 19 | self.failure_message_list = [] 20 | 21 | 22 | def assert_results(self): 23 | """ Check if the test passed or failed """ 24 | assert self.passed == self.total 25 | 26 | 27 | def write(self, msg, level='info'): 28 | """ This method use the logging method """ 29 | self.logger.write(msg, level) 30 | 31 | 32 | def conditional_write(self, condition, positive, negative, level='info', pre_format=" - "): 33 | """ Write out either the positive or the negative message based on flag """ 34 | if condition: 35 | self.write(pre_format + positive, level) 36 | self.written_passed += 1 37 | else: 38 | self.write(pre_format + negative, level) 39 | self.written += 1 40 | 41 | 42 | def log_result(self, flag, positive, negative, level='info'): 43 | """ Write out the result of the test """ 44 | if flag is True: 45 | self.success(positive, level=level) 46 | if flag is False: 47 | self.failure(negative, level=level) 48 | raise Exception 49 | self.write('~~~~~~~~\n', level) 50 | 51 | 52 | def success(self, msg, level='info', pre_format='PASS: '): 53 | """ Write out a success message """ 54 | self.logger.write(pre_format + msg, level) 55 | self.total += 1 56 | self.passed += 1 57 | 58 | 59 | def failure(self, msg, level='info', pre_format='FAIL: '): 60 | """ Write out a failure message """ 61 | self.logger.write(pre_format + msg, level) 62 | self.total += 1 63 | self.failure_message_list.append(pre_format + msg) 64 | 65 | 66 | def get_failure_message_list(self): 67 | """ Return the failure message list """ 68 | 69 | return self.failure_message_list 70 | 71 | 72 | def write_test_summary(self): 73 | """ Print out a useful, human readable summary """ 74 | self.write('\n************************\n--------RESULT--------\nTotal number of checks=%d' % self.total) 75 | self.write('Total number of checks passed=%d\n----------------------\n************************\n\n' % self.passed) 76 | self.write('Total number of mini-checks=%d' % self.written) 77 | self.write('Total number of mini-checks passed=%d' % self.written_passed) 78 | failure_message_list = self.get_failure_message_list() 79 | if len(failure_message_list) > 0: 80 | self.write('\n--------FAILURE SUMMARY--------\n') 81 | for msg in failure_message_list: 82 | self.write(msg) 83 | -------------------------------------------------------------------------------- /utils/stop_test_exception_util.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This utility is for Custom Exceptions. 3 | 4 | a) Stop_Test_Exception 5 | You can raise a generic exceptions using just a string. 6 | This is particularly useful when you want to end a test midway based on some condition. 7 | ''' 8 | 9 | class Stop_Test_Exception(Exception): 10 | def __init__(self,message): 11 | self.message=message 12 | 13 | def __str__(self): 14 | return self.message --------------------------------------------------------------------------------