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