├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images └── modus.logo.svg └── test_root ├── .editorconfig ├── .gitignore ├── .pylintrc ├── .testrailapi ├── builds ├── beep-android.apk └── beep-ios.ipa ├── conftest.py ├── features ├── check_account.feature ├── check_password.feature └── home.feature ├── i18n.json ├── page_objects ├── __init__.py ├── account_page.py ├── base_page.py ├── home_page.py ├── locators.py └── password_page.py ├── pytest.ini ├── requirements.txt ├── screenshots └── base │ ├── account_button.png │ └── logo.png ├── scripts └── package_tests.sh ├── test_data ├── __init__.py └── faker_data.py ├── tests ├── test_actions.py ├── test_assertions.py ├── test_check_account.py ├── test_check_password.py ├── test_context.py └── test_homepage.py ├── utils ├── __init__.py ├── env_variables.py ├── gherkin_utils.py └── utils.py ├── variables.json └── webdriver ├── __init__.py ├── capabilities_android_app.json ├── capabilities_android_web.json ├── capabilities_ios_app.json ├── capabilities_ios_web.json ├── capabilities_web.json ├── custom_commands.py ├── custom_expected_conditions.py ├── custom_wait.py └── local_storage.py /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Open Source Code Of Conduct 2 | 3 | This code of conduct outlines our expectations for participants within the Modus Create community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 4 | 5 | Our open source community strives to: 6 | 7 | #### Be friendly and patient. 8 | 9 | #### Be welcoming 10 | 11 | We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 12 | 13 | #### Be considerate 14 | 15 | Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. 16 | 17 | #### Be respectful 18 | 19 | Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. 20 | Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: 21 | 22 | * Violent threats or language directed against another person. 23 | * Discriminatory jokes and language. 24 | * Posting sexually explicit or violent material. 25 | * Posting (or threatening to post) other people’s personally identifying information (“doxing”). 26 | * Personal insults, especially those using racist or sexist terms. 27 | * Unwelcome sexual attention. 28 | * Advocating for, or encouraging, any of the above behavior. 29 | * Repeated harassment of others. In general, if someone asks you to stop, then stop. 30 | 31 | #### When we disagree, try to understand why 32 | 33 | Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. 34 | 35 | #### Remember that we’re different 36 | 37 | The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 38 | 39 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. 40 | 41 | ## Diversity Statement 42 | 43 | We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. 44 | 45 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. 46 | 47 | ## Reporting Issues 48 | 49 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via `opensource@moduscreate.com`. All reports will be handled with discretion. In your report please include: 50 | 51 | * Your contact information. 52 | * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 53 | * Any additional information that may be helpful. 54 | 55 | After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. 56 | 57 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. 58 | 59 | This Code Of Conduct follows the [template](http://todogroup.org/opencodeofconduct/) established by the [TODO Group](http://todogroup.org/). 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Project 2 | 3 | ### Code of Conduct 4 | 5 | Modus has adopted a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect project participants to adhere to. 6 | 7 | ### Submitting a Pull Request 8 | 9 | If you are a first time contributor, you can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 10 | 11 | ### License 12 | 13 | By contributing, you agree that your contributions will belicensed under it's [license](./LICENSE) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011-present, Modus Create, Inc. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selenium & Appium Pytest Web, Mobile and Hybrid App Testing Boilerplate 2 | 3 | ## Description: 4 | This is a boilerplate for testing mobile hybrid apps on Web, iOS & Android. 5 | 6 | ## Dependencies: 7 | `Python` `pip` `pyenv` `virtualenv` 8 | 9 | ## Installation Steps 10 | In order to get the tests to run locally, you need to install the following pieces of software.
11 | **NOTE: **All commands shall be executed from Automation Project root directory:
12 | ```../[PROJECT_DIR]/tests/```. 13 | 14 | ### MacOS 15 | 1. Install Homebrew with `ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 16 | 1.1. Fix commandline `sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /` 17 | 2. Install Pyenv with `brew install pyenv` This is a python version manager.
18 | Add the following to *~/.bash_profile* 19 | ```# Pyenv 20 | export PYENV_ROOT="$HOME/.pyenv" 21 | export PATH="$PYENV_ROOT/bin:$PATH" 22 | export PATH="$PYENV_ROOT/shims:$PATH" 23 | export PATH="$PYENV_ROOT/completions/pyenv.bash:$PATH" 24 | ``` 25 | 3. Install python 3.7.5 with `pyenv install 3.7.5` 26 | 4. Set python version 3.7.5 to be used globally with `pyenv global 3.7.5` 27 | 5. Install virtualenv with `python3 -m pip install --user virtualenv` 28 | 6. Create new virtual env with `python3 -m virtualenv .venv` 29 | 7. Activate new virtual env with `source ./.venv/bin/activate` 30 | 8. Install all project dependencies with `pip install -r requirements.txt` 31 | 9. Check python version used with `which python`.
32 | Shall be `[PROJECT_DIR]/tests/UI/.venv_boilerplate/bin/python` 33 | 34 | ### Windows 35 | 1. Install GitBash 36 | 2. Uninstall any previous python version 37 | 3. Install python 3.7.5 using official installation file 38 | 4. Install virtualenv with `python -m pip install --user virtualenv` 39 | 5. Create new virtual env with `python -m virtualenv .venv` 40 | 6. Activate new virtual env with `source ./.venv/Scripts/activate` 41 | 7. Install all project dependencies with `pip install -r requirements.txt` 42 | 8. Check python version used with `which python`.
43 | Shall be `[PROJECT_DIR]/tests/UI/.venv_boilerplate/Scripts/python` 44 | 45 | ## Test execution 46 | 47 | ### Local Terminal run 48 | - Chrome example: 49 | ``` 50 | python -m pytest -vv --gherkin-terminal-reporter --driver Chrome --driver-path ./selenium_drivers/chromedriver_mac --base-url http://localhost:3001 --variables webdriver/capabilities_web.json --variables i18n.json --variables variables.json --tags="" 51 | ``` 52 | - Appium example: 53 | ``` 54 | python -m pytest -vv --gherkin-terminal-reporter --driver Appium --appium-capability app ./[APP_NAME].apk --appium-capability platformName Android --appium-capability platformVersion '7.0' --appium-capability deviceName device --capability env Android --capability os_version 7.0 --tags="" --variables variables.json --variables i18n.json 55 | python -m pytest -vv --gherkin-terminal-reporter --driver Appium --appium-capability browserName Chrome --appium-capability base_url https://beep.modus.app --appium-capability platformName Android --appium-capability platformVersion '7.0' --appium-capability deviceName device --tags="" --variables variables.json --variables i18n.json 56 | ``` 57 | 58 | ### Browserstack run 59 | Please read BS documentation for more details on configurations: 60 | - https://www.browserstack.com/automate/python 61 | - https://www.browserstack.com/app-automate/appium-python 62 | 63 | ### Parallel testing 64 | - Just add the `-n=3 --dist=loadscope` args and remove `--gherkin-terminal-reporter` as this reporting type is not compatible with parallel testing 65 | NOTE: 66 | `n=3` means 3 parallel tests 67 | `dist=loadscope` means that parallelism is done at *.feature*s file level 68 | 69 | ### Create PyCharm Run Configurations 70 | 1. Edit Configurations > + > Python Tests > pytest 71 | - Chrome 72 | ``` 73 | Script Path = [UI_TESTS_PATH] 74 | Additional Arguments = -vv --gherkin-terminal-reporter --driver Chrome --driver-path ./selenium_drivers/chromedriver_mac --variables variables.json --variables i18n.json 75 | Python Interpreter = 'Previously created virtualenv' 76 | Working Directory = [UI_TESTS_PATH] 77 | ``` 78 | - Firefox 79 | ``` 80 | Script Path = [UI_TESTS_PATH] 81 | Additional Arguments = -vv --gherkin-terminal-reporter --driver Firefox --driver-path ./selenium_drivers/geckodriver_mac --variables variables.json --variables i18n.json 82 | Python Interpreter = 'Previously created virtualenv' 83 | Working Directory = [UI_TESTS_PATH] 84 | ``` 85 | - BrowserStack execution arguments based on env (just replace 'Additional Arguments' with correct value: 86 | ``` 87 | Android App: -vv --gherkin-terminal-reporter --driver Appium --host '[BS_USERNAME]:[BS_KEY]@hub-cloud.browserstack.com' --port 80 --variables webdriver/capabilities_android_app.json --variables i18n.json --variables variables.json --tags="" 88 | Android Web: -vv --gherkin-terminal-reporter --driver Browserstack --capability build '[NAME_OF_BUILD_APP_OR_FEATURE]' --base-url [BASE_URL] --variables webdriver/capabilities_android_web.json --variables i18n.json --variables variables.json --tags="" 89 | 90 | iOS App: -vv --gherkin-terminal-reporter --driver Appium --host '[BS_USERNAME]:[BS_KEY]@hub-cloud.browserstack.com' --port 80 --variables webdriver/capabilities_ios_app.json --variables i18n.json --variables variables.json --tags="" 91 | iOS Web: -vv --gherkin-terminal-reporter --driver BrowserStack --capability device 'iPad Pro 12.9 2018' --capability os_version '12.0' --base-url [BASE_URL] --variables variables.json --variables i18n.json 92 | 93 | IE: -vv --gherkin-terminal-reporter --driver BrowserStack --capability browser 'IE' --capability browser_version '11' --base-url http://localhost:3001 --variables webdriver/capabilities_web.json --variables i18n.json --variables variables.json --tags="" 94 | Edge: -vv --gherkin-terminal-reporter --driver BrowserStack --capability browser 'Edge' --capability browser_version '18.0' --base-url http://localhost:3001 --variables webdriver/capabilities_web.json --variables i18n.json --variables variables.json --tags="" 95 | Chrome: -vv --gherkin-terminal-reporter --driver Browserstack --capability build '[NAME_OF_BUILD_APP_OR_FEATURE]' --base-url [BASE_URL] --variables webdriver/capabilities_web.json --variables webdriver/capabilities_web.json --variables i18n.json --variables variables.json --tags="" 96 | Safari: -vv --gherkin-terminal-reporter --driver BrowserStack --capability browser 'Safari' --capability browser_version '12.0' --base-url [BASE_URL] --variables webdriver/capabilities_web.json --variables i18n.json --variables variables.json --tags="" 97 | ``` 98 | 2. Run or Debug with the above configurations 99 | 100 | ## Code Quality 101 | Linting = the process of analyzing the source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. 102 | 103 | **IMPORTANT:** Lint your code before any commit 104 | 105 | - Go to _tests_root_ folder 106 | - Run `pylint ./**/**.py` 107 | - There should be only one Error: `E: 4, 0: invalid syntax (, line 4) (syntax-error)` 108 | - This is due to a _pylint_ issue: root files or folders cannot be ignored from linting. Will follow the fix 109 | - A rating above 9.00 should be kept for the code 110 | 111 | ## Package tests for AWS CI 112 | TODO 113 | 114 | 115 | # Browserstack Configuration: 116 | Add BrowserStack API credentials to `./.browserstack` file 117 | 118 | ``` 119 | [credentials] 120 | username=TODO 121 | key=TODO 122 | ``` 123 | 124 | 125 | # TestRail Integration 126 | This is HOW TO guide for TestRail integration of this project 127 | 128 | ## Configuration: 129 | Add TestRail API credentials to `./.testrailapi` file 130 | 131 | ``` 132 | [credentials] 133 | email=TODO 134 | key=TODO 135 | url=https://moduscreateinc.testrail.io 136 | verify_ssl=True 137 | ``` 138 | Note: Please follow instructions for generating user_key: http://docs.gurock.com/testrail-api2/accessing 139 | 140 | **!DO NOT PUSH your credentials into git repo** 141 | 142 | ## Export test cases to TestRail 143 | - From project root directory 144 | - Run: 145 | 146 | **To import/update test Scenarios for ALL feature files** 147 | - ```python -m pytest -vv --export_tests_path "features" --variables variables.json --variables i18n.json``` 148 | 149 | **To import/update test Scenarios for INDIVIDUAL .feature file** 150 | - ```python -m pytest -vv --export_tests_path "features/[DIR_NAME]/[FILE_NAME].feature" --variables variables.json --variables i18n.json``` 151 | 152 | ## Implementation details 153 | - Each *.feature* file is a product functionality 154 | - Unique key is the pair of **Feature Name - Functionality** + **feature description** 155 | ```gherkin 156 | Feature: Create User - Email registration 157 | As an anonymous user 158 | I open the app for the first time 159 | I want to be able to register with email 160 | ``` 161 | - It will create a new **Test Suite** for each unique *Feature Name* file published 162 | - It will create a new **Section** within the **Test Suite** for each unique **Functionality** 163 | - If **test suite** was previously imported it will update all the tests within 164 | - Each *Scenario* is a TestRail *Case* 165 | - Unique key is pair of **scenario name** + **data set** (The Examples line in json format) 166 | ```gherkin 167 | Scenario: Add two numbers 168 | Given I have powered calculator on 169 | When I enter <50> into the calculator 170 | When I enter <70> into the calculator 171 | When I press add 172 | Then The result should be <120> on the screen 173 | Examples: 174 | | number_1 | number_2 | result | 175 | | 10 | 20 | 30 | 176 | | 50 | 60 | 120 | 177 | ``` 178 | - It will create a new **case** for each *Scenario* published 179 | - If **case** was imported it will update it with latest changes 180 | - Scenario *tags*: 181 | - **@automation** = **case** is automated 182 | - **@JIRA-1** = **case ref** to Jira ticket (the feature ticket) 183 | - **@smoke** / **@sanity** / **@regression** / **None** = **case priority** Critical / High / Medium / Low 184 | - **@market_us** = **case** is for USA market 185 | - **@not_market_ca** = **case** is not for Canada market 186 | - *Steps* are imported as separate ones with empty *Expected Results* 187 | - Do **NOT** use *And* and *But* keys as it will fail the match of test cases during results publishing 188 | 189 | ## Publish test results to TestRail 190 | ###Prerequisites: 191 | #### 1. Create Test Plan in TestRail 192 | - You have to manually create the test plan in TestRail 193 | - Naming convention: [JIRA_PROJECT_NAME]_[SPRINT_NAME]_[MARKET] - MARKET only if applied 194 | - eg: *JIRA_Sprint-1_us* or *JIRA_Regression_us* 195 | - Test Plan shall contain all Cases that you want to execute within the session 196 | - The correct configuration shall be present. This is described by *variables.json* in *env* 197 | - Test results are published to TestRail at the end of testing 198 | - The reason of failure is also added to the test step 199 | 200 | #### 2. Test run details in `project` 201 | - Go to *variables.json* 202 | - Edit *project* with corresponding data. eg: 203 | - ``` 204 | "project": { 205 | "id": 1, 206 | "name": "JIRA", 207 | "language": "en", 208 | "tags":"", 209 | "test_plan": "JIRA_Sprint_1", 210 | "market": "us" 211 | } 212 | 213 | - Info 214 | - **id** = **mandatory**, taken from TestRail, is the id of the project. Can be picked up from url in TestRail. Make sure id is correct. 215 | - **name** = **mandatory**, name of the project you will publish to. 216 | - **tags** = **optional**, filtering scenarios by required parameters 217 | - **test_plan** = **mandatory**, title of the test plan created manually in TestRail 218 | - **language** = **mandatory**, taking string from i18n.json for selected language 219 | - **market** = **optional**, in order to know for which market to trigger tests 220 | 221 | ### Run tests and publish results to TestRail 222 | - Add the following argument to CLI 223 | - ```--export_results``` - this will run and publish tests results 224 | 225 | 226 | # Notes 227 | ## Tips and Tricks 228 | To benefit from autocomplete please set *UI* folder as **Sources Root** 229 | - Right click on *UI_* 230 | - Click on *Mark Directory As* 231 | - Click on *Sources Root* 232 | -------------------------------------------------------------------------------- /images/modus.logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test_root/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /test_root/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | ../.pytest_cache/ 6 | ../.DS_Store 7 | ../.idea/ 8 | .idea/ 9 | .DS_Store 10 | tests/.DS_Store 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | #ADF 113 | test_bundle.zip 114 | wheelhouse/ 115 | .pytest_cache/ 116 | -------------------------------------------------------------------------------- /test_root/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # DEPRECATED 25 | include-ids=no 26 | 27 | # DEPRECATED 28 | symbols=no 29 | 30 | # Use multiple processes to speed up Pylint. 31 | jobs=1 32 | 33 | # Allow loading of arbitrary C extensions. Extensions are imported into the 34 | # active Python interpreter and may run arbitrary code. 35 | unsafe-load-any-extension=no 36 | 37 | # A comma-separated list of package or module names from where C extensions may 38 | # be loaded. Extensions are loading into the active Python interpreter and may 39 | # run arbitrary code 40 | extension-pkg-whitelist= 41 | 42 | 43 | [MESSAGES CONTROL] 44 | 45 | # Only show warnings with the listed confidence levels. Leave empty to show 46 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 47 | confidence= 48 | 49 | # Enable the message, report, category or checker with the given id(s). You can 50 | # either give multiple identifier separated by comma (,) or put this option 51 | # multiple time. See also the "--disable" option for examples. 52 | #enable= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once).You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use"--disable=all --enable=classes 62 | # --disable=W" 63 | # disable=E1608,W1627,E1601,E1603,E1602,E1605,E1604,E1607,E1606,W1621,W1620,W1623,W1622,W1625,W1624,W1609,W1608,W1607,W1606,W1605,W1604,W1603,W1602,W1601,W1639,I0021,W1638,I0020,W1618,W1619,W1630,W1626,W1637,W1634,W1635,W1610,W1611,W1612,W1613,W1614,W1615,W1616,W1617,W1632,W1633,W0704,W1628,W1629,W1636 64 | disable=C0111,C0325,R0903,W0621,W0110 65 | 66 | 67 | [REPORTS] 68 | 69 | # Set the output format. Available formats are text, parseable, colorized, msvs 70 | # (visual studio) and html. You can also give a reporter class, eg 71 | # mypackage.mymodule.MyReporterClass. 72 | output-format=text 73 | 74 | # Put messages in a separate file for each module / package specified on the 75 | # command line instead of printing them on stdout. Reports (if any) will be 76 | # written in a file name "pylint_global.[txt|html]". 77 | files-output=no 78 | 79 | # Tells whether to display a full report or only the messages 80 | reports=yes 81 | 82 | # Python expression which should return a note less than 10 (10 is the highest 83 | # note). You have access to the variables errors warning, statement which 84 | # respectively contain the number of errors / warnings messages and the total 85 | # number of statements analyzed. This is used by the global evaluation report 86 | # (RP0004). 87 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 88 | 89 | # Add a comment according to your evaluation note. This is used by the global 90 | # evaluation report (RP0004). 91 | comment=no 92 | 93 | # Template used to display messages. This is a python new-style format string 94 | # used to format the message information. See doc for all details 95 | #msg-template= 96 | 97 | 98 | [LOGGING] 99 | 100 | # Logging modules to check that the string format arguments are in logging 101 | # function parameter format 102 | logging-modules=logging 103 | 104 | 105 | [VARIABLES] 106 | 107 | # Tells whether we should check for unused import in __init__ files. 108 | init-import=no 109 | 110 | # A regular expression matching the name of dummy variables (i.e. expectedly 111 | # not used). 112 | dummy-variables-rgx=_$|dummy 113 | 114 | # List of additional names supposed to be defined in builtins. Remember that 115 | # you should avoid to define new builtins when possible. 116 | additional-builtins= 117 | 118 | # List of strings which can identify a callback function by name. A callback 119 | # name must start or end with one of those strings. 120 | callbacks=cb_,_cb 121 | 122 | 123 | [BASIC] 124 | 125 | # Required attributes for module, separated by a comma 126 | required-attributes= 127 | 128 | # List of builtins function names that should not be used, separated by a comma 129 | bad-functions=map,filter,input 130 | 131 | # Good variable names which should always be accepted, separated by a comma 132 | good-names=i,j,k,ex,Run,_ 133 | 134 | # Bad variable names which should always be refused, separated by a comma 135 | bad-names=foo,bar,baz,toto,tutu,tata 136 | 137 | # Colon-delimited sets of names that determine each other's naming style when 138 | # the name regexes allow several styles. 139 | name-group= 140 | 141 | # Include a hint for the correct naming format with invalid-name 142 | include-naming-hint=no 143 | 144 | # Regular expression matching correct function names 145 | function-rgx=[a-z_][a-z0-9_]{1,40}$ 146 | 147 | # Naming hint for function names 148 | function-name-hint=[a-z_][a-z0-9_]{1,40}$ 149 | 150 | # Regular expression matching correct variable names 151 | variable-rgx=[a-z_][a-z0-9_]{1,40}$ 152 | 153 | # Naming hint for variable names 154 | variable-name-hint=[a-z_][a-z0-9_]{1,40}$ 155 | 156 | # Regular expression matching correct constant names 157 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 158 | 159 | # Naming hint for constant names 160 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 161 | 162 | # Regular expression matching correct attribute names 163 | attr-rgx=[a-z_][a-z0-9_]{1,40}$ 164 | 165 | # Naming hint for attribute names 166 | attr-name-hint=[a-z_][a-z0-9_]{1,40}$ 167 | 168 | # Regular expression matching correct argument names 169 | argument-rgx=[a-z_][a-z0-9_]{1,40}$ 170 | 171 | # Naming hint for argument names 172 | argument-name-hint=[a-z_][a-z0-9_]{1,40}$ 173 | 174 | # Regular expression matching correct class attribute names 175 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,40}|(__.*__))$ 176 | 177 | # Naming hint for class attribute names 178 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{1,40}|(__.*__))$ 179 | 180 | # Regular expression matching correct inline iteration names 181 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 182 | 183 | # Naming hint for inline iteration names 184 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 185 | 186 | # Regular expression matching correct class names 187 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 188 | 189 | # Naming hint for class names 190 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 191 | 192 | # Regular expression matching correct module names 193 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 194 | 195 | # Naming hint for module names 196 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 197 | 198 | # Regular expression matching correct method names 199 | method-rgx=[a-z_][a-z0-9_]{1,40}$ 200 | 201 | # Naming hint for method names 202 | method-name-hint=[a-z_][a-z0-9_]{1,40}$ 203 | 204 | # Regular expression which should only match function or class names that do 205 | # not require a docstring. 206 | no-docstring-rgx=__.*__ 207 | 208 | # Minimum line length for functions/classes that require docstrings, shorter 209 | # ones are exempt. 210 | docstring-min-length=-1 211 | 212 | 213 | [MISCELLANEOUS] 214 | 215 | # List of note tags to take in consideration, separated by a comma. 216 | notes=FIXME,XXX,TODO 217 | 218 | 219 | [TYPECHECK] 220 | 221 | # Tells whether missing members accessed in mixin class should be ignored. A 222 | # mixin class is detected if its name ends with "mixin" (case insensitive). 223 | ignore-mixin-members=yes 224 | 225 | # List of module names for which member attributes should not be checked 226 | # (useful for modules/projects where namespaces are manipulated during runtime 227 | # and thus existing member attributes cannot be deduced by static analysis 228 | ignored-modules= 229 | 230 | # List of classes names for which member attributes should not be checked 231 | # (useful for classes with attributes dynamically set). 232 | ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local 233 | 234 | # When zope mode is activated, add a predefined set of Zope acquired attributes 235 | # to generated-members. 236 | zope=no 237 | 238 | # List of members which are set dynamically and missed by pylint inference 239 | # system, and so shouldn't trigger E1101 when accessed. Python regular 240 | # expressions are accepted. 241 | generated-members=REQUEST,acl_users,aq_parent 242 | 243 | 244 | [SPELLING] 245 | 246 | # Spelling dictionary name. Available dictionaries: none. To make it working 247 | # install python-enchant package. 248 | spelling-dict= 249 | 250 | # List of comma separated words that should not be checked. 251 | spelling-ignore-words= 252 | 253 | # A path to a file that contains private dictionary; one word per line. 254 | spelling-private-dict-file= 255 | 256 | # Tells whether to store unknown words to indicated private dictionary in 257 | # --spelling-private-dict-file option instead of raising a message. 258 | spelling-store-unknown-words=no 259 | 260 | 261 | [FORMAT] 262 | 263 | # Maximum number of characters on a single line. 264 | max-line-length=120 265 | 266 | # Regexp for a line that is allowed to be longer than the limit. 267 | ignore-long-lines=^\s*(# )??$ 268 | 269 | # Allow the body of an if to be on the same line as the test if there is no 270 | # else. 271 | single-line-if-stmt=no 272 | 273 | # List of optional constructs for which whitespace checking is disabled 274 | no-space-check=trailing-comma,dict-separator 275 | 276 | # Maximum number of lines in a module 277 | max-module-lines=1000 278 | 279 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 280 | # tab). 281 | indent-string=' ' 282 | 283 | # Number of spaces of indent required inside a hanging or continued line. 284 | indent-after-paren=4 285 | 286 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 287 | expected-line-ending-format= 288 | 289 | 290 | [SIMILARITIES] 291 | 292 | # Minimum lines number of a similarity. 293 | min-similarity-lines=4 294 | 295 | # Ignore comments when computing similarities. 296 | ignore-comments=yes 297 | 298 | # Ignore docstrings when computing similarities. 299 | ignore-docstrings=yes 300 | 301 | # Ignore imports when computing similarities. 302 | ignore-imports=no 303 | 304 | 305 | [CLASSES] 306 | 307 | # List of interface methods to ignore, separated by a comma. This is used for 308 | # instance to not check methods defines in Zope's Interface base class. 309 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 310 | 311 | # List of method names used to declare (i.e. assign) instance attributes. 312 | defining-attr-methods=__init__,__new__,setUp 313 | 314 | # List of valid names for the first argument in a class method. 315 | valid-classmethod-first-arg=cls 316 | 317 | # List of valid names for the first argument in a metaclass class method. 318 | valid-metaclass-classmethod-first-arg=mcs 319 | 320 | # List of member names, which should be excluded from the protected access 321 | # warning. 322 | exclude-protected=_asdict,_fields,_replace,_source,_make 323 | 324 | 325 | [DESIGN] 326 | 327 | # Maximum number of arguments for function / method 328 | max-args=5 329 | 330 | # Argument names that match this expression will be ignored. Default to name 331 | # with leading underscore 332 | ignored-argument-names=_.* 333 | 334 | # Maximum number of locals for function / method body 335 | max-locals=20 336 | 337 | # Maximum number of return / yield for function / method body 338 | max-returns=6 339 | 340 | # Maximum number of branch for function / method body 341 | max-branches=12 342 | 343 | # Maximum number of statements in function / method body 344 | max-statements=50 345 | 346 | # Maximum number of parents for a class (see R0901). 347 | max-parents=7 348 | 349 | # Maximum number of attributes for a class (see R0902). 350 | max-attributes=7 351 | 352 | # Minimum number of public methods for a class (see R0903). 353 | min-public-methods=2 354 | 355 | # Maximum number of public methods for a class (see R0904). 356 | max-public-methods=20 357 | 358 | 359 | [IMPORTS] 360 | 361 | # Deprecated modules which should not be used, separated by a comma 362 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 363 | 364 | # Create a graph of every (i.e. internal and external) dependencies in the 365 | # given file (report RP0402 must not be disabled) 366 | import-graph= 367 | 368 | # Create a graph of external dependencies in the given file (report RP0402 must 369 | # not be disabled) 370 | ext-import-graph= 371 | 372 | # Create a graph of internal dependencies in the given file (report RP0402 must 373 | # not be disabled) 374 | int-import-graph= 375 | 376 | 377 | [EXCEPTIONS] 378 | 379 | # Exceptions that will emit a warning when being caught. Defaults to 380 | # "Exception" 381 | overgeneral-exceptions=Exception 382 | -------------------------------------------------------------------------------- /test_root/.testrailapi: -------------------------------------------------------------------------------- 1 | [credentials] 2 | email=TODO 3 | key=TODO 4 | url=https://TODO.testrail.io 5 | verify_ssl=True 6 | -------------------------------------------------------------------------------- /test_root/builds/beep-android.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/builds/beep-android.apk -------------------------------------------------------------------------------- /test_root/builds/beep-ios.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/builds/beep-ios.ipa -------------------------------------------------------------------------------- /test_root/conftest.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | # pylint: disable=protected-access 3 | # pylint: disable=wildcard-import 4 | # pylint: disable=unused-wildcard-import 5 | 6 | from collections import defaultdict 7 | from copy import deepcopy 8 | 9 | import pytest 10 | from pytest_testrail.testrail_api import TestRailAPI 11 | from pytest_testrail.testrail_utils import export_tests, export_tests_results 12 | 13 | # from tests.test_assertions import * 14 | # from tests.test_common import * 15 | from utils.env_variables import EnvVariables 16 | from utils.gherkin_utils import get_feature, get_feature_files_path 17 | from utils.utils import initialize_screenshot_dirs, get_env_name 18 | from webdriver.custom_commands import add_custom_commands 19 | 20 | pytest_plugins = [ 21 | 'pytest_testrail', 22 | ] 23 | 24 | 25 | def pytest_configure(config): 26 | config.option.keyword = 'automated' 27 | config.option.markexpr = 'not not_in_scope' 28 | pytest.globalDict = defaultdict() 29 | 30 | 31 | def pytest_addoption(parser): 32 | parser.addoption('--language', 33 | action='store', 34 | default='en', 35 | type=str, 36 | help='Application language') 37 | parser.addoption('--export_tests_path', 38 | metavar="str", 39 | help='Will export tests form given file or directory to TestRail') 40 | parser.addoption('--export_results', 41 | action='store_true', 42 | help='If false will not publish results to TestRail') 43 | parser.addoption('--tags', 44 | metavar="str", 45 | help='Will filter tests by given tags') 46 | 47 | 48 | def pytest_collection_modifyitems(config, items): 49 | export_tests_path = config.option.export_tests_path 50 | if export_tests_path: 51 | print('\nUn-select all tests. Exporting is selected') 52 | for item in items: 53 | item.add_marker(pytest.mark.not_in_scope) 54 | 55 | raw_tags = config.option.tags 56 | if raw_tags: 57 | print('\nFilter tests by given tags: %s' % raw_tags) 58 | tags = raw_tags.split(',') 59 | filter_collection_by_tags(items, tags) 60 | 61 | 62 | def filter_collection_by_tags(items, tags): 63 | for item in items: 64 | has_tag = False 65 | for tag in tags: 66 | if tag.startswith('not:'): 67 | if any(m.name == tag.replace('not:', '') for m in item.own_markers): 68 | item.add_marker(pytest.mark.not_in_scope) 69 | has_tag = True 70 | else: 71 | if any(m.name == tag for m in item.own_markers): 72 | has_tag = True 73 | if not has_tag: 74 | item.add_marker(pytest.mark.not_in_scope) 75 | 76 | 77 | def pytest_sessionstart(session): 78 | pytest.globalDict['env'] = session.config._variables['env'] if 'env' in session.config._variables else {} 79 | 80 | export_tests_path = session.config.option.export_tests_path 81 | export_results = session.config.option.export_results 82 | if export_results is True and bool(export_tests_path) is True: 83 | raise ValueError('Cannot export "Test Cases" and "Test Results" at the same time') 84 | 85 | project_variables = session.config._variables['project'] 86 | pytest.globalDict['project'] = project_variables 87 | pytest.globalDict['scenarios_run'] = {} 88 | 89 | pytest.globalDict['i18n'] = session.config._variables[project_variables['language']] 90 | 91 | env_name = get_env_name(session.config._capabilities) 92 | pytest.globalDict['env_name'] = env_name 93 | 94 | headless_chrome = session.config._capabilities['headless'] in ['true', 'True', '1', 'ty', 'Y'] \ 95 | if 'headless' in session.config._capabilities else False 96 | pytest.globalDict['headless_chrome'] = headless_chrome 97 | 98 | initialize_screenshot_dirs() 99 | add_custom_commands() 100 | 101 | 102 | def pytest_sessionfinish(session): 103 | export_tests_path = session.config.option.export_tests_path 104 | export_results = session.config.option.export_results 105 | 106 | project_variables = session.config._variables['project'] 107 | 108 | if export_tests_path: 109 | print('Initialize TestRail client') 110 | tr = TestRailAPI() 111 | export_feature_files(tr, project_variables, export_tests_path) 112 | 113 | if export_results: 114 | print('Initialize TestRail client') 115 | scenarios_run = pytest.globalDict['scenarios_run'] 116 | env_name = pytest.globalDict['env_name'] 117 | tr = TestRailAPI() 118 | export_tests_results(tr, project_variables, scenarios_run, env_name) 119 | 120 | 121 | def pytest_bdd_after_scenario(request, feature, scenario): 122 | # Adding Scenario to the list of Scenarios ran 123 | if request.config.option.export_results: 124 | add_scenario_to_run(request, scenario) 125 | if request.config.option.reruns >= request.node.execution_count: 126 | scenario.failed = False 127 | for scenario_step in scenario.steps: 128 | scenario_step.failed = False 129 | 130 | 131 | def pytest_bdd_step_error(request, scenario, step, exception): 132 | scenario.exception = exception 133 | scenario.failed = True 134 | # Setting Scenario and Steps statuses and exception error if the case 135 | flag = False 136 | for scenario_step in scenario.steps: 137 | scenario_step.failed = None if flag else False 138 | if scenario_step == step: 139 | scenario_step.exception = exception 140 | scenario_step.failed = True 141 | flag = True 142 | print('Following step FAILED: %s' % step.name) 143 | exception_message = exception.msg if hasattr(exception, 'msg') \ 144 | else exception.message if hasattr(exception, 'message') \ 145 | else exception.args[0] if hasattr(exception, 'args') \ 146 | else 'no error message' 147 | scenario.exception_message = exception_message 148 | print('Error: %s' % exception_message) 149 | 150 | 151 | def export_feature_files(tr: TestRailAPI, project_variables: dict, export_tests_path: str): 152 | files_path = get_feature_files_path(export_tests_path) 153 | for file_path in files_path: 154 | feature = get_feature(file_path) 155 | export_tests(tr, project_variables['id'], project_variables['name'], feature) 156 | 157 | 158 | def add_scenario_to_run(request, scenario): 159 | scenario.data_set = {} 160 | for key, value in request.node.funcargs.items(): 161 | if key in scenario.params: 162 | scenario.data_set.update({key: value}) 163 | 164 | suite_name = scenario.feature.name.split(' - ')[0] 165 | if suite_name not in pytest.globalDict['scenarios_run']: 166 | pytest.globalDict['scenarios_run'][suite_name] = [] 167 | pytest.globalDict['scenarios_run'][suite_name].append(deepcopy(scenario)) 168 | 169 | 170 | @pytest.fixture 171 | def selenium(selenium): 172 | selenium.set_page_load_timeout(90) 173 | selenium.implicitly_wait(60) 174 | selenium.set_script_timeout(60) 175 | 176 | selenium.delete_all_cookies() 177 | return selenium 178 | 179 | 180 | @pytest.fixture 181 | def chrome_options(chrome_options): 182 | if pytest.globalDict['headless_chrome'] is True: 183 | chrome_options.add_argument('--headless') 184 | return chrome_options 185 | 186 | 187 | @pytest.fixture(scope='session') 188 | def language(request): 189 | config = request.config 190 | language = config.getoption('language') 191 | if language is not None: 192 | return language 193 | return None 194 | 195 | 196 | @pytest.fixture(scope='session') 197 | def env_variables(request): 198 | env_vars_file_path = "%s/.local.env" % request.session.config.known_args_namespace.confcutdir 199 | return EnvVariables(env_vars_file_path) 200 | -------------------------------------------------------------------------------- /test_root/features/check_account.feature: -------------------------------------------------------------------------------- 1 | @account_screen @nondestructive 2 | Feature: Account screen of Beep web app 3 | As a user of Beep web app 4 | I want to be able to provide my username on the Account page 5 | So that I know if my account data has been compromised 6 | 7 | 8 | Background: 9 | Given I load the Beep app 10 | 11 | @S1 @automated @web @mobile-web @mobile-app 12 | Scenario: I can navigate to the Account page 13 | When I click the button 14 | Then I should be on the Account page 15 | Then I should see the Your username or email text field 16 | Then I should see the Back and Check button 17 | 18 | @S2 @manual 19 | Scenario: Functionality of "Your username or email" text box 20 | Given I am on https://beep.modus.app/#/acc Page 21 | When I tap inside "Your username or email" text box 22 | Then I should see a cursor 23 | Then I should be able to type 24 | 25 | @S3 @manual 26 | Scenario: Acceptance of data by"Your username or email" text box 27 | Given I am on https://beep.modus.app/acc Page 28 | When I tap inside "Your username or email" text box 29 | Then I should be able to add User name 30 | Then I should be able to add Email ID 31 | 32 | @S4.1 @manual 33 | Scenario: Activation of "Check" option 34 | Given I am on "https://beep.modus.app" 35 | Given I click on "Account field" 36 | When User should see "check" option 37 | Then I should be on https://beep.modus.app/acc Page 38 | Then I should see deactivated "Check" option 39 | 40 | @S4.2 @manual 41 | Scenario: Activation of "Check" option 42 | Given I am on https://beep.modus.app/#/acc Page 43 | When I tap inside "Your username or email" text box 44 | When I don't type anything in "Your username or email" text box 45 | Then I should see deactivated "Check" option 46 | 47 | @S4.3 @manual 48 | Scenario: Activation of "Check" option 49 | Given I am on https://beep.modus.app/#/acc Page 50 | When I tap inside "Your username or email" text box 51 | When I add spaces in "Your username or email" text box 52 | Then I should see deactivated "Check" option 53 | 54 | 55 | @S4.4 @manual 56 | Scenario: Action when click on deactivated "Check" option 57 | Given I am on https://beep.modus.app/#/acc Page 58 | When I tap inside "Your username or email" text box 59 | When I add spaces in "Your username or email" text box 60 | Then I see deactivated "Check" option 61 | Then I click on "Check" Option 62 | Then I should "Check" option deactivated only 63 | Then I should see no validation and change for "Your username or email" text box 64 | 65 | @S4.5 @manual 66 | Scenario: Action when click on deactivated "Check" option 67 | Given I am on https://beep.modus.app/#/acc Page 68 | When I tap inside "Your username or email" text box 69 | Then I see deactivated "Check" option 70 | Then I click on "Check" Option 71 | Then I should "Check" option deactivated only 72 | Then I should see no validation and change for "Your username or email" text box 73 | 74 | @S5.1 @manual 75 | Scenario: Verification of a non hacked Email ID 76 | Given I am on https://beep.modus.app/#/acc Page 77 | When I tap inside "Your username or email" text box 78 | When User enters a non hacked Email ID 79 | When I click on "Check" Option 80 | Then I should redirects to "https://beep.modus.app/#/safe" Page 81 | Then I should see "Congratulations! Your account has not been compromised" message 82 | 83 | @S5.2 @manual 84 | Scenario: Verification of a non hacked Username 85 | Given I am on https://beep.modus.app/#/acc Page 86 | When I tap inside "Your username or email" text box 87 | When User enters a non hacked Username 88 | When I click on "Check" Option 89 | Then I should redirects to "https://beep.modus.app/#/safe" Page 90 | Then I should see "Congratulations! Your account has not been compromised" message 91 | 92 | @S5.3 @manual 93 | Scenario: Verification of a hacked Email ID 94 | Given I am on https://beep.modus.app/#/acc Page 95 | When I tap inside "Your username or email" text box 96 | When User enters a non hacked Email ID 97 | When I click on "Check" Option 98 | Then I should redirects to "https://beep.modus.app/#/breaches" Page 99 | Then I should display of websites from where it had been hacked 100 | 101 | @S5.4 @manual 102 | Scenario: Verification of a hacked Username 103 | Given I am on https://beep.modus.app/#/acc Page 104 | When I tap inside "Your username or email" text box 105 | When User enters a non hacked Username 106 | When I click on "Check" Option 107 | Then I should redirects to "https://beep.modus.app/#/breaches" Page 108 | Then I should display of websites from where it had been hacked 109 | 110 | @S5.5 @manual 111 | Scenario: Verification of a invalid Email ID or Username 112 | Given I am on https://beep.modus.app/#/acc Page 113 | When I tap inside "Your username or email" text box 114 | When User enters a invalid Username or Email ID 115 | Then I should see a validation message 116 | 117 | @S6 @manual 118 | Scenario: Verification on "back" button from Acc. Page 119 | Given I am on "https://beep.modus.app" Page 120 | When User click on left back button 121 | Then I should redirects to Home screen 'https://beep.modus.app/#/" Page 122 | -------------------------------------------------------------------------------- /test_root/features/check_password.feature: -------------------------------------------------------------------------------- 1 | @password_screen @nondestructive 2 | Feature: Password screen of Beep web app 3 | As a user of Beep web app 4 | I want to be able to provide my password on the Password page 5 | So that I know if my account data has been compromised 6 | 7 | 8 | Background: 9 | Given I load the Beep app 10 | 11 | @S1 @automated @web @mobile-web @mobile-app 12 | Scenario: I can navigate to the Password page 13 | When I click the button 14 | Then I should be on the Password page 15 | Then I should see the Your password text field 16 | Then I should see the Back and Check button 17 | -------------------------------------------------------------------------------- /test_root/features/home.feature: -------------------------------------------------------------------------------- 1 | @home_screen @nondestructive 2 | Feature: Home Screen of Beep Web App 3 | As a User 4 | I want to navigate to the appropriate screen 5 | So that I can check if my account name or password have been compromised 6 | 7 | Background: 8 | Given I load the Beep app 9 | 10 | @S1 @automated @web @mobile-web 11 | Scenario: I can load the Beep homepage on web 12 | Then I should see app logo 13 | Then I should see the button 14 | Then I should see the button 15 | Then I should see the link 16 | Then I should see the link 17 | Then I should see the link 18 | 19 | @S1 @automated @mobile-app 20 | Scenario: I can load the Beep homepage on mobile-app 21 | Then I should see app logo 22 | Then I should see the button 23 | Then I should see the button 24 | Then I should see the link 25 | 26 | @automated @web @visual-regression @this 27 | Scenario: Homepage visual regression 28 | Then The app logo default visual is valid 29 | Then The account button default visual is valid 30 | Then The password button default visual is valid 31 | -------------------------------------------------------------------------------- /test_root/i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": {} 3 | } 4 | -------------------------------------------------------------------------------- /test_root/page_objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/page_objects/__init__.py -------------------------------------------------------------------------------- /test_root/page_objects/account_page.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | from assertpy import assert_that 3 | from selenium.webdriver.common.by import By 4 | 5 | from page_objects.base_page import BasePage 6 | from page_objects.locators import ActionLocators, ContextLocators 7 | 8 | 9 | class AccountPage(BasePage): 10 | 11 | def __init__(self, selenium, base_url): 12 | super().__init__(selenium, base_url) 13 | 14 | import pytest 15 | self._env = pytest.globalDict['env'] 16 | 17 | @property 18 | def _back_button(self): 19 | return self._selenium.find_element(By.XPATH, ActionLocators.back_button) 20 | 21 | @property 22 | def _check_button(self): 23 | return self._selenium.find_element(By.XPATH, ActionLocators.check_button) 24 | 25 | @property 26 | def _page_title(self): 27 | return self._selenium.title 28 | 29 | @property 30 | def _title(self): 31 | return self._selenium.find_element(By.XPATH, ContextLocators.title) 32 | 33 | @property 34 | def _text_field(self): 35 | return self._selenium.find_element(By.XPATH, ActionLocators.text_field % 'email') 36 | 37 | @property 38 | def _text_field_label(self): 39 | return self._selenium.find_element(By.XPATH, ContextLocators.text_field_label) 40 | 41 | def validate_action_buttons(self): 42 | assert_that(self._back_button.get_property('hidden')).is_false() 43 | assert_that(self._check_button.get_property('hidden')).is_false() 44 | 45 | def validate_text_field(self): 46 | assert_that(self._text_field_label.get_property('hidden')).is_false() 47 | assert_that(self._text_field.get_property('hidden')).is_false() 48 | 49 | def is_loaded(self, **kwargs): 50 | self.wait.wait_for_element_visible(value='//ion-title', time_to_wait=20) 51 | assert_that(self._title.text).is_equal_to('Check Account') 52 | -------------------------------------------------------------------------------- /test_root/page_objects/base_page.py: -------------------------------------------------------------------------------- 1 | from webdriver.custom_wait import CustomWait 2 | 3 | 4 | class BasePage: 5 | 6 | def __init__(self, selenium, base_url): 7 | self._base_url = base_url 8 | self._selenium = selenium 9 | self.wait = CustomWait(self._selenium) 10 | 11 | def __getitem__(self, key): 12 | return getattr(self, key) 13 | 14 | def open(self, **kwargs): 15 | pass 16 | 17 | def is_loaded(self, **kwargs): 18 | pass 19 | -------------------------------------------------------------------------------- /test_root/page_objects/home_page.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | from assertpy import assert_that 3 | from selenium.webdriver.common.by import By 4 | 5 | from page_objects.base_page import BasePage 6 | from page_objects.locators import ActionLocators, ContextLocators 7 | from utils.utils import compare_images 8 | 9 | 10 | class HomePage(BasePage): 11 | 12 | def __init__(self, selenium, base_url): 13 | super().__init__(selenium, base_url) 14 | 15 | import pytest 16 | self._env = pytest.globalDict['env'] 17 | self._page_url = '' 18 | 19 | extension = '.png' 20 | self._base_logo_screenshot_url = pytest.globalDict['base_screenshot_dir'] + '/homepage/base/logo' + extension 21 | self._actual_logo_screenshot_url = pytest.globalDict['actual_screenshot_dir'] + '/homepage/actual/logo' + extension 22 | self._diff_logo_screenshot_url = pytest.globalDict['diff_screenshot_dir'] + '/homepage/diff/logo' + extension 23 | self._base_account_button_screenshot_url = pytest.globalDict['base_screenshot_dir'] + '/homepage/base/account_button' + extension 24 | self._actual_account_button_screenshot_url = pytest.globalDict['actual_screenshot_dir'] + '/homepage/actual/account_button' + extension 25 | self._diff_account_button_screenshot_url = pytest.globalDict['diff_screenshot_dir'] + '/homepage/diff/account_button' + extension 26 | 27 | @property 28 | def _account_button(self): 29 | return self._selenium.find_element(By.XPATH, ActionLocators.button % 'Account logo') 30 | 31 | @property 32 | def _app_store_link(self): 33 | return self._selenium.find_element(By.XPATH, ActionLocators.link % 'beepios') 34 | 35 | @property 36 | def _google_play_link(self): 37 | return self._selenium.find_element(By.XPATH, ActionLocators.link % 'beepandroid') 38 | 39 | @property 40 | def _header(self): 41 | return self._selenium.find_element(By.XPATH, ContextLocators.title) 42 | 43 | @property 44 | def _how_does_it_work_link(self): 45 | return self._selenium.find_element(By.XPATH, '//span[.="How does it work?"]') 46 | 47 | @property 48 | def _logo(self): 49 | return self._selenium.find_element(By.XPATH, ContextLocators.image % 'Beep logo') 50 | 51 | @property 52 | def _title(self): 53 | return self._selenium.find_element(By.XPATH, ContextLocators.title) 54 | 55 | @property 56 | def _password_button(self): 57 | return self._selenium.find_element(By.XPATH, ActionLocators.button % 'Password logo') 58 | 59 | def click_button(self, element): 60 | self[element].click() 61 | 62 | def validate_element_is_visible(self, element): 63 | assert_that(self[element].get_property('hidden')).is_equal_to(False) 64 | 65 | def validate_account_button_default_visual(self): 66 | self._account_button.get_screenshot_as_file(self._actual_account_button_screenshot_url) 67 | score = compare_images(self._base_account_button_screenshot_url, self._actual_account_button_screenshot_url, self._diff_account_button_screenshot_url) 68 | assert_that(score, 'Actual _account_button screenshot is different from Base with %s. Diff saved here: %s' 69 | % (score, self._diff_account_button_screenshot_url)).is_greater_than_or_equal_to(0.98) 70 | 71 | def validate_logo_default_visual(self): 72 | self._logo.get_screenshot_as_file(self._actual_logo_screenshot_url) 73 | score = compare_images(self._base_logo_screenshot_url, self._actual_logo_screenshot_url, self._diff_logo_screenshot_url) 74 | assert_that(score, 'Actual _logo screenshot is different from Base with %s. Diff saved here: %s' 75 | % (score, self._diff_logo_screenshot_url)).is_greater_than_or_equal_to(0.98) 76 | 77 | def open(self, **kwargs): 78 | self._selenium.get('%s/%s' % (self._base_url, self._page_url)) 79 | 80 | def is_loaded(self, **kwargs): 81 | self.wait.wait_for_element_visible(value=ContextLocators.image % 'Beep logo', time_to_wait=30) 82 | self.wait.wait_for_the_attribute_value(element=self._logo, attribute='class', value='ion-page hydrated', time_to_wait=30) 83 | -------------------------------------------------------------------------------- /test_root/page_objects/locators.py: -------------------------------------------------------------------------------- 1 | class ContextLocators: 2 | image = '//img[@alt="%s"]' 3 | title = '//ion-title' 4 | text_field_label = '//ion-label' 5 | 6 | 7 | class ActionLocators: 8 | button = ContextLocators.image + '/parent::div' 9 | back_button = '//ion-back-button' 10 | check_button = '//ion-button[.="Check"]' 11 | link = '//a[contains(@href,"%s")]' 12 | text_field = '//input[@type="%s"]' 13 | -------------------------------------------------------------------------------- /test_root/page_objects/password_page.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | from assertpy import assert_that 3 | from selenium.webdriver.common.by import By 4 | 5 | from page_objects.base_page import BasePage 6 | from page_objects.locators import ActionLocators, ContextLocators 7 | 8 | 9 | class PasswordPage(BasePage): 10 | def __init__(self, selenium, base_url): 11 | super().__init__(selenium, base_url) 12 | import pytest 13 | self._env = pytest.globalDict['env'] 14 | 15 | @property 16 | def _back_button(self): 17 | return self._selenium.find_element(By.XPATH, ActionLocators.back_button) 18 | 19 | @property 20 | def _check_button(self): 21 | return self._selenium.find_element(By.XPATH, ActionLocators.check_button) 22 | 23 | @property 24 | def _title(self): 25 | return self._selenium.find_element(By.XPATH, ContextLocators.title) 26 | 27 | @property 28 | def _text_field(self): 29 | return self._selenium.find_element(By.XPATH, ActionLocators.text_field % 'password') 30 | 31 | @property 32 | def _text_field_label(self): 33 | return self._selenium.find_element(By.XPATH, ContextLocators.text_field_label) 34 | 35 | def validate_action_buttons(self): 36 | assert_that(self._back_button.get_property('hidden')).is_false() 37 | assert_that(self._check_button.get_property('hidden')).is_false() 38 | 39 | def validate_text_field(self): 40 | assert_that(self._text_field_label.get_property('hidden')).is_false() 41 | assert_that(self._text_field.get_property('hidden')).is_false() 42 | 43 | def is_loaded(self, **kwargs): 44 | self.wait.wait_for_element_visible(value='//ion-title', time_to_wait=20) 45 | assert_that(self._title.text).is_equal_to('Check Password') 46 | -------------------------------------------------------------------------------- /test_root/pytest.ini: -------------------------------------------------------------------------------- 1 | # content of pytest.ini 2 | [pytest] 3 | filterwarnings = ignore:PytestUnknownMarkWarning 4 | log_level=INFO 5 | log_format = %(asctime)s %(levelname)s %(message)s 6 | log_date_format = %Y-%m-%d %H:%M:%S 7 | rp_uuid = 1111 8 | rp_endpoint = 1111 9 | rp_project = 1111 10 | rp_verify_ssl = False 11 | -------------------------------------------------------------------------------- /test_root/requirements.txt: -------------------------------------------------------------------------------- 1 | Appium-Python-Client==0.50 2 | assertpy==1.0 3 | browserstack-local==1.2.2 4 | configparser==4.0.2 5 | diffimg==0.2.3 6 | python-dotenv==0.12.0 7 | Faker==4.0.1 8 | gherkin-official==4.1.3 9 | imutils==0.5.3 10 | opencv-python==4.2.0.32 11 | py==1.8.1 12 | pylint==2.4.4 13 | pytest==5.3.5 14 | pytest-base-url==1.4.1 15 | pytest-bdd==3.2.1 16 | pytest-rerunfailures==8.0 17 | pytest-selenium==1.17.0 18 | # -e git://github.com/ModusCreateOrg/python-automation-boilerplate.git@v0.3-pytest-testrail#egg=pytest_testrail 19 | git+ssh://git@github.com/ModusCreateOrg/python-automation-boilerplate.git@v0.3-pytest-testrail#egg=pytest_testrail 20 | pytest-xdist==1.31.0 21 | python-dateutil==2.8.1 22 | PyYAML==5.3 23 | requests==2.23.0 24 | scikit-image==0.16.2 25 | selenium==3.141.0 26 | Selenium-Screenshot==1.6.0 27 | SSIM-PIL==1.0.10 28 | -------------------------------------------------------------------------------- /test_root/screenshots/base/account_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/screenshots/base/account_button.png -------------------------------------------------------------------------------- /test_root/screenshots/base/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/screenshots/base/logo.png -------------------------------------------------------------------------------- /test_root/scripts/package_tests.sh: -------------------------------------------------------------------------------- 1 | py.test --collect-only tests/ 2 | 3 | read -rp $'\nMake sure desired capabilities is empty.\n\tVerify your tests were collected, press any key to continue or CTRL+C to abort.\n' key 4 | 5 | ## Remove cached files 6 | find . -name '__pycache__' -type d -exec rm -r {} + 7 | find . -name '*.pyc' -exec rm -f {} + 8 | find . -name '*.pyo' -exec rm -f {} + 9 | find . -name '*~' -exec rm -f {} + 10 | 11 | ## Write installed packages to requirements.txt 12 | #pip freeze > requirements.txt 13 | 14 | ## Build wheel archive 15 | pip wheel --wheel-dir wheelhouse -r requirements.txt 16 | 17 | ## Zip tests into test_bundle.zip 18 | zip -r ../test_bundle.zip ../test_root 19 | -------------------------------------------------------------------------------- /test_root/test_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/test_data/__init__.py -------------------------------------------------------------------------------- /test_root/test_data/faker_data.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-member 2 | # pylint: disable=no-self-use 3 | from datetime import timedelta, datetime 4 | 5 | from faker import Faker 6 | 7 | 8 | class DataUtils: 9 | def __init__(self): 10 | self._faker = Faker() 11 | 12 | def get_random_datetime(self, days=0, hours=1): 13 | current_datetime = datetime.utcnow() \ 14 | .replace(minute=0, second=0, microsecond=0) + timedelta(days=days, hours=hours) 15 | next_datetime_displayed = current_datetime.strftime('Today' + ' ' + '%b %-d %-I:%M %p') 16 | return next_datetime_displayed 17 | 18 | def get_random_incomplete_password(self, length=7): 19 | return self._faker.password(length) 20 | -------------------------------------------------------------------------------- /test_root/tests/test_actions.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import when, parsers 2 | 3 | from page_objects.home_page import HomePage 4 | 5 | 6 | @when(parsers.re("I click the <(?P.*)> button"), converters=dict(button_type=str)) 7 | def click_password_button(selenium, base_url, button_type): 8 | HomePage(selenium, base_url).click_button('_%s_button' % button_type) 9 | -------------------------------------------------------------------------------- /test_root/tests/test_assertions.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import then 2 | 3 | from page_objects.home_page import HomePage 4 | 5 | 6 | @then("I should see app logo") 7 | def see_app_logo(selenium, base_url): 8 | HomePage(selenium, base_url).validate_element_is_visible('_logo') 9 | -------------------------------------------------------------------------------- /test_root/tests/test_check_account.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import then, scenarios 2 | 3 | from page_objects.account_page import AccountPage 4 | 5 | scenarios('../features/check_account.feature', strict_gherkin=False) 6 | 7 | 8 | @then("I should be on the Account page") 9 | def should_be_on_account_page(selenium, base_url): 10 | AccountPage(selenium, base_url).is_loaded() 11 | 12 | 13 | @then("I should see the Your username or email text field") 14 | def should_see_email_text_field(selenium, base_url): 15 | AccountPage(selenium, base_url).validate_text_field() 16 | 17 | 18 | @then("I should see the Back and Check button") 19 | def should_see_back_and_check_button(selenium, base_url): 20 | AccountPage(selenium, base_url).validate_action_buttons() 21 | -------------------------------------------------------------------------------- /test_root/tests/test_check_password.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import then, scenarios 2 | 3 | from page_objects.password_page import PasswordPage 4 | 5 | scenarios('../features/check_password.feature', strict_gherkin=False) 6 | 7 | 8 | @then("I should be on the Password page") 9 | def should_be_on_password_page(selenium, base_url): 10 | PasswordPage(selenium, base_url).is_loaded() 11 | 12 | 13 | @then("I should see the Your password text field") 14 | def should_see_password_text_field(selenium, base_url): 15 | PasswordPage(selenium, base_url).validate_text_field() 16 | 17 | 18 | @then("I should see the Back and Check button") 19 | def should_see_back_and_check_button(selenium, base_url): 20 | PasswordPage(selenium, base_url).validate_action_buttons() 21 | -------------------------------------------------------------------------------- /test_root/tests/test_context.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | from pytest_bdd import given 3 | 4 | from page_objects.home_page import HomePage 5 | 6 | 7 | @given("I load the Beep app") 8 | def load_beep_app(selenium, base_url): 9 | import pytest 10 | if pytest.globalDict['env']['app'] is False: 11 | HomePage(selenium, base_url).open() 12 | if pytest.globalDict['env']['app'] is True: 13 | webview = selenium.contexts[1] 14 | selenium.switch_to.context(webview) 15 | -------------------------------------------------------------------------------- /test_root/tests/test_homepage.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import parsers, then, scenarios 2 | 3 | from page_objects.home_page import HomePage 4 | 5 | scenarios('../features/home.feature', strict_gherkin=False) 6 | 7 | 8 | @then(parsers.re("I should see the <(?P.*)> button"), converters=dict(button_type=str)) 9 | def see_buttons(selenium, base_url, button_type): 10 | HomePage(selenium, base_url).validate_element_is_visible('_%s_button' % button_type) 11 | 12 | 13 | @then(parsers.re("I should see the <(?P.*)> link"), converters=dict(link_type=str)) 14 | def see_links(selenium, base_url, link_type): 15 | HomePage(selenium, base_url).validate_element_is_visible('_%s_link' % link_type) 16 | 17 | 18 | @then("The app logo default visual is valid") 19 | def logo_default_visual_is_valid(selenium, base_url): 20 | HomePage(selenium, base_url).validate_logo_default_visual() 21 | 22 | @then("The account button default visual is valid") 23 | def account_button_default_visual_is_valid(selenium, base_url): 24 | HomePage(selenium, base_url).validate_account_button_default_visual() 25 | 26 | @then("The password button default visual is valid") 27 | def password_button_default_visual_is_valid(selenium, base_url): 28 | pass 29 | # HomePage(selenium, base_url).validate_password_button_default_visual() 30 | -------------------------------------------------------------------------------- /test_root/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/utils/__init__.py -------------------------------------------------------------------------------- /test_root/utils/env_variables.py: -------------------------------------------------------------------------------- 1 | import os 2 | from threading import Lock 3 | 4 | 5 | class SingletonMeta(type): 6 | """ 7 | This is a thread-safe implementation of Singleton. 8 | """ 9 | _instance = None 10 | 11 | _lock: Lock = Lock() 12 | """ 13 | We now have a lock object that will be used to synchronize threads during 14 | first access to the Singleton. 15 | """ 16 | 17 | def __call__(cls, *args, **kwargs): 18 | # Now, imagine that the program has just been launched. Since there's no 19 | # Singleton instance yet, multiple threads can simultaneously pass the 20 | # previous conditional and reach this point almost at the same time. The 21 | # first of them will acquire lock and will proceed further, while the 22 | # rest will wait here. 23 | with cls._lock: 24 | # The first thread to acquire the lock, reaches this conditional, 25 | # goes inside and creates the Singleton instance. Once it leaves the 26 | # lock block, a thread that might have been waiting for the lock 27 | # release may then enter this section. But since the Singleton field 28 | # is already initialized, the thread won't create a new object. 29 | if not cls._instance: 30 | cls._instance = super().__call__(*args, **kwargs) 31 | return cls._instance 32 | 33 | 34 | class EnvVariables(metaclass=SingletonMeta): 35 | def __init__(self, env_vars_file_path) -> None: 36 | from dotenv import load_dotenv 37 | load_dotenv(dotenv_path=env_vars_file_path, verbose=True) 38 | self.env = env_vars_file_path 39 | 40 | @property 41 | def base_url(self): 42 | return os.getenv("BASE_URL") 43 | 44 | @property 45 | def login_url(self): 46 | return os.getenv("LOGIN_URL") 47 | 48 | @property 49 | def basic_auth_username(self): 50 | return os.getenv("BASIC_AUTH_USERNAME") 51 | 52 | @property 53 | def basic_auth_password(self): 54 | return os.getenv("BASIC_AUTH_PASSWORD") 55 | 56 | @property 57 | def login_username(self): 58 | return os.getenv("LOGIN_USERNAME") 59 | 60 | @property 61 | def login_password(self): 62 | return os.getenv("LOGIN_PASSWORD") 63 | 64 | @property 65 | def content_editor_username(self): 66 | return os.getenv("CONTENT_EDITOR_USERNAME") 67 | 68 | @property 69 | def content_editor_password(self): 70 | return os.getenv("CONTENT_EDITOR_PASSWORD") 71 | -------------------------------------------------------------------------------- /test_root/utils/gherkin_utils.py: -------------------------------------------------------------------------------- 1 | from os import path, listdir 2 | 3 | from gherkin.token_scanner import TokenScanner 4 | from gherkin.parser import Parser 5 | 6 | 7 | def data_table_vertical_converter(data_table_raw: str): 8 | data_table_list = data_table_raw.split("|") 9 | data_table_list = [elem.strip() for elem in data_table_list] 10 | data_table_list = list(filter(lambda elem: elem != "", data_table_list)) 11 | return {data_table_list[i]: data_table_list[i + 1] for i in range(0, len(data_table_list) - 1, 2)} 12 | 13 | 14 | def data_table_horizontal_converter(data_table_raw: str): 15 | data_table_rows = data_table_raw.split("\n") 16 | data_table_rows = [i for i in data_table_rows if i] 17 | data_table_header = data_table_rows[0].split('|') 18 | data_table_header = [i.strip() for i in data_table_header if i] 19 | data_table = {data_table_header[i]: [] for i in range(0, len(data_table_header))} 20 | 21 | for i in range(1, len(data_table_header)): 22 | data_table_row = data_table_rows[i].split('|') 23 | data_table_row = [i.strip() for i in data_table_row if i] 24 | for j in range(0, len(data_table_header)): 25 | data_table[data_table_header[j]].append(data_table_row[j]) 26 | 27 | 28 | def get_feature(file_path: str): 29 | """ Read and parse given feature file""" 30 | print('Reading feature file ', file_path) 31 | file_obj = open(file_path, "r") 32 | steam = file_obj.read() 33 | parser = Parser() 34 | return parser.parse(TokenScanner(steam)) 35 | 36 | 37 | def get_feature_files_path(export_tests_path: str): 38 | tests_abs_path = path.abspath(export_tests_path) 39 | if path.isfile(tests_abs_path): 40 | return [tests_abs_path] 41 | files_path = [path.join(tests_abs_path, f) for f in listdir(tests_abs_path) if 42 | path.isfile(path.join(tests_abs_path, f))] 43 | dirs_name = [f for f in listdir(tests_abs_path) if not path.isfile(path.join(tests_abs_path, f))] 44 | for dir_name in dirs_name: 45 | curent_path = tests_abs_path + "/" + dir_name 46 | files_path = files_path + [path.join(curent_path, f) for f in listdir(curent_path) if 47 | path.isfile(path.join(curent_path, f))] 48 | return files_path 49 | -------------------------------------------------------------------------------- /test_root/utils/utils.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | # pylint: disable=no-member 3 | import errno 4 | import json 5 | import os 6 | from os import path, strerror 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | 12 | def get_env_name(caps: dict): 13 | os = caps['os'] if 'os' in caps else None 14 | os_version = caps['os_version'] if 'os_version' in caps else '' 15 | env = caps['browser'] if 'browser' in caps else caps['device'] if 'device' in caps else '' 16 | 17 | if os is None: 18 | return '%s - %s' % (env, os_version) 19 | return '%s %s - %s' % (os, os_version, env) 20 | 21 | 22 | def initialize_screenshot_dirs(): 23 | import os 24 | project_pwd = os.path.dirname(__file__) 25 | pytest.globalDict['base_screenshot_dir'] = project_pwd.replace('/utils', '') + '/screenshots/base' 26 | pytest.globalDict['actual_screenshot_dir'] = project_pwd.replace('/utils', '') + '/screenshots/actual' 27 | pytest.globalDict['diff_screenshot_dir'] = project_pwd.replace('/utils', '') + '/screenshots/diff' 28 | 29 | 30 | def read_file(file_name): 31 | # file_path = re.sub(r'utils.(\w+)', file_name, path.abspath(__file__)) 32 | file_path = get_file_path(file_name) 33 | with open(file_path, encoding="utf-8") as fl: 34 | extension = path.splitext(file_path)[1] 35 | if extension == '.json': 36 | raw_data = json.load(fl) 37 | return raw_data 38 | if extension == '.txt': 39 | raw_data = fl.read() 40 | return raw_data 41 | raise extension 42 | 43 | 44 | def get_file_path(file_name): 45 | path_object = Path(file_name) 46 | if not path_object.exists(): 47 | raise FileNotFoundError(errno.ENOENT, strerror(errno.ENOENT), file_name) 48 | return path_object.resolve() 49 | -------------------------------------------------------------------------------- /test_root/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "id": 8, 4 | "name": "HS2", 5 | "language": "en", 6 | "tags": "", 7 | "test_plan": "Regression_Testing-Automation_TODO" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test_root/webdriver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/python-automation-boilerplate/b0d068f1419f0a2cee0cb19f23922a65fac60ea4/test_root/webdriver/__init__.py -------------------------------------------------------------------------------- /test_root/webdriver/capabilities_android_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": { 3 | "app": "bs://", 4 | "device": "Google Pixel 3", 5 | "os_version": "9.0", 6 | "realMobile": "true", 7 | "noReset": false, 8 | "fullReset": true, 9 | "autoGrantPermissions": true, 10 | "deviceReadyTimeout": 60, 11 | "newCommandTimeout": 120, 12 | "launchTimeout": 60000, 13 | "appWaitDuration": 60000, 14 | "webkitResponseTimeout": 60000, 15 | "databaseEnabled": false, 16 | "webStorageEnabled": false, 17 | "browserstack.debug": false, 18 | "browserstack.timezone": "UTC", 19 | "browserstack.local": false 20 | }, 21 | "env": { 22 | "app": true, 23 | "device": "mobile", 24 | "os": "android" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test_root/webdriver/capabilities_android_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": { 3 | "realMobile": "true", 4 | "autoAcceptAlerts": "true", 5 | "browserstack.appium_version": "1.16.0", 6 | "noReset": false, 7 | "fullReset": true, 8 | "autoGrantPermissions": true, 9 | "deviceReadyTimeout": 60, 10 | "newCommandTimeout": 120, 11 | "launchTimeout": 60000, 12 | "appWaitDuration": 60000, 13 | "webkitResponseTimeout": 60000, 14 | "databaseEnabled": false, 15 | "webStorageEnabled": false, 16 | "browserstack.debug": false, 17 | "browserstack.timezone": "UTC", 18 | "browserstack.local": false 19 | }, 20 | "env": { 21 | "app": false, 22 | "device": "mobile", 23 | "os": "android" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test_root/webdriver/capabilities_ios_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": { 3 | "app": "bs://", 4 | "device": "iPhone XS", 5 | "os_version": "13", 6 | "realMobile": "true", 7 | "noReset": false, 8 | "fullReset": true, 9 | "autoGrantPermissions": true, 10 | "deviceReadyTimeout": 60, 11 | "newCommandTimeout": 120, 12 | "launchTimeout": 60000, 13 | "appWaitDuration": 60000, 14 | "webkitResponseTimeout": 60000, 15 | "databaseEnabled": false, 16 | "webStorageEnabled": false, 17 | "browserstack.debug": false, 18 | "browserstack.timezone": "UTC", 19 | "browserstack.local": false 20 | }, 21 | "env": { 22 | "app": true, 23 | "device": "mobile", 24 | "os": "ios" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test_root/webdriver/capabilities_ios_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": { 3 | "realMobile": "true", 4 | "autoAcceptAlerts": "true", 5 | "browserstack.appium_version": "1.16.0", 6 | "deviceReadyTimeout": 60, 7 | "newCommandTimeout": 120, 8 | "launchTimeout": 60000, 9 | "appWaitDuration": 60000, 10 | "webkitResponseTimeout": 60000, 11 | "databaseEnabled": false, 12 | "webStorageEnabled": false, 13 | "browserstack.debug": false, 14 | "browserstack.timezone": "UTC", 15 | "browserstack.local": false 16 | }, 17 | "env": { 18 | "app": false, 19 | "device": "mobile", 20 | "os": "ios" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test_root/webdriver/capabilities_web.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": { 3 | "os": "Windows", 4 | "os_version": "10", 5 | "resolution": "1920x1080", 6 | "browserstack.debug": false, 7 | "browserstack.local": "false", 8 | "browserstack.timezone": "UTC", 9 | "realMobile": true, 10 | "deviceOrientation": "landscape" 11 | }, 12 | "env": { 13 | "app": false, 14 | "device": "desktop", 15 | "os": "windows" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test_root/webdriver/custom_commands.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=import-outside-toplevel 2 | # pylint: disable=unused-variable 3 | import base64 4 | from io import BytesIO 5 | from time import sleep 6 | 7 | import cv2 8 | import numpy as np 9 | from PIL import Image 10 | from selenium.webdriver import ActionChains 11 | 12 | 13 | def add_custom_commands(): 14 | from selenium.webdriver.remote.webdriver import WebDriver 15 | from selenium.webdriver.remote.webelement import WebElement 16 | 17 | @add_method(WebDriver) 18 | def shadow_find_element(self, css_selector): 19 | return self.execute_script('return document.shadowRoot.querySelector(arguments[0])', css_selector) 20 | 21 | @add_method(WebDriver) 22 | def shadow_cascade_find_element(self, *args): 23 | script = 'return document' 24 | for arg in args: 25 | script += '.querySelector("%s").shadowRoot' % arg 26 | script = script[:-11] + ';' 27 | return self.execute_script(script) 28 | 29 | @add_method(WebDriver) 30 | def shadow_find_elements(self, css_selector): 31 | return self.execute_script('return document.shadowRoot.querySelectorAll(arguments[0])', css_selector) 32 | 33 | @add_method(WebElement) 34 | def shadow_find_element(self, css_selector): 35 | return self.parent.execute_script('return arguments[0].shadowRoot.querySelector(arguments[1])', self, css_selector) 36 | 37 | @add_method(WebElement) 38 | def shadow_cascade_find_element(self, *args): 39 | script = 'return %s' % self 40 | for arg in args: 41 | script += '.shadowRoot.querySelector("%s")' % arg 42 | return self.parent.execute_script(script) 43 | 44 | @add_method(WebElement) 45 | def shadow_find_elements(self, css_selector): 46 | return self.parent.execute_script('return arguments[0].shadowRoot.querySelectorAll(arguments[1])', self, css_selector) 47 | 48 | 49 | def add_method(cls): 50 | def decorator(func): 51 | from functools import wraps 52 | @wraps(func) 53 | def wrapper(self, *args, **kwargs): 54 | return func(self, *args, **kwargs) 55 | 56 | setattr(cls, func.__name__, wrapper) 57 | # Note we are not binding func, but wrapper which accepts self but does exactly the same as func 58 | return func # returning func means func can still be used normally 59 | 60 | return decorator 61 | -------------------------------------------------------------------------------- /test_root/webdriver/custom_expected_conditions.py: -------------------------------------------------------------------------------- 1 | from selenium.common.exceptions import WebDriverException 2 | 3 | 4 | # pylint: disable=invalid-name 5 | class is_webview_present: 6 | def __init__(self, driver): 7 | self.driver = driver 8 | 9 | def __call__(self, driver): 10 | print('Printing app contexts: ' + ' / '.join(self.driver.contexts)) 11 | if self.driver.desired_capabilities['platformName'].lower() == 'ios': 12 | return any('WEBVIEW_' in context for context in self.driver.contexts) 13 | if self.driver.desired_capabilities['platformName'].lower() == 'android': 14 | return any('WEBVIEW_com.' in context for context in self.driver.contexts) 15 | return None 16 | 17 | 18 | # pylint: disable=invalid-name 19 | class is_ionic_loaded: 20 | def __init__(self, driver): 21 | self.driver = driver 22 | 23 | def __call__(self, driver): 24 | return self.driver.execute_script('return Ionic != undefined && Ionic.platform != undefined;') 25 | 26 | 27 | # pylint: disable=invalid-name 28 | class visibility_of_child_element_located: 29 | """ An expectation for checking that a child element, known to be present on the 30 | DOM of a page, is visible. Visibility means that the element is not only 31 | displayed but also has a height and width that is greater than 0. 32 | element is the WebElement 33 | returns the (same) WebElement once it is visible 34 | """ 35 | 36 | def __init__(self, parent_element, locator): 37 | self.parent_element = parent_element 38 | self.locator = locator 39 | 40 | def __call__(self, driver): 41 | try: 42 | return _child_element_if_visible(self.parent_element.find_element(*self.locator)) 43 | except WebDriverException: 44 | return False 45 | 46 | 47 | # pylint: disable=invalid-name 48 | class invisibility_of_child_element_located: 49 | """ An Expectation for checking that an element is either invisible or not 50 | present on the DOM. 51 | """ 52 | 53 | def __init__(self, parent_element, locator): 54 | self.parent_element = parent_element 55 | self.locator = locator 56 | 57 | def __call__(self, driver): 58 | try: 59 | return _child_element_if_visible(self.parent_element.find_element(*self.locator), False) 60 | except WebDriverException: 61 | return True 62 | 63 | 64 | # pylint: disable=invalid-name 65 | class wait_for_the_attribute_value: 66 | def __init__(self, element, attribute, value): 67 | self.element = element 68 | self.attribute = attribute 69 | self.value = value 70 | 71 | def __call__(self, driver): 72 | try: 73 | element_attribute = self.element.get_attribute(self.attribute) 74 | return element_attribute == self.value 75 | except WebDriverException: 76 | return False 77 | 78 | 79 | # pylint: disable=invalid-name 80 | class wait_for_the_attribute_contain_value: 81 | def __init__(self, element, attribute, value): 82 | self.element = element 83 | self.attribute = attribute 84 | self.value = value 85 | 86 | def __call__(self, driver): 87 | try: 88 | element_attribute = self.element.get_attribute(self.attribute) 89 | return self.value in element_attribute 90 | except WebDriverException: 91 | return False 92 | 93 | 94 | # pylint: disable=invalid-name 95 | class wait_for_condition: 96 | def __init__(self, condition): 97 | self.condition = condition 98 | 99 | def __call__(self, driver): 100 | return self.condition 101 | 102 | 103 | def _child_element_if_visible(element, visibility=True): 104 | return element if element.is_displayed() == visibility else False 105 | -------------------------------------------------------------------------------- /test_root/webdriver/custom_wait.py: -------------------------------------------------------------------------------- 1 | import time 2 | from time import sleep 3 | 4 | import pytest 5 | from assertpy import assert_that 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from selenium.webdriver.support.wait import WebDriverWait 9 | 10 | from webdriver import custom_expected_conditions as CEC 11 | 12 | 13 | class CustomWait: 14 | def __init__(self, driver): 15 | self.driver = driver 16 | 17 | def wait_for_element_present(self, by=By.XPATH, value=None, text=None, time_to_wait=10): 18 | if text is not None: 19 | value = value % text 20 | wait = WebDriverWait(self.driver, time_to_wait) 21 | return wait.until(EC.presence_of_element_located((by, value))) 22 | 23 | def wait_for_element_visible(self, by=By.XPATH, value=None, text=None, time_to_wait=10): 24 | if text is not None: 25 | value = value % text 26 | wait = WebDriverWait(self.driver, time_to_wait) 27 | return wait.until(EC.visibility_of_element_located((by, value))) 28 | 29 | def wait_for_element_not_visible(self, by=By.XPATH, value=None, text=None): 30 | sleep(2) 31 | if text is not None: 32 | value = value % text 33 | 34 | wait = WebDriverWait(self.driver, 5) 35 | self.driver.implicitly_wait(5) 36 | result = wait.until(EC.invisibility_of_element_located((by, value))) 37 | # pylint: disable=no-member 38 | self.driver.implicitly_wait(pytest.globalDict['implicit_wait_time']) 39 | return result 40 | 41 | def wait_for_element_clickable(self, by=By.XPATH, value=None, text=None): 42 | if text is not None: 43 | value = value % text 44 | wait = WebDriverWait(self.driver, 10) 45 | return wait.until(EC.element_to_be_clickable((by, value))) 46 | 47 | def wait_for_child_element_visible(self, parent_element, by=By.XPATH, value=None, text=None): 48 | if text is not None: 49 | value = value % text 50 | wait = WebDriverWait(self.driver, 10) 51 | return wait.until(CEC.visibility_of_child_element_located(parent_element, (by, value))) 52 | 53 | def wait_for_child_element_not_visible(self, parent_element, by=By.XPATH, value=None, text=None): 54 | if text is not None: 55 | value = value % text 56 | 57 | wait = WebDriverWait(self.driver, 5) 58 | self.driver.implicitly_wait(5) 59 | result = wait.until(CEC.invisibility_of_child_element_located(parent_element, (by, value))) 60 | # pylint: disable=no-member 61 | self.driver.implicitly_wait(pytest.globalDict['implicit_wait_time']) 62 | return result 63 | 64 | def wait_for_the_attribute_value(self, element, attribute, value, time_to_wait=10): 65 | wait = WebDriverWait(self.driver, time_to_wait) 66 | return wait.until(CEC.wait_for_the_attribute_value(element, attribute, value)) 67 | 68 | def wait_for_the_attribute_contain_value(self, element, attribute, value, time_to_wait=10): 69 | wait = WebDriverWait(self.driver, time_to_wait) 70 | return wait.until(CEC.wait_for_the_attribute_contain_value(element, attribute, value)) 71 | 72 | @staticmethod 73 | def wait_until(some_predicate, timeout=20, period=0.25, description=""): 74 | must_end = time.time() + timeout 75 | while time.time() < must_end: 76 | if some_predicate(): 77 | return True 78 | time.sleep(period) 79 | assert_that(some_predicate(), description=description).is_true() 80 | 81 | # pylint: disable=no-member 82 | @staticmethod 83 | def static_wait(period=1): 84 | sleep(period) 85 | -------------------------------------------------------------------------------- /test_root/webdriver/local_storage.py: -------------------------------------------------------------------------------- 1 | class LocalStorage: 2 | def __init__(self, driver): 3 | self.driver = driver 4 | 5 | def __len__(self): 6 | return self.driver.execute_script("return window.localStorage.length;") 7 | 8 | def items(self): 9 | return self.driver.execute_script( 10 | "var ls = window.localStorage, items = {}; " 11 | "for (var i = 0, k; i < ls.length; ++i) " 12 | " items[k = ls.key(i)] = ls.getItem(k); " 13 | "return items; ") 14 | 15 | def keys(self): 16 | return self.driver.execute_script( 17 | "var ls = window.localStorage, keys = []; " 18 | "for (var i = 0; i < ls.length; ++i) " 19 | " keys[i] = ls.key(i); " 20 | "return keys; ") 21 | 22 | def get(self, key): 23 | return self.driver.execute_script("return window.localStorage.getItem(arguments[0]);", key) 24 | 25 | def set(self, key, value): 26 | self.driver.execute_script("window.localStorage.setItem(arguments[0], arguments[1]);", key, value) 27 | 28 | def has(self, key): 29 | return key in self.keys() 30 | 31 | def remove(self, key): 32 | self.driver.execute_script("window.localStorage.removeItem(arguments[0]);", key) 33 | 34 | def clear(self): 35 | self.driver.execute_script("window.localStorage.clear();") 36 | 37 | def __getitem__(self, key): 38 | value = self.get(key) 39 | if value is None: 40 | raise KeyError(key) 41 | return value 42 | 43 | def __setitem__(self, key, value): 44 | self.set(key, value) 45 | 46 | def __contains__(self, key): 47 | return key in self.keys() 48 | 49 | def __iter__(self): 50 | return self.items().__iter__() 51 | 52 | def __repr__(self): 53 | return self.items().__str__() 54 | --------------------------------------------------------------------------------