├── .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
--------------------------------------------------------------------------------