├── .gitignore ├── .python-version ├── LICENSE ├── README.rst ├── config.local.env.example ├── config.prod.env ├── config.py ├── docs └── resources │ ├── allure-report-pageobjects-test-body-sub-steps.png │ ├── allure-report-pageobjects-test-body.png │ └── allure-report-straightforward-test-body.png ├── etc └── selenoid │ ├── browsers.json │ └── compose.yaml ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── run ├── clear_reports.sh ├── lint_black.sh ├── print_docker_images_for_selenoid_browsers.sh ├── pull_docker_images_for_selenoid_browsers.sh ├── remove_docker_images_for_selenoid_browsers.sh ├── serve_report.sh ├── serve_report_in_background.sh ├── serve_report_stop.sh ├── start_up_selenoid.sh ├── start_up_selenoid_with_ui.sh ├── stop_and_remove_selenoids.sh ├── stop_selenoid.sh ├── tests_and_reserve_report.sh ├── tests_and_reserve_report_in_background.sh ├── tests_in_firefox_headless.sh ├── tests_marked_by.sh ├── tests_marked_by_not_smoke.sh ├── tests_marked_by_smoke.sh ├── tests_parallelized_auto.sh ├── tests_parallelized_in.sh └── tests_remote_on.sh ├── tests ├── __init__.py ├── conftest.py ├── test_search_engines_should_search.py └── test_self.py └── web_test ├── __init__.py ├── alternative ├── __init__.py ├── pytest │ ├── __init__.py │ └── project │ │ ├── __init__.py │ │ └── settings.py └── settings │ ├── __init__.py │ ├── source.py │ └── sourced.py ├── app.py ├── assist ├── __init__.py ├── allure │ ├── __init__.py │ ├── aaa.py │ ├── gherkin.py │ └── report.py ├── project.py ├── python │ ├── __init__.py │ ├── etc.py │ ├── fp.py │ └── monkey.py ├── selene │ ├── __init__.py │ ├── report.py │ └── shared │ │ ├── __init__.py │ │ └── hook.py ├── selenium │ ├── __init__.py │ └── typing.py └── webdriver_manager │ ├── __init__.py │ ├── set_up.py │ └── supported.py ├── pages ├── __init__.py ├── ecosia.py ├── github.py ├── google.py ├── pypi.py ├── python_org.py └── searchencrypt.py └── test_markers ├── __init__.py └── mark.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | .idea 4 | reports/ 5 | config.local.env 6 | *.log -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.5 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Iakiv Kramarenko 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.rst: -------------------------------------------------------------------------------- 1 | Selene + Pytest tests project template 2 | ====================================== 3 | 4 | Overview and general guidelines 5 | ------------------------------- 6 | 7 | This is a template project. It's supposed to be cloned or downloaded and edited according to your needs. 8 | 9 | The project itself reflects an implementation of acceptance web ui tests for a "web", i.e. as "application under test" we consider here the "whole web", under "root pages" we mean "web sites", under "sub-pages" we mean "web site pages". Take this into account when applying the template to your context;) 10 | 11 | Hence, download it, rename the project folder to something like ``my-product-test``, then rename the modules correspondingly (like ``web_test`` to ``my_product_test``, etc...), edit the "project" section in ``pyproject.toml`` to something like:: 12 | 13 | [tool.poetry] 14 | name = "my-product-test" 15 | version = "0.1.0" 16 | description = "my product web ui acceptance tests" 17 | authors = ["Your Name "] 18 | 19 | ... 20 | 21 | And you should be ready to go ;) 22 | 23 | You can also consider keeping the template examples for some time. Maybe just leave ``web_test`` package as it is, and add your own ``my_product_test`` module. Then duplicate the ``tests`` folder, edit the copy as you need, while keeping the original ``tests`` folder under another name, e.g. ``examples``;) 24 | 25 | Pay attention to a lot of comments and docstrings in code to understand what happens. You will find different styles of implementing page-objects. Probably you will need only one style in your case. So read all explanations and choose the one that fits your needs. 26 | 27 | 28 | Installation 29 | ------------ 30 | 31 | Given installed: 32 | 33 | * `pyenv + python `_ 34 | * `poetry `_ 35 | * `allure `_ 36 | 37 | Clone template with ``git clone`` 38 | 39 | Do in your favourite terminal:: 40 | 41 | cd $YOUR_PROJECT_FOLDER_PATH 42 | poetry install 43 | poetry shell 44 | 45 | 46 | So you can run your tests via:: 47 | 48 | pytest tests/ 49 | 50 | The `./run `_ folder contains a bunch shell scripts as examples of different variations of running tests and other tasks, like setting selenoid up & running. To make them runnable do in your unix terminal inside ``$YOUR_PROJECT_FOLDER_PATH`` :: 51 | 52 | chmod -R u+x run 53 | 54 | 55 | Now you can do something like:: 56 | 57 | ./run/tests_and_reserve_report.sh 58 | 59 | or even passing additional args, for example to filter only tests marked as "smoke":: 60 | 61 | ./run/tests_and_reserve_report.sh -m smoke 62 | 63 | Some scripts expects at least one argument, like in this example:: 64 | 65 | ./run/tests_marked_by.sh smoke 66 | 67 | Or this:: 68 | 69 | ./run/tests_remote_on.sh http://127.0.0.1:4444/wd/hub 70 | 71 | 72 | Some needs docker and docker-compose to be preinstalled, so you can pull all needed browser images:: 73 | 74 | ./run/pull_docker_images_for_selenoid_browsers.sh 75 | 76 | And get selenoid up and running:: 77 | 78 | ./run/start_up/selenoid_with_ui.sh 79 | 80 | You might want to check corresponding `etc/selenoid/compose.yaml `_ docker-dompose file and tune it to your needs. 81 | 82 | Check all scripts to see more examples of the framework usage, tune and remove not needed ones, according to your needs. 83 | 84 | 85 | So you are ready to go;) 86 | Feel free to use your editor of choice to surf the code base, and tune it correspondingly. 87 | 88 | Details 89 | ------- 90 | 91 | Features supported: 92 | 93 | * parallel execution via pytest-xdist 94 | 95 | * closer integration of pytest markers and allure 96 | 97 | * rerun tests via pytest-rerunfailures 98 | 99 | * project configuration management by pydantic 100 | 101 | * local and remote webdriver management 102 | 103 | * allure report 104 | 105 | * example of reported test case in straightforward/PageObejctLess style 106 | 107 | * code:: 108 | 109 | def test_duckduckgo(): 110 | browser.open('https://duckduckgo.com/') 111 | 112 | browser.element('[name=q]')\ 113 | .should(be.blank)\ 114 | .type('yashaka selene python').press_enter() 115 | browser.all('.result__body') \ 116 | .should(have.size_greater_than(5)) \ 117 | .first.should(have.text('User-oriented Web UI browser tests')) 118 | 119 | browser.all('.result__body').first.element('a').click() 120 | browser.should(have.title_containing('yashaka/selene')) 121 | 122 | * reported test body 123 | |allure-report-straightforward-test-body| 124 | 125 | * example of reported test case with PageObejcts 126 | 127 | * code:: 128 | 129 | def test_ecosia(): 130 | ecosia.open() 131 | 132 | ecosia.search('selene python') 133 | ecosia.results\ 134 | .should_have_size_at_least(5)\ 135 | .should_have_text(0, 'User-oriented Web UI browser tests') 136 | 137 | ecosia.results.follow_link(0) 138 | github.should_be_on('yashaka/selene') 139 | 140 | * reported 141 | 142 | * test body 143 | |allure-report-pageobjects-test-body| 144 | 145 | * sub-steps 146 | |allure-report-pageobjects-test-body-sub-steps| 147 | 148 | * reporting steps with automatic rendering of 149 | 150 | * underscores to spaces 151 | * inline params 152 | * context of step-function (object, class or module) 153 | * actions on raw selene elements 154 | 155 | * last screenshot and page-source are attached to test body on failure 156 | 157 | * use allure webserver to see reports with webui:: 158 | 159 | allure serve reports 160 | 161 | 162 | More resources and useful links 163 | -------------------------- 164 | 165 | - `Pytest basic patterns and examples `_ 166 | 167 | TODO list 168 | --------- 169 | 170 | This template is yet in progress. See `opened issues `_ for all todos. 171 | 172 | 173 | .. |allure-report-pageobjects-test-body| image:: ./docs/resources/allure-report-pageobjects-test-body.png 174 | .. |allure-report-pageobjects-test-body-sub-steps| image:: ./docs/resources/allure-report-pageobjects-test-body-sub-steps.png 175 | .. |allure-report-straightforward-test-body| image:: ./docs/resources/allure-report-straightforward-test-body.png 176 | -------------------------------------------------------------------------------- /config.local.env.example: -------------------------------------------------------------------------------- 1 | timeout=1 2 | -------------------------------------------------------------------------------- /config.prod.env: -------------------------------------------------------------------------------- 1 | timeout=4 2 | browser_name=chrome 3 | headless=True 4 | #base_url='http://autotest.how/' 5 | # ;P) 6 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional 2 | 3 | from web_test import assist 4 | 5 | EnvContext = Literal['local', 'prod'] 6 | """ 7 | Extend it in accordance with your conditions. 8 | 9 | It defines valid env contexts for better IDE support when dealing with them. 10 | - but your Editor of Choice should support it;) 11 | - e.g. PyCharm support only from versions >= 2019.3 12 | """ 13 | 14 | 15 | import pydantic 16 | from web_test.assist.webdriver_manager import supported 17 | 18 | 19 | class Settings(pydantic.BaseSettings): 20 | """ 21 | Implemented based on pydantic modeling, see: 22 | - https://pydantic-docs.helpmanual.io/usage/settings/ for docs 23 | - https://rednafi.github.io/digressions/python/2020/06/03/python-configs.html 24 | for some further conf management ideas 25 | 26 | Some recommendations: 27 | - add type hints, even if the type can be inferred from the default value 28 | to store field ordering (just in case... see more on this in docs) 29 | 30 | Things that could be here: 31 | ========================== 32 | parallelize: bool = True 33 | parallelize_in_exact_subprocess_number: Optional[int] = None 34 | ''' 35 | though it would be consistent and pretty readable to have all potential 36 | project options at one place, 37 | the more KISS way is just to keep things as they are in pytest 38 | and use -n OR -n auto when parallelization is needed 39 | 40 | In future we can play with this and implement it though external plugin 41 | (yet stored locally in this project) 42 | ''' 43 | 44 | """ 45 | 46 | context: EnvContext = 'local' 47 | """ 48 | controls the environment context, 49 | will result in where to load other settings from 50 | 51 | 'local' is the default value and a special one... 52 | - the corresponding config.local.env file is ignored in .gitignore 53 | - so if absent (e.g. on first git clone) 54 | - the default values defined below will be used 55 | - if present (e.g. once copied&edited from config.local.env.example) 56 | - then will be used only locally by you, 57 | not mixing stuff for other team members ;) 58 | """ 59 | 60 | base_url: str = '' 61 | timeout: float = 6.0 62 | browser_name: supported.BrowserName = 'chrome' # todo: consider renaming to browserName for consistency with capability 63 | headless: bool = False 64 | window_width: int = 1440 65 | window_height: int = 900 66 | maximize_window: bool = False 67 | """ 68 | Should be False by default, 69 | because considered a bad practice 70 | to write tests for not predictable window size. 71 | Maximized window will have different size on different machines, 72 | that can make tests unstable. 73 | """ 74 | remote_url: Optional[str] = None 75 | remote_version: Optional[str] = None 76 | remote_platform: Optional[str] = None 77 | remote_enableVNC: bool = True 78 | remote_screenResolution: str = '1920x1080x24' 79 | remote_enableVideo: bool = False 80 | remote_enableLog: bool = True 81 | """ 82 | named not in snake_case for consistency with original capability name 83 | """ 84 | hold_browser_open: bool = False 85 | save_page_source_on_failure: bool = True 86 | author: str = 'yashaka' 87 | 88 | @classmethod 89 | def in_context(cls, env: Optional[EnvContext] = None) -> 'Settings': 90 | """ 91 | factory method to init Settings with values from corresponding .env file 92 | """ 93 | asked_or_current = env or cls().context 94 | return cls(_env_file=assist.project.abs_path_from_project( 95 | f'config.{asked_or_current}.env' 96 | )) 97 | 98 | 99 | settings = Settings.in_context() 100 | """ 101 | USAGE 102 | ===== 103 | import config 104 | browser.config.timeout = config.settings.timeout 105 | 106 | =================================== 107 | Alternative implementation 108 | - to separate 109 | original timeout on process start 110 | from current timeout 111 | - just in case;) 112 | =================================== 113 | # config.py 114 | on_start = Settings.in_context() 115 | 116 | 117 | def get(): 118 | return Settings.in_context() 119 | 120 | # conftest.py 121 | import config 122 | # ... 123 | browser.config.timeout = config.on_start.timeout 124 | # ... 125 | browser.config.timeout = config.get().timeout 126 | # – or same: – 127 | # browser.config.timeout = config.Settings.in_context().timeout 128 | 129 | """ 130 | 131 | 132 | if __name__ == '__main__': 133 | """ 134 | for debugging purposes 135 | to check the actual config values on start 136 | when simply running `python config.py` 137 | """ 138 | print(settings.__repr__()) 139 | -------------------------------------------------------------------------------- /docs/resources/allure-report-pageobjects-test-body-sub-steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/docs/resources/allure-report-pageobjects-test-body-sub-steps.png -------------------------------------------------------------------------------- /docs/resources/allure-report-pageobjects-test-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/docs/resources/allure-report-pageobjects-test-body.png -------------------------------------------------------------------------------- /docs/resources/allure-report-straightforward-test-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/docs/resources/allure-report-straightforward-test-body.png -------------------------------------------------------------------------------- /etc/selenoid/browsers.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": { 3 | "default": "89.0", 4 | "versions": { 5 | "89.0": { 6 | "image": "selenoid/vnc_chrome:89.0", 7 | "port": "4444", 8 | "path": "/" 9 | } 10 | } 11 | }, 12 | "firefox": { 13 | "default": "86.0", 14 | "versions": { 15 | "86.0": { 16 | "image": "selenoid/vnc_firefox:86.0", 17 | "port": "4444", 18 | "path": "/wd/hub" 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /etc/selenoid/compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | selenoid: 5 | image: "aerokube/selenoid:1.10.1" 6 | network_mode: bridge 7 | restart: always 8 | ports: # if 4444 is already used on your host machine 9 | # then change it to something else, like 10 | # - "4447:4444" 11 | - "4444:4444" 12 | volumes: 13 | - "$PWD/etc/selenoid:/etc/selenoid/" 14 | # assumed contains etc/selenoid/browsers.json 15 | # where is the directory 16 | # from where you run > docker-compose -f etc/selenoid/compose.yaml -d up 17 | - "/var/run/docker.sock:/var/run/docker.sock" 18 | command: ["-conf", "/etc/selenoid/browsers.json", "-limit", "4"] 19 | 20 | selenoid-ui: 21 | image: "aerokube/selenoid-ui:1.10.3" 22 | network_mode: bridge 23 | restart: always 24 | depends_on: 25 | - selenoid 26 | links: # todo: refactor because links are deprecated 27 | - selenoid 28 | ports: # if 8080 is already used on your host machine 29 | # then change it to something else, like 30 | # - "8090:8080" 31 | - "8080:8080" 32 | command: ["--selenoid-uri", "http://selenoid:4444"] -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "allure-pytest" 3 | version = "2.10.0" 4 | description = "Allure pytest integration" 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [package.dependencies] 10 | allure-python-commons = "2.10.0" 11 | pytest = ">=4.5.0" 12 | six = ">=1.9.0" 13 | 14 | [[package]] 15 | name = "allure-python-commons" 16 | version = "2.10.0" 17 | description = "Common module for integrate allure with python-based frameworks" 18 | category = "main" 19 | optional = false 20 | python-versions = ">=3.5" 21 | 22 | [package.dependencies] 23 | attrs = ">=16.0.0" 24 | pluggy = ">=0.4.0" 25 | six = ">=1.9.0" 26 | 27 | [[package]] 28 | name = "async-generator" 29 | version = "1.10" 30 | description = "Async generators and context managers for Python 3.5+" 31 | category = "main" 32 | optional = false 33 | python-versions = ">=3.5" 34 | 35 | [[package]] 36 | name = "attrs" 37 | version = "22.1.0" 38 | description = "Classes Without Boilerplate" 39 | category = "main" 40 | optional = false 41 | python-versions = ">=3.5" 42 | 43 | [package.extras] 44 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] 45 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 46 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] 47 | tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] 48 | 49 | [[package]] 50 | name = "black" 51 | version = "22.8.0" 52 | description = "The uncompromising code formatter." 53 | category = "dev" 54 | optional = false 55 | python-versions = ">=3.6.2" 56 | 57 | [package.dependencies] 58 | click = ">=8.0.0" 59 | mypy-extensions = ">=0.4.3" 60 | pathspec = ">=0.9.0" 61 | platformdirs = ">=2" 62 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 63 | 64 | [package.extras] 65 | colorama = ["colorama (>=0.4.3)"] 66 | d = ["aiohttp (>=3.7.4)"] 67 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 68 | uvloop = ["uvloop (>=0.15.2)"] 69 | 70 | [[package]] 71 | name = "certifi" 72 | version = "2022.6.15" 73 | description = "Python package for providing Mozilla's CA Bundle." 74 | category = "main" 75 | optional = false 76 | python-versions = ">=3.6" 77 | 78 | [[package]] 79 | name = "cffi" 80 | version = "1.15.1" 81 | description = "Foreign Function Interface for Python calling C code." 82 | category = "main" 83 | optional = false 84 | python-versions = "*" 85 | 86 | [package.dependencies] 87 | pycparser = "*" 88 | 89 | [[package]] 90 | name = "charset-normalizer" 91 | version = "2.1.1" 92 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 93 | category = "main" 94 | optional = false 95 | python-versions = ">=3.6.0" 96 | 97 | [package.extras] 98 | unicode_backport = ["unicodedata2"] 99 | 100 | [[package]] 101 | name = "click" 102 | version = "8.1.3" 103 | description = "Composable command line interface toolkit" 104 | category = "dev" 105 | optional = false 106 | python-versions = ">=3.7" 107 | 108 | [package.dependencies] 109 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 110 | 111 | [[package]] 112 | name = "colorama" 113 | version = "0.4.5" 114 | description = "Cross-platform colored terminal text." 115 | category = "main" 116 | optional = false 117 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 118 | 119 | [[package]] 120 | name = "cryptography" 121 | version = "37.0.4" 122 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 123 | category = "main" 124 | optional = false 125 | python-versions = ">=3.6" 126 | 127 | [package.dependencies] 128 | cffi = ">=1.12" 129 | 130 | [package.extras] 131 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] 132 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 133 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 134 | sdist = ["setuptools_rust (>=0.11.4)"] 135 | ssh = ["bcrypt (>=3.1.5)"] 136 | test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] 137 | 138 | [[package]] 139 | name = "execnet" 140 | version = "1.9.0" 141 | description = "execnet: rapid multi-Python deployment" 142 | category = "main" 143 | optional = false 144 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 145 | 146 | [package.extras] 147 | testing = ["pre-commit"] 148 | 149 | [[package]] 150 | name = "future" 151 | version = "0.18.2" 152 | description = "Clean single-source support for Python 3 and 2" 153 | category = "main" 154 | optional = false 155 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 156 | 157 | [[package]] 158 | name = "h11" 159 | version = "0.13.0" 160 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 161 | category = "main" 162 | optional = false 163 | python-versions = ">=3.6" 164 | 165 | [[package]] 166 | name = "idna" 167 | version = "3.3" 168 | description = "Internationalized Domain Names in Applications (IDNA)" 169 | category = "main" 170 | optional = false 171 | python-versions = ">=3.5" 172 | 173 | [[package]] 174 | name = "iniconfig" 175 | version = "1.1.1" 176 | description = "iniconfig: brain-dead simple config-ini parsing" 177 | category = "main" 178 | optional = false 179 | python-versions = "*" 180 | 181 | [[package]] 182 | name = "mypy-extensions" 183 | version = "0.4.3" 184 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 185 | category = "dev" 186 | optional = false 187 | python-versions = "*" 188 | 189 | [[package]] 190 | name = "outcome" 191 | version = "1.2.0" 192 | description = "Capture the outcome of Python function calls." 193 | category = "main" 194 | optional = false 195 | python-versions = ">=3.7" 196 | 197 | [package.dependencies] 198 | attrs = ">=19.2.0" 199 | 200 | [[package]] 201 | name = "packaging" 202 | version = "21.3" 203 | description = "Core utilities for Python packages" 204 | category = "main" 205 | optional = false 206 | python-versions = ">=3.6" 207 | 208 | [package.dependencies] 209 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 210 | 211 | [[package]] 212 | name = "pathspec" 213 | version = "0.10.1" 214 | description = "Utility library for gitignore style pattern matching of file paths." 215 | category = "dev" 216 | optional = false 217 | python-versions = ">=3.7" 218 | 219 | [[package]] 220 | name = "platformdirs" 221 | version = "2.5.2" 222 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 223 | category = "dev" 224 | optional = false 225 | python-versions = ">=3.7" 226 | 227 | [package.extras] 228 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] 229 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 230 | 231 | [[package]] 232 | name = "pluggy" 233 | version = "1.0.0" 234 | description = "plugin and hook calling mechanisms for python" 235 | category = "main" 236 | optional = false 237 | python-versions = ">=3.6" 238 | 239 | [package.extras] 240 | dev = ["pre-commit", "tox"] 241 | testing = ["pytest", "pytest-benchmark"] 242 | 243 | [[package]] 244 | name = "py" 245 | version = "1.11.0" 246 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 250 | 251 | [[package]] 252 | name = "pycparser" 253 | version = "2.21" 254 | description = "C parser in Python" 255 | category = "main" 256 | optional = false 257 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 258 | 259 | [[package]] 260 | name = "pydantic" 261 | version = "1.10.1" 262 | description = "Data validation and settings management using python type hints" 263 | category = "main" 264 | optional = false 265 | python-versions = ">=3.7" 266 | 267 | [package.dependencies] 268 | typing-extensions = ">=4.1.0" 269 | 270 | [package.extras] 271 | dotenv = ["python-dotenv (>=0.10.4)"] 272 | email = ["email-validator (>=1.0.3)"] 273 | 274 | [[package]] 275 | name = "pyopenssl" 276 | version = "22.0.0" 277 | description = "Python wrapper module around the OpenSSL library" 278 | category = "main" 279 | optional = false 280 | python-versions = ">=3.6" 281 | 282 | [package.dependencies] 283 | cryptography = ">=35.0" 284 | 285 | [package.extras] 286 | docs = ["sphinx", "sphinx-rtd-theme"] 287 | test = ["flaky", "pretend", "pytest (>=3.0.1)"] 288 | 289 | [[package]] 290 | name = "pyparsing" 291 | version = "3.0.9" 292 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 293 | category = "main" 294 | optional = false 295 | python-versions = ">=3.6.8" 296 | 297 | [package.extras] 298 | diagrams = ["jinja2", "railroad-diagrams"] 299 | 300 | [[package]] 301 | name = "pysocks" 302 | version = "1.7.1" 303 | description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." 304 | category = "main" 305 | optional = false 306 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 307 | 308 | [[package]] 309 | name = "pytest" 310 | version = "7.1.3" 311 | description = "pytest: simple powerful testing with Python" 312 | category = "main" 313 | optional = false 314 | python-versions = ">=3.7" 315 | 316 | [package.dependencies] 317 | attrs = ">=19.2.0" 318 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 319 | iniconfig = "*" 320 | packaging = "*" 321 | pluggy = ">=0.12,<2.0" 322 | py = ">=1.8.2" 323 | tomli = ">=1.0.0" 324 | 325 | [package.extras] 326 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 327 | 328 | [[package]] 329 | name = "pytest-forked" 330 | version = "1.4.0" 331 | description = "run tests in isolated forked subprocesses" 332 | category = "main" 333 | optional = false 334 | python-versions = ">=3.6" 335 | 336 | [package.dependencies] 337 | py = "*" 338 | pytest = ">=3.10" 339 | 340 | [[package]] 341 | name = "pytest-rerunfailures" 342 | version = "10.2" 343 | description = "pytest plugin to re-run tests to eliminate flaky failures" 344 | category = "main" 345 | optional = false 346 | python-versions = ">= 3.6" 347 | 348 | [package.dependencies] 349 | pytest = ">=5.3" 350 | setuptools = ">=40.0" 351 | 352 | [[package]] 353 | name = "pytest-xdist" 354 | version = "2.5.0" 355 | description = "pytest xdist plugin for distributed testing and loop-on-failing modes" 356 | category = "main" 357 | optional = false 358 | python-versions = ">=3.6" 359 | 360 | [package.dependencies] 361 | execnet = ">=1.1" 362 | pytest = ">=6.2.0" 363 | pytest-forked = "*" 364 | 365 | [package.extras] 366 | psutil = ["psutil (>=3.0)"] 367 | setproctitle = ["setproctitle"] 368 | testing = ["filelock"] 369 | 370 | [[package]] 371 | name = "python-dotenv" 372 | version = "0.15.0" 373 | description = "Add .env support to your django/flask apps in development and deployments" 374 | category = "main" 375 | optional = false 376 | python-versions = "*" 377 | 378 | [package.extras] 379 | cli = ["click (>=5.0)"] 380 | 381 | [[package]] 382 | name = "requests" 383 | version = "2.28.1" 384 | description = "Python HTTP for Humans." 385 | category = "main" 386 | optional = false 387 | python-versions = ">=3.7, <4" 388 | 389 | [package.dependencies] 390 | certifi = ">=2017.4.17" 391 | charset-normalizer = ">=2,<3" 392 | idna = ">=2.5,<4" 393 | urllib3 = ">=1.21.1,<1.27" 394 | 395 | [package.extras] 396 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 397 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 398 | 399 | [[package]] 400 | name = "selene" 401 | version = "2.0.0b8" 402 | description = "User-oriented browser tests in Python (Selenide port)" 403 | category = "main" 404 | optional = false 405 | python-versions = ">=3.7,<4.0" 406 | 407 | [package.dependencies] 408 | future = "*" 409 | selenium = "4.2.0" 410 | typing-extensions = "4.3.0" 411 | webdriver-manager = "3.7.0" 412 | 413 | [[package]] 414 | name = "selenium" 415 | version = "4.2.0" 416 | description = "" 417 | category = "main" 418 | optional = false 419 | python-versions = "~=3.7" 420 | 421 | [package.dependencies] 422 | trio = ">=0.17,<1.0" 423 | trio-websocket = ">=0.9,<1.0" 424 | urllib3 = {version = ">=1.26,<2.0", extras = ["secure", "socks"]} 425 | 426 | [[package]] 427 | name = "setuptools" 428 | version = "65.3.0" 429 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 430 | category = "main" 431 | optional = false 432 | python-versions = ">=3.7" 433 | 434 | [package.extras] 435 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 436 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 437 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 438 | 439 | [[package]] 440 | name = "six" 441 | version = "1.16.0" 442 | description = "Python 2 and 3 compatibility utilities" 443 | category = "main" 444 | optional = false 445 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 446 | 447 | [[package]] 448 | name = "sniffio" 449 | version = "1.3.0" 450 | description = "Sniff out which async library your code is running under" 451 | category = "main" 452 | optional = false 453 | python-versions = ">=3.7" 454 | 455 | [[package]] 456 | name = "sortedcontainers" 457 | version = "2.4.0" 458 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 459 | category = "main" 460 | optional = false 461 | python-versions = "*" 462 | 463 | [[package]] 464 | name = "tomli" 465 | version = "2.0.1" 466 | description = "A lil' TOML parser" 467 | category = "main" 468 | optional = false 469 | python-versions = ">=3.7" 470 | 471 | [[package]] 472 | name = "trio" 473 | version = "0.21.0" 474 | description = "A friendly Python library for async concurrency and I/O" 475 | category = "main" 476 | optional = false 477 | python-versions = ">=3.7" 478 | 479 | [package.dependencies] 480 | async-generator = ">=1.9" 481 | attrs = ">=19.2.0" 482 | cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} 483 | idna = "*" 484 | outcome = "*" 485 | sniffio = "*" 486 | sortedcontainers = "*" 487 | 488 | [[package]] 489 | name = "trio-websocket" 490 | version = "0.9.2" 491 | description = "WebSocket library for Trio" 492 | category = "main" 493 | optional = false 494 | python-versions = ">=3.5" 495 | 496 | [package.dependencies] 497 | async-generator = ">=1.10" 498 | trio = ">=0.11" 499 | wsproto = ">=0.14" 500 | 501 | [[package]] 502 | name = "typing-extensions" 503 | version = "4.3.0" 504 | description = "Backported and Experimental Type Hints for Python 3.7+" 505 | category = "main" 506 | optional = false 507 | python-versions = ">=3.7" 508 | 509 | [[package]] 510 | name = "urllib3" 511 | version = "1.26.12" 512 | description = "HTTP library with thread-safe connection pooling, file post, and more." 513 | category = "main" 514 | optional = false 515 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" 516 | 517 | [package.dependencies] 518 | certifi = {version = "*", optional = true, markers = "extra == \"secure\""} 519 | cryptography = {version = ">=1.3.4", optional = true, markers = "extra == \"secure\""} 520 | idna = {version = ">=2.0.0", optional = true, markers = "extra == \"secure\""} 521 | pyOpenSSL = {version = ">=0.14", optional = true, markers = "extra == \"secure\""} 522 | PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} 523 | urllib3-secure-extra = {version = "*", optional = true, markers = "extra == \"secure\""} 524 | 525 | [package.extras] 526 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 527 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 528 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 529 | 530 | [[package]] 531 | name = "urllib3-secure-extra" 532 | version = "0.1.0" 533 | description = "Marker library to detect whether urllib3 was installed with the deprecated [secure] extra" 534 | category = "main" 535 | optional = false 536 | python-versions = "*" 537 | 538 | [[package]] 539 | name = "webdriver-manager" 540 | version = "3.7.0" 541 | description = "Library provides the way to automatically manage drivers for different browsers" 542 | category = "main" 543 | optional = false 544 | python-versions = ">=3.6" 545 | 546 | [package.dependencies] 547 | python-dotenv = "*" 548 | requests = "*" 549 | 550 | [[package]] 551 | name = "wsproto" 552 | version = "1.2.0" 553 | description = "WebSockets state-machine based protocol implementation" 554 | category = "main" 555 | optional = false 556 | python-versions = ">=3.7.0" 557 | 558 | [package.dependencies] 559 | h11 = ">=0.9.0,<1" 560 | 561 | [metadata] 562 | lock-version = "1.1" 563 | python-versions = "^3.10" 564 | content-hash = "c33052b2d8763476d9b949e0e1bc794d0e0e788165e1dc774a8633aa75278a54" 565 | 566 | [metadata.files] 567 | allure-pytest = [ 568 | {file = "allure-pytest-2.10.0.tar.gz", hash = "sha256:3b2ab67629f4cbd8617abd817d2b22292c6eb7efd5584f992d1af8143aea6ee7"}, 569 | {file = "allure_pytest-2.10.0-py3-none-any.whl", hash = "sha256:08274096594758447db54c3b2c382526ee04f1fe12119cdaee92d2d93c84b530"}, 570 | ] 571 | allure-python-commons = [ 572 | {file = "allure-python-commons-2.10.0.tar.gz", hash = "sha256:d4d31344b0f0037a4a11e16b91b28cf0eeb23ffa0e50c27fcfc6aabe72212d3c"}, 573 | {file = "allure_python_commons-2.10.0-py3-none-any.whl", hash = "sha256:2a717e8ca8d296bf89cd57f38fc3c21893bd7ea8cd02a6ae5420e6d1a6eda5d0"}, 574 | ] 575 | async-generator = [ 576 | {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, 577 | {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, 578 | ] 579 | attrs = [ 580 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 581 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 582 | ] 583 | black = [ 584 | {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, 585 | {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, 586 | {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, 587 | {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, 588 | {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, 589 | {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, 590 | {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, 591 | {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, 592 | {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, 593 | {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, 594 | {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, 595 | {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, 596 | {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, 597 | {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, 598 | {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, 599 | {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, 600 | {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, 601 | {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, 602 | {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, 603 | {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, 604 | {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, 605 | {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, 606 | {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, 607 | ] 608 | certifi = [ 609 | {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, 610 | {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, 611 | ] 612 | cffi = [ 613 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, 614 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, 615 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, 616 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, 617 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, 618 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, 619 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, 620 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, 621 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, 622 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, 623 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, 624 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, 625 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, 626 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, 627 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, 628 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, 629 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, 630 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, 631 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, 632 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, 633 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, 634 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, 635 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, 636 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, 637 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, 638 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, 639 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, 640 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, 641 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, 642 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, 643 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, 644 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, 645 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, 646 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, 647 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, 648 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, 649 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, 650 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, 651 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, 652 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, 653 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, 654 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, 655 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, 656 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, 657 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, 658 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, 659 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, 660 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, 661 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, 662 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, 663 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, 664 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, 665 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, 666 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, 667 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, 668 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, 669 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, 670 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, 671 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, 672 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, 673 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, 674 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, 675 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, 676 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, 677 | ] 678 | charset-normalizer = [ 679 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 680 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 681 | ] 682 | click = [ 683 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 684 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 685 | ] 686 | colorama = [ 687 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 688 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 689 | ] 690 | cryptography = [ 691 | {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, 692 | {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, 693 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, 694 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, 695 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, 696 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, 697 | {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, 698 | {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, 699 | {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, 700 | {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, 701 | {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, 702 | {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, 703 | {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, 704 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, 705 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, 706 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, 707 | {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, 708 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, 709 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, 710 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, 711 | {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, 712 | {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, 713 | ] 714 | execnet = [ 715 | {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, 716 | {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, 717 | ] 718 | future = [ 719 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 720 | ] 721 | h11 = [ 722 | {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, 723 | {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, 724 | ] 725 | idna = [ 726 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 727 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 728 | ] 729 | iniconfig = [ 730 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 731 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 732 | ] 733 | mypy-extensions = [ 734 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 735 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 736 | ] 737 | outcome = [ 738 | {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"}, 739 | {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"}, 740 | ] 741 | packaging = [ 742 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 743 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 744 | ] 745 | pathspec = [ 746 | {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, 747 | {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, 748 | ] 749 | platformdirs = [ 750 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 751 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 752 | ] 753 | pluggy = [ 754 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 755 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 756 | ] 757 | py = [ 758 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 759 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 760 | ] 761 | pycparser = [ 762 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 763 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 764 | ] 765 | pydantic = [ 766 | {file = "pydantic-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221166d99726238f71adc4fa9f3e94063a10787574b966f86a774559e709ac5a"}, 767 | {file = "pydantic-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a90e85d95fd968cd7cae122e0d3e0e1f6613bc88c1ff3fe838ac9785ea4b1c4c"}, 768 | {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2157aaf5718c648eaec9e654a34179ae42ffc363dc3ad058538a4f3ecbd9341"}, 769 | {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6142246fc9adb51cadaeb84fb52a86f3adad4c6a7b0938a5dd0b1356b0088217"}, 770 | {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:60dad97a09b6f44690c05467a4f397b62bfc2c839ac39102819d6979abc2be0d"}, 771 | {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6f5bcb59d33ec46621dae76e714c53035087666cac80c81c9047a84f3ff93d0"}, 772 | {file = "pydantic-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:522906820cd60e63c7960ba83078bf2d2ad2dd0870bf68248039bcb1ec3eb0a4"}, 773 | {file = "pydantic-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d545c89d88bdd5559db17aeb5a61a26799903e4bd76114779b3bf1456690f6ce"}, 774 | {file = "pydantic-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad2374b5b3b771dcc6e2f6e0d56632ab63b90e9808b7a73ad865397fcdb4b2cd"}, 775 | {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90e02f61b7354ed330f294a437d0bffac9e21a5d46cb4cc3c89d220e497db7ac"}, 776 | {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc5ffe7bd0b4778fa5b7a5f825c52d6cfea3ae2d9b52b05b9b1d97e36dee23a8"}, 777 | {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7acb7b66ffd2bc046eaff0063df84c83fc3826722d5272adaeadf6252e17f691"}, 778 | {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e6786ed5faa559dea5a77f6d2de9a08d18130de9344533535d945f34bdcd42e"}, 779 | {file = "pydantic-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:c7bf8ff1d18186eb0cbe42bd9bfb4cbf7fde1fd01b8608925458990c21f202f0"}, 780 | {file = "pydantic-1.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14a5babda137a294df7ad5f220986d79bbb87fdeb332c6ded61ce19da7f5f3bf"}, 781 | {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5659cb9c6b3d27fc0067025c4f5a205f5e838232a4a929b412781117c2343d44"}, 782 | {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d70fb91b03c32d2e857b071a22a5225e6b625ca82bd2cc8dd729d88e0bd200"}, 783 | {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9a93be313e40f12c6f2cb84533b226bbe23d0774872e38d83415e6890215e3a6"}, 784 | {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d55aeb01bb7bd7c7e1bd904668a4a2ffcbb1c248e7ae9eb40a272fd7e67dd98b"}, 785 | {file = "pydantic-1.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:43d41b6f13706488e854729955ba8f740e6ec375cd16b72b81dc24b9d84f0d15"}, 786 | {file = "pydantic-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f31ffe0e38805a0e6410330f78147bb89193b136d7a5f79cae60d3e849b520a6"}, 787 | {file = "pydantic-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8eee69eda7674977b079a21e7bf825b59d8bf15145300e8034ed3eb239ac444f"}, 788 | {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f927bff6c319fc92e0a2cbeb2609b5c1cd562862f4b54ec905e353282b7c8b1"}, 789 | {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1bc3f8fef6ba36977108505e90558911e7fbccb4e930805d5dd90891b56ff4"}, 790 | {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96ab6ce1346d14c6e581a69c333bdd1b492df9cf85ad31ad77a8aa42180b7e09"}, 791 | {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:444cf220a12134da1cd42fe4f45edff622139e10177ce3d8ef2b4f41db1291b2"}, 792 | {file = "pydantic-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:dbfbff83565b4514dd8cebc8b8c81a12247e89427ff997ad0a9da7b2b1065c12"}, 793 | {file = "pydantic-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5327406f4bfd5aee784e7ad2a6a5fdd7171c19905bf34cb1994a1ba73a87c468"}, 794 | {file = "pydantic-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1072eae28bf034a311764c130784e8065201a90edbca10f495c906737b3bd642"}, 795 | {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce901335667a68dfbc10dd2ee6c0d676b89210d754441c2469fbc37baf7ee2ed"}, 796 | {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54d6465cd2112441305faf5143a491b40de07a203116b5755a2108e36b25308d"}, 797 | {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b5e5e7a0ec96704099e271911a1049321ba1afda92920df0769898a7e9a1298"}, 798 | {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ae43704358304da45c1c3dd7056f173c618b252f91594bcb6d6f6b4c6c284dee"}, 799 | {file = "pydantic-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:2d7da49229ffb1049779a5a6c1c50a26da164bd053cf8ee9042197dc08a98259"}, 800 | {file = "pydantic-1.10.1-py3-none-any.whl", hash = "sha256:f8b10e59c035ff3dcc9791619d6e6c5141e0fa5cbe264e19e267b8d523b210bf"}, 801 | {file = "pydantic-1.10.1.tar.gz", hash = "sha256:d41bb80347a8a2d51fbd6f1748b42aca14541315878447ba159617544712f770"}, 802 | ] 803 | pyopenssl = [ 804 | {file = "pyOpenSSL-22.0.0-py2.py3-none-any.whl", hash = "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"}, 805 | {file = "pyOpenSSL-22.0.0.tar.gz", hash = "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf"}, 806 | ] 807 | pyparsing = [ 808 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 809 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 810 | ] 811 | pysocks = [ 812 | {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, 813 | {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, 814 | {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, 815 | ] 816 | pytest = [ 817 | {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, 818 | {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, 819 | ] 820 | pytest-forked = [ 821 | {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, 822 | {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, 823 | ] 824 | pytest-rerunfailures = [ 825 | {file = "pytest-rerunfailures-10.2.tar.gz", hash = "sha256:9e1e1bad51e07642c5bbab809fc1d4ec8eebcb7de86f90f1a26e6ef9de446697"}, 826 | {file = "pytest_rerunfailures-10.2-py3-none-any.whl", hash = "sha256:d31d8e828dfd39363ad99cd390187bf506c7a433a89f15c3126c7d16ab723fe2"}, 827 | ] 828 | pytest-xdist = [ 829 | {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, 830 | {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, 831 | ] 832 | python-dotenv = [ 833 | {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, 834 | {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, 835 | ] 836 | requests = [ 837 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 838 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 839 | ] 840 | selene = [ 841 | {file = "selene-2.0.0b8-py3-none-any.whl", hash = "sha256:30d2cef7f993c3f33baac0361000ba6494347a6162474b9a74d82b7bd7b701d5"}, 842 | {file = "selene-2.0.0b8.tar.gz", hash = "sha256:575d0f121c71a733453bd9b54ec7088250d5ddf7a881bce9daf9f3ea3fd356df"}, 843 | ] 844 | selenium = [ 845 | {file = "selenium-4.2.0-py3-none-any.whl", hash = "sha256:ba5b2633f43cf6fe9d308fa4a6996e00a101ab9cb1aad6fd91ae1f3dbe57f56f"}, 846 | ] 847 | setuptools = [ 848 | {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, 849 | {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, 850 | ] 851 | six = [ 852 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 853 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 854 | ] 855 | sniffio = [ 856 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 857 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 858 | ] 859 | sortedcontainers = [ 860 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 861 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 862 | ] 863 | tomli = [ 864 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 865 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 866 | ] 867 | trio = [ 868 | {file = "trio-0.21.0-py3-none-any.whl", hash = "sha256:4dc0bf9d5cc78767fc4516325b6d80cc0968705a31d0eec2ecd7cdda466265b0"}, 869 | {file = "trio-0.21.0.tar.gz", hash = "sha256:523f39b7b69eef73501cebfe1aafd400a9aad5b03543a0eded52952488ff1c13"}, 870 | ] 871 | trio-websocket = [ 872 | {file = "trio-websocket-0.9.2.tar.gz", hash = "sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"}, 873 | {file = "trio_websocket-0.9.2-py3-none-any.whl", hash = "sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc"}, 874 | ] 875 | typing-extensions = [ 876 | {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, 877 | {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, 878 | ] 879 | urllib3 = [ 880 | {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, 881 | {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, 882 | ] 883 | urllib3-secure-extra = [ 884 | {file = "urllib3-secure-extra-0.1.0.tar.gz", hash = "sha256:ee9409cbfeb4b8609047be4c32fb4317870c602767e53fd8a41005ebe6a41dff"}, 885 | {file = "urllib3_secure_extra-0.1.0-py2.py3-none-any.whl", hash = "sha256:f7adcb108b4d12a4b26b99eb60e265d087f435052a76aefa396b6ee85e9a6ef9"}, 886 | ] 887 | webdriver-manager = [ 888 | {file = "webdriver_manager-3.7.0-py2.py3-none-any.whl", hash = "sha256:ee09f7c5d9c61ca9cf2b78a036355f617f5dede2d68b7d2d77877d1d48df1361"}, 889 | {file = "webdriver_manager-3.7.0.tar.gz", hash = "sha256:4a7247086b181d3a077a7f0be71b2f8e297d213ddd563ecea263dcb17e4e865f"}, 890 | ] 891 | wsproto = [ 892 | {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, 893 | {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, 894 | ] 895 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "web-test" 3 | version = "0.1.0" 4 | description = "python + pytest + selene + allure web tests project template" 5 | authors = ["Iakiv Kramarenko "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | selene = {version = "^2.0.0-beta.2", allow-prereleases = true} 10 | python-dotenv = "^0.15.0" 11 | pydantic = "^1.8.1" 12 | pytest = "^7.1.3" 13 | pytest-xdist = "^2.5.0" 14 | pytest-rerunfailures = "^10.2" 15 | allure-pytest = "^2.8.11" 16 | 17 | [tool.poetry.dev-dependencies] 18 | black = "^22.3.0" 19 | 20 | [build-system] 21 | requires = ["poetry>=0.12"] 22 | build-backend = "poetry.masonry.api" 23 | 24 | [tool.black] 25 | line-length = 79 26 | target-version = ['py37'] 27 | skip-string-normalization = true 28 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | smoke: suite of smoke tests 4 | fast: just a very fast test :D 5 | in_progress: indicate that test implementation is not finished yet -------------------------------------------------------------------------------- /run/clear_reports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf reports -------------------------------------------------------------------------------- /run/lint_black.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | black . --check 3 | -------------------------------------------------------------------------------- /run/print_docker_images_for_selenoid_browsers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | awk -F'"' '$0 ~ /selenoid/ {print $4}' etc/selenoid/browsers.json 4 | # | | 5 | # use " as column separator | 6 | # each line in a file---------this file (browsers.json) 7 | # if matches selenoid in text 8 | # print 4th column 9 | # 10 | # NOTES 11 | # - read more on awk at https://dev.to/rrampage/awk---a-useful-little-language-2fhf -------------------------------------------------------------------------------- /run/pull_docker_images_for_selenoid_browsers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | awk -F'"' '$0 ~ /selenoid/ {print $4}' etc/selenoid/browsers.json | while read -r image ; do docker pull "$image" ; done 4 | # | | 5 | # use " as column separator | 6 | # each line in a file---------this file (browsers.json) 7 | # if matches selenoid in text 8 | # print 4th column 9 | # pipe everything to next command (docker pull) 10 | # where everything are images parsed in previous step -------------------------------------------------------------------------------- /run/remove_docker_images_for_selenoid_browsers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | awk -F'"' '$0 ~ /selenoid/ {print $4}' etc/selenoid/browsers.json | while read -r image ; do docker rmi "$image" ; done 4 | # | | 5 | # use " as column separator | 6 | # each line in a file---------this file (browsers.json) 7 | # if matches selenoid in text 8 | # print 4th column 9 | # pipe everything to next command (docker pull) 10 | # where everything are images parsed in previous step -------------------------------------------------------------------------------- /run/serve_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | allure serve reports -------------------------------------------------------------------------------- /run/serve_report_in_background.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | allure serve reports & -------------------------------------------------------------------------------- /run/serve_report_stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkill -f allure -------------------------------------------------------------------------------- /run/start_up_selenoid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f etc/selenoid/compose.yaml up -d selenoid -------------------------------------------------------------------------------- /run/start_up_selenoid_with_ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f etc/selenoid/compose.yaml up -d -------------------------------------------------------------------------------- /run/stop_and_remove_selenoids.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f etc/selenoid/compose.yaml down -------------------------------------------------------------------------------- /run/stop_selenoid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f etc/selenoid/compose.yaml stop -------------------------------------------------------------------------------- /run/tests_and_reserve_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkill -f allure 4 | pytest tests -n auto --alluredir=reports "${@:1}" 5 | allure serve reports -------------------------------------------------------------------------------- /run/tests_and_reserve_report_in_background.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkill -f allure 4 | pytest tests -n auto --alluredir=reports "${@:1}" 5 | allure serve reports & -------------------------------------------------------------------------------- /run/tests_in_firefox_headless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | env -S 'browser_name=firefox headless=True' pytest tests -n auto --alluredir=reports "${@:1}" -------------------------------------------------------------------------------- /run/tests_marked_by.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest tests -n auto --alluredir=reports -m "$1" "${@:2}" -------------------------------------------------------------------------------- /run/tests_marked_by_not_smoke.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest tests -n auto --alluredir=reports -m 'not smoke' "${@:1}" -------------------------------------------------------------------------------- /run/tests_marked_by_smoke.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest tests -n auto --alluredir=reports -m smoke "${@:1}" -------------------------------------------------------------------------------- /run/tests_parallelized_auto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest tests -n auto --alluredir=reports "${@:1}" -------------------------------------------------------------------------------- /run/tests_parallelized_in.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pytest tests -n $1 --alluredir=reports "${@:2}" -------------------------------------------------------------------------------- /run/tests_remote_on.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | env -S "remote_url=$1" pytest tests -n auto --alluredir=reports "${@:2}" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import allure_commons 2 | import pytest 3 | import allure 4 | 5 | import web_test 6 | from selene.support.shared import browser 7 | from selene import support 8 | from web_test import assist 9 | 10 | 11 | @pytest.fixture(scope='session', autouse=True) 12 | def add_reporting_to_selene_steps(): 13 | 14 | from web_test.assist.python import monkey 15 | from selene.support.shared import SharedConfig, SharedBrowser 16 | 17 | original_open = SharedBrowser.open 18 | 19 | @monkey.patch_method_in(SharedBrowser) 20 | def open(self, relative_or_absolute_url: str): 21 | from web_test.assist.allure import report 22 | 23 | return report.step(original_open)(self, relative_or_absolute_url) 24 | 25 | 26 | import config 27 | 28 | 29 | @pytest.fixture(scope='function', autouse=True) 30 | def browser_management(): 31 | """ 32 | Here, before yield, 33 | goes all "setup" code for each test case 34 | aka "before test function" hook 35 | """ 36 | 37 | import config 38 | 39 | browser.config.base_url = config.settings.base_url 40 | browser.config.timeout = config.settings.timeout 41 | browser.config.save_page_source_on_failure = ( 42 | config.settings.save_page_source_on_failure 43 | ) 44 | browser.config._wait_decorator = support._logging.wait_with( 45 | context=allure_commons._allure.StepContext 46 | ) 47 | 48 | browser.config.driver = _driver_from(config.settings) 49 | browser.config.hold_browser_open = config.settings.hold_browser_open 50 | """ 51 | TODO: do we even need it? below we quit it manually.... 52 | """ 53 | 54 | yield 55 | """ 56 | Here, after yield, 57 | goes all "tear down" code for each test case 58 | aka "after test function" hook 59 | """ 60 | 61 | if not config.settings.hold_browser_open: 62 | browser.quit() 63 | 64 | 65 | from web_test.assist.selenium.typing import WebDriver 66 | 67 | 68 | def _driver_from(settings: config.Settings) -> WebDriver: 69 | driver_options = _driver_options_from(settings) 70 | 71 | from selenium import webdriver 72 | 73 | driver = ( 74 | web_test.assist.webdriver_manager.set_up.local( 75 | settings.browser_name, 76 | driver_options, 77 | ) 78 | if not settings.remote_url 79 | else webdriver.Remote( 80 | command_executor=settings.remote_url, 81 | options=driver_options, 82 | ) 83 | ) 84 | 85 | if settings.maximize_window: 86 | driver.maximize_window() 87 | else: 88 | driver.set_window_size( 89 | width=settings.window_width, 90 | height=settings.window_height, 91 | ) 92 | 93 | # other driver configuration todos: 94 | # file upload when remote 95 | # - http://allselenium.info/file-upload-using-python-selenium-webdriver/ 96 | # - https://sqa.stackexchange.com/questions/12851/how-can-i-work-with-file-uploads-during-a-webdriver-test 97 | 98 | return driver 99 | 100 | 101 | from web_test.assist.selenium.typing import WebDriverOptions 102 | 103 | 104 | def _driver_options_from(settings: config.Settings) -> WebDriverOptions: 105 | options = None 106 | 107 | from selenium import webdriver 108 | from web_test.assist.webdriver_manager import supported 109 | 110 | if settings.browser_name in [supported.chrome, supported.chromium]: 111 | options = webdriver.ChromeOptions() 112 | options.headless = settings.headless 113 | 114 | if settings.browser_name == supported.firefox: 115 | options = webdriver.FirefoxOptions() 116 | options.headless = settings.headless 117 | 118 | if settings.browser_name == supported.ie: 119 | options = webdriver.IeOptions() 120 | 121 | from web_test.assist.selenium.typing import EdgeOptions 122 | 123 | if settings.browser_name == supported.edge: 124 | options = EdgeOptions() 125 | 126 | from web_test.assist.selenium.typing import OperaOptions 127 | 128 | if settings.browser_name == supported.edge: 129 | options = OperaOptions() 130 | 131 | if settings.remote_url: 132 | options.set_capability( 133 | 'screenResolution', settings.remote_screenResolution 134 | ) 135 | options.set_capability('enableVNC', settings.remote_enableVNC) 136 | options.set_capability('enableVideo', settings.remote_enableVideo) 137 | options.set_capability('enableLog', settings.remote_enableLog) 138 | if settings.remote_version: 139 | options.set_capability('version', settings.remote_version) 140 | if settings.remote_platform: 141 | options.set_capability('platform', settings.remote_platform) 142 | 143 | return options 144 | 145 | 146 | prev_test_screenshot = None 147 | prev_test_page_source = None 148 | 149 | 150 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 151 | def pytest_runtest_setup(item): 152 | yield 153 | 154 | global prev_test_screenshot 155 | prev_test_screenshot = browser.config.last_screenshot 156 | global prev_test_page_source 157 | prev_test_page_source = browser.config.last_page_source 158 | 159 | 160 | from _pytest.nodes import Item 161 | from _pytest.runner import CallInfo 162 | 163 | 164 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 165 | def pytest_runtest_makereport(item: Item, call: CallInfo): 166 | """ 167 | Attach snapshots on test failure 168 | """ 169 | 170 | # All code prior to yield statement would be ran prior 171 | # to any other of the same fixtures defined 172 | 173 | outcome = ( 174 | yield # Run all other pytest_runtest_makereport non wrapped hooks 175 | ) 176 | 177 | result = outcome.get_result() 178 | 179 | if result.when == 'call' and result.failed: 180 | last_screenshot = browser.config.last_screenshot 181 | if last_screenshot and not last_screenshot == prev_test_screenshot: 182 | allure.attach.file( 183 | source=last_screenshot, 184 | name='screenshot', 185 | attachment_type=allure.attachment_type.PNG, 186 | ) 187 | 188 | last_page_source = browser.config.last_page_source 189 | if last_page_source and not last_page_source == prev_test_page_source: 190 | allure.attach.file( 191 | source=last_page_source, 192 | name='page source', 193 | attachment_type=allure.attachment_type.HTML, 194 | ) 195 | -------------------------------------------------------------------------------- /tests/test_search_engines_should_search.py: -------------------------------------------------------------------------------- 1 | # This file contains examples of different "modeling styles" when implementing tests. 2 | # The algorithm of choosing your way to write tests might be the following: 3 | # 1. Start reviewing examples from top to bottom 4 | # 2. Stop where you understand what happens yet AND you like it 5 | # But ensure you quickly check at least a few more examples;) Just in case;) 6 | # Ensure you check the Allure Report for corresponding example. 7 | # 3. Remove all not needed examples 8 | # and corresponding python modules that are used in them 9 | # 4. Stick to the style chosen;) 10 | # 11 | # Of course you can mix styles, if you understand what you do;) 12 | # 13 | # Ensure you check the Allure Report of corresponding test examples. 14 | # * The test style will influence the reporting too 15 | # * what is good to count when making your choice 16 | 17 | import pytest 18 | 19 | 20 | def test_bing(): 21 | """ 22 | Pending test example (Option 1) 23 | =============================== 24 | 25 | opened bing.com 26 | search 'yashaka selene python' 27 | results should be of count more than 5, first text '... Web UI ...' 28 | follows first link 29 | should be on github repo 'yashaka/selene' 30 | """ 31 | pytest.skip('as pending') 32 | 33 | 34 | from web_test.test_markers import mark 35 | 36 | 37 | @mark.pending # todo: find a way to automatically skip empty tests, maybe use some pytest plugin 38 | def test_yahoo(): 39 | """ 40 | Pending test example (Option 2) 41 | =============================== 42 | 43 | opened yahoo.com 44 | search 'yashaka selene python' 45 | results should be of count more than 5, first text '... Web UI ...' 46 | follows first link 47 | should be on github repo 'yashaka/selene' 48 | """ 49 | 50 | 51 | from selene.support.shared import browser 52 | from selene import have, be 53 | """ 54 | ... the only 3 things you need to import 55 | to start working with Selene in a straightforward KISS style;) 56 | """ 57 | 58 | 59 | @mark.suite.smoke 60 | def test_duckduckgo(): 61 | """ 62 | Straightforward/PageObjectLess style 63 | =================================== 64 | 65 | GO FOR: 66 | * KISS (Keep It Simple Stupid), straightforward style 67 | * easy for newbies in automation (no need to learn modules/OOP(classes)) 68 | * easy for some DEVs if they will use these tests (and they should!) 69 | they know selectors and internals of app they develop 70 | hence, building more abstractions (modules/classes) on top of more 71 | low level straightforward code (like below) would add too much complexity 72 | to them, and harder in day-to-day usage 73 | 74 | TRADEOFFS: 75 | - given selectors are duplicated all over the project code base 76 | when you want to change it 77 | then you have to use global Find&Replace text, 78 | with sometimes pretty thorough manual checks line by line 79 | all places where the change will be applied. 80 | You CAN'T use some refactoring features of IDE like Refactor>Rename 81 | """ 82 | 83 | browser.open('https://duckduckgo.com/') 84 | 85 | browser.element('[name=q]')\ 86 | .should(be.blank)\ 87 | .type('yashaka selene python').press_enter() 88 | browser.all('[data-testid=result]') \ 89 | .should(have.size_greater_than(5)) \ 90 | .first.should(have.text('User-oriented Web UI browser tests')) 91 | 92 | browser.all( 93 | '[data-testid=result]' 94 | ).first.element('[data-testid=result-title-a]').click() 95 | browser.should(have.title_containing('yashaka/selene')) 96 | 97 | 98 | from selene.support.shared.jquery_style import s, ss 99 | """ 100 | some JQuery style shortcuts, if considered sexy, can help with conciseness;) 101 | """ 102 | 103 | 104 | def test_duckduckgo_(): 105 | """ 106 | Straightforward/PageObjectLess style + s, ss for JQuery/Selenide's $, $$ 107 | ======================================================================== 108 | 109 | GO FOR: 110 | * the most concise 111 | * KISS (Keep It Simple Stupid), straightforward style 112 | * easy for newbies in automation (no need to learn modules/OOP(classes)) 113 | * easy for some DEVs if they will use these tests (and they should!) 114 | they know selectors and internals of app they develop 115 | hence, building more abstractions (modules/classes) on top of more 116 | low level straightforward code (like below) would add too much complexity 117 | to them, and harder in day-to-day usage 118 | 119 | TRADEOFFS: 120 | - given selectors are duplicated all over the project code base 121 | when you want to change it 122 | then you have to use global Find&Replace text, 123 | with sometimes pretty thorough manual checks line by line 124 | all places where the change will be applied. 125 | You CAN'T use some refactoring features of IDE like Refactor>Rename 126 | """ 127 | browser.open('https://duckduckgo.com/') 128 | 129 | s('[name=q]').type('yashaka selene python').press_enter() 130 | ss('[data-testid=result]') \ 131 | .should(have.size_greater_than(5)) \ 132 | .first.should(have.text('User-oriented Web UI browser tests')) 133 | 134 | ss('[data-testid=result]').first.s('[data-testid=result-title-a]').click() 135 | browser.should(have.title_containing('yashaka/selene')) 136 | 137 | 138 | from web_test.pages import pypi 139 | """ 140 | # where pypi is your locators storage – 141 | # as simple as simple python module 142 | # (listed here for easier demo) 143 | 144 | from selene.support.shared import browser 145 | 146 | url = 'https://pypi.org/' 147 | 148 | search = browser.element('#search') 149 | results = browser.all('.package-snippet') 150 | """ 151 | 152 | 153 | def test_pypi(): 154 | """ 155 | LocatorModules/PageObjectLess 156 | LocatorModules == page locators/selectors are simply vars in python modules 157 | Might be also called as PageModules 158 | =========================================================================== 159 | 160 | Here the page model is implemented in the simplest modular way 161 | with simplification to "just vars, no functions for steps" in python modules. 162 | 163 | GO FOR: 164 | * a bit higher abstraction (no more technical selectors in tests code) 165 | * extra readability in test code 166 | * reusable vars with locators 167 | * easier refactoring (Refactor>Rename, etc. can be applied) 168 | * yet KISS modeling (Keep It Simple Stupid) 169 | 170 | TRADEOFFS: 171 | - common ones for "programming without functions" style ;) 172 | some code might be too bulky, 173 | business steps might be hardly visible in a long e2e test 174 | """ 175 | browser.open(pypi.url) 176 | 177 | pypi.search.type('selene').press_enter() 178 | pypi.results\ 179 | .should(have.size_greater_than_or_equal(9)) \ 180 | .first.should(have.text('Concise API for selenium in Python')) 181 | 182 | pypi.results.first.click() 183 | browser.should(have.url(pypi.url + 'project/selene/')) 184 | browser.should(have.title_containing('selene · PyPI')) 185 | 186 | 187 | from web_test.assist.allure.gherkin import when, given, then 188 | """ 189 | for extra BDD-style decoration with extra comments to log in report 190 | """ 191 | 192 | 193 | def test_duckduckgo__(): 194 | """ 195 | Straightforward/PageObjectLess/BDD style + reported "steps-comments" (Option 1) 196 | ============================================================================== 197 | 198 | GO FOR: 199 | * KISS (Keep It Simple Stupid), straightforward style 200 | * extra readability and structure in both Test and its Report 201 | * to add comments reflecting some test logic from higher business perspective 202 | * to break the long End-to-End test into meaningful chunks 203 | (the code below in not actually so long, but just for example;)) 204 | 205 | TRADEOFFS: 206 | - extra "texts" to support in code 207 | - steps can't be reused # todo: there are some ideas though;) 208 | - too much of test-like-steps might made code less focused, too vague 209 | especially when repeating the logic or test data as it is already used in code 210 | - Manual old-fashioned Find&Replace instead of Refactor>Rename 211 | 212 | NOTES 213 | * you can use 3 AAA naming over BDD: 214 | @arrange over @given 215 | @act over @then 216 | @assert_ over @then 217 | 218 | * you can call step fn whatever you like, examples: 219 | 220 | @when('search') 221 | def step(text='selene python'): 222 | browser.element('[name=q]').type(text).press_enter() 223 | 224 | @when() 225 | def search(text='selene python'): 226 | browser.element('[name=q]').type(text).press_enter() 227 | 228 | @when('search') 229 | def params(text='selene python'): 230 | browser.element('[name=q]').type(text).press_enter() 231 | 232 | @when('search') 233 | def args(text='selene python'): 234 | browser.element('[name=q]').type(text).press_enter() 235 | 236 | @when('search') 237 | def ine(text='selene python'): 238 | browser.element('[name=q]').type(text).press_enter() 239 | 240 | @when('search') 241 | def _(text='selene python'): 242 | browser.element('[name=q]').type(text).press_enter() 243 | 244 | ;) 245 | 246 | * you can skip params at all, 247 | if you don't need them to be logged in report 248 | as high level step's test data 249 | 250 | @when('search') 251 | def ine(): 252 | browser.element('[name=q]').type('selene python').press_enter() 253 | 254 | @when() 255 | def search(): 256 | browser.element('[name=q]').type('selene python').press_enter() 257 | 258 | * take into account that this is mostly a bad practice – 259 | to DUPLICATE test data in "step comments" – 260 | you bloat code with same info and increase support time on each change 261 | 262 | @when('search "selene python"') 263 | def ine(): 264 | browser.element('[name=q]').type('selene python').press_enter() 265 | """ 266 | 267 | @given('opened duckduckgo') 268 | def step(): 269 | browser.open('https://duckduckgo.com/') 270 | 271 | @when('search') 272 | def step(text='yashaka selene python'): 273 | s('[name=q]').type(text).press_enter() 274 | 275 | @then('results should be') 276 | def step(more_than=5, 277 | first_result_text='User-oriented Web UI browser tests'): 278 | ss('[data-testid=result]') \ 279 | .should(have.size_greater_than(more_than)) \ 280 | .first.should(have.text(first_result_text)) 281 | 282 | @when('follows first link') 283 | def step(): 284 | ss('[data-testid=result]').first.element('[data-testid=result-title-a]').click() 285 | 286 | @then('should be on github') 287 | def step(repo='yashaka/selene'): 288 | browser.should(have.title_containing(repo)) 289 | 290 | 291 | def test_duckduckgo___(): 292 | """ 293 | Straightforward/PageObjectLess/BDD style + reported "steps-comments" (Option 2) 294 | ============================================================================== 295 | 296 | GO FOR: 297 | * KISS (Keep It Simple Stupid), straightforward style 298 | * extra readability and structure in both Test and its Report 299 | ... see more in previous example 300 | 301 | TRADEOFFS: 302 | - extra "texts" to support in code 303 | - steps can't be reused 304 | - Manual old-fashioned Find&Replace instead of Refactor>Rename 305 | """ 306 | 307 | @given() 308 | def opened_duckduckgo(): 309 | browser.open('https://duckduckgo.com/') 310 | 311 | @when() 312 | def search(text='yashaka selene python'): 313 | browser.element('[name=q]').type(text).press_enter() 314 | 315 | @then() 316 | def results_should_be( 317 | more_than=5, 318 | first_result_text='User-oriented Web UI browser tests' 319 | ): 320 | browser.all('[data-testid=result]')\ 321 | .should(have.size_greater_than(more_than))\ 322 | .first.should(have.text(first_result_text)) 323 | 324 | @when() 325 | def follows_first_link(): 326 | browser.all('[data-testid=result]').first.element('[data-testid=result-title-a]').click() 327 | 328 | @then() 329 | def should_be_on_github(repo='yashaka/selene'): 330 | browser.should(have.title_containing(repo)) 331 | 332 | 333 | from web_test.pages import searchencrypt, python_org 334 | """ 335 | abstracting things out into more high level step-functions in simple python modules 336 | """ 337 | 338 | 339 | @mark.suite.smoke 340 | @mark.flaky(reruns=1) 341 | def test_searchencrypt(): 342 | """ 343 | PageModules/PageObjectLess 344 | PageModules == page steps are simply functions in python modules 345 | ================================================================ 346 | 347 | Here the page model is implemented in the simplest modular way. 348 | The `searchencrypt` is page module (python module with functions) 349 | not page object (a python object of class with methods) 350 | 351 | GO FOR: 352 | * higher abstraction (no lower tech details in tests code) 353 | * extra readability in both Test and its Report 354 | * reusable steps 355 | * easier refactoring (Refactor>Rename, etc. can be applied) 356 | * yet KISS modeling (Keep It Simple Stupid) 357 | 358 | TRADEOFFS: 359 | - extra bloated functions sometimes repeating already readable raw/straightforward selene code 360 | - common tradeoffs for Modular/Procedural paradigm in Python 361 | - potentially less readable in complex modeling scenarios 362 | - functions can become complicated when have many parameters 363 | - procedural composition looks less sexy as object oriented in python 364 | - TODO: provide example 365 | - no fluent style like page.results.first.follow_link() 366 | """ 367 | searchencrypt.visit() 368 | 369 | searchencrypt.search('python') 370 | searchencrypt.should_have_results_amount_at_least(5) 371 | 372 | searchencrypt.follow_result_link('Welcome to Python.org') 373 | python_org.should_be_opened() 374 | 375 | 376 | # === Notes === 377 | # Take into account that here we don't differentiate between 378 | # * Page(Object/etc.) 379 | # and 380 | # * Steps(Object/etc.) 381 | # Usually such separation is relevant only in extremely complicated applications 382 | # which are pretty rare in real life. In the majority of situations, to simplify 383 | # the application model implementation, it's easier to decompose "*Objects" 384 | # into smaller ones at the same layer of abstraction, not adding one more layer. 385 | # Since tests nevertheless should test "user behavioural steps", 386 | # it's natural to start refactoring the straightforward code to user-steps-functions 387 | # This allows us to forget about advantages of "assertion-free PageObjects" approach. 388 | # Since it's natural to consider "assertion methods" also "user steps" and 389 | # put them at same place ;) 390 | # Many things becomes easier then;) 391 | # ================================= 392 | 393 | 394 | from web_test import app 395 | """ 396 | collecting everything into one place for easier and faster access 397 | """ 398 | 399 | 400 | @mark.tag.in_progress 401 | @mark.flaky 402 | def test_searchencrypt_(): 403 | """ 404 | PageModules/PageObjectLess + ApplicationManager 405 | =============================================== 406 | 407 | GO FOR: 408 | * one entry point to all app model, you import it once as app and 409 | gain access to all available pages without trying to remember them 410 | * ... see more at test_searchencrypt example 411 | 412 | TRADEOFFS: 413 | - supporting all page imports in a separate file 414 | - ... same as for test_searchencrypt example 415 | """ 416 | app.searchencrypt.visit() 417 | 418 | app.searchencrypt.search('python') 419 | app.searchencrypt.should_have_results_amount_at_least(5) 420 | 421 | app.searchencrypt.follow_result_link('Welcome to Python.org') 422 | app.python_org.should_be_opened() 423 | 424 | 425 | from web_test.pages.google import Google 426 | google = Google() 427 | 428 | 429 | def test_google(): 430 | """ 431 | Here we use simplified implementation 432 | with one page object (google) 433 | It's implemented in "fluent style", 434 | where its methods return self, 435 | where possible. 436 | 437 | Notice, that technically we could write everything 438 | in "one chain" 439 | 440 | web.google\ 441 | .open()\ 442 | .search('selene python')\ 443 | .should_have_results_amount_at_least(5)\ 444 | .search('selene python')\ 445 | .should_have_results_amount_at_least(5) 446 | .follow_result_link('User-oriented Web UI browser tests') 447 | 448 | web.github.should_be_on('yashaka/selene') 449 | 450 | But such a code lacks "structure". 451 | It's hardly visible from one sight 452 | how much steps does this scenario have 453 | 454 | Take this into account. 455 | Fluent style is handy, but not overuse it;) 456 | 457 | """ 458 | google.open() 459 | 460 | google \ 461 | .search('yashaka selene python') \ 462 | .should_have_results_amount_at_least(12) # demo-failure ;) 463 | # .should_have_results_amount_at_least(5) 464 | 465 | google.follow_result_link('User-oriented Web UI browser tests') 466 | github.should_be_on('yashaka/selene') 467 | 468 | 469 | from web_test.pages.ecosia import ecosia 470 | from web_test.pages.github import github 471 | 472 | 473 | def test_ecosia(): 474 | ecosia.open() 475 | 476 | ecosia.search(text='yashaka selene python') 477 | ecosia.results \ 478 | .should_have_size_at_least(5) \ 479 | .should_have_text(0, 'User-oriented Web UI browser tests') 480 | 481 | ecosia.results.follow_link(0) 482 | github.should_be_on('yashaka/selene') 483 | 484 | 485 | def test_ecosia_(): 486 | app.ecosia.open() 487 | 488 | app.ecosia.search(text='yashaka selene python') 489 | app.ecosia.results \ 490 | .should_have_size_at_least(5) \ 491 | .should_have_text(0, 'User-oriented Web UI browser tests') 492 | 493 | app.ecosia.results.follow_link(0) 494 | app.github.should_be_on('yashaka/selene') 495 | -------------------------------------------------------------------------------- /tests/test_self.py: -------------------------------------------------------------------------------- 1 | import config 2 | from web_test import __version__ 3 | from web_test.test_markers import mark 4 | 5 | 6 | pytestmark = mark.tag.fast 7 | """ 8 | marking all tests below as 'fast' 9 | """ 10 | 11 | 12 | def test_author(): 13 | assert config.settings.author == 'yashaka' 14 | 15 | 16 | def test_version(): 17 | assert __version__ == '0.1.0' 18 | -------------------------------------------------------------------------------- /web_test/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | 3 | from .test_markers import mark 4 | -------------------------------------------------------------------------------- /web_test/alternative/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | a package with some alternative implementations of already used in framework features 3 | 4 | ignore it for now;) 5 | """ 6 | -------------------------------------------------------------------------------- /web_test/alternative/pytest/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2015-2021 Iakiv Kramarenko 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /web_test/alternative/pytest/project/__init__.py: -------------------------------------------------------------------------------- 1 | from . import settings 2 | -------------------------------------------------------------------------------- /web_test/alternative/pytest/project/settings.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2015-2021 Iakiv Kramarenko 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | from __future__ import annotations 23 | 24 | from typing import List 25 | 26 | 27 | class Option: 28 | """ 29 | Usage 30 | ===== 31 | # somewhere in conftest.py 32 | class Config: 33 | 34 | def __init__(self, request): 35 | self.request = request 36 | 37 | # just an example 38 | # @Option.default('http://todomvc4tasj.herokuapp.com/') 39 | # def base_url(self): 40 | # pass 41 | 42 | @Option.default(6.0) 43 | def timeout(self): 44 | pass 45 | 46 | @Option.default(True) 47 | def save_page_source_on_failure(self): 48 | pass 49 | 50 | @Option.default("yashaka") 51 | def author(self): 52 | pass 53 | 54 | 55 | def pytest_addoption(parser): 56 | Option.register_all(from_cls=project.Config, in_parser=parser) 57 | 58 | @pytest.fixture 59 | def config(request): 60 | return project.Config(request) 61 | 62 | @pytest.fixture(scope='function', autouse=True) 63 | def browser_management(config): 64 | browser.config.timeout = config.timeout 65 | # ... 66 | 67 | """ 68 | 69 | @staticmethod 70 | def s_from(cls) -> List[Option]: 71 | return [Option.from_(field) for field in cls.__dict__.values() 72 | if Option.in_(field)] 73 | 74 | @staticmethod 75 | def from_(prop) -> Option: 76 | return prop.fget.option 77 | 78 | @staticmethod 79 | def in_(field) -> bool: 80 | return hasattr(field, 'fget') and hasattr(field.fget, 'option') 81 | 82 | @staticmethod 83 | def register_all(from_cls, in_parser): # todo: consider moving out from Option 84 | for option in Option.s_from(from_cls): 85 | option.register(in_parser) 86 | 87 | @staticmethod 88 | def default(value, **attributes): 89 | def decorator(fun_on_self_with_request): 90 | option = Option( 91 | f'--{fun_on_self_with_request.__name__}', 92 | action='store', 93 | default=value, 94 | type=type(value), 95 | **attributes) 96 | 97 | def fun(self): 98 | return option.value(self.request) 99 | 100 | fun.option = option 101 | 102 | return property(fun) 103 | 104 | return decorator 105 | 106 | def __init__(self, name, **attributes): 107 | self.name = name 108 | self.attributes = attributes 109 | 110 | def value(self, from_request): 111 | return from_request.settings.getoption(self.name) 112 | 113 | def register(self, parser): 114 | parser.addoption(self.name, **self.attributes) 115 | -------------------------------------------------------------------------------- /web_test/alternative/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from . import sourced, source 2 | """ 3 | An alternative implementation of Project Configuration management. 4 | Not used in this project yet. 5 | - But you can consider it for more control and 6 | less dependencies from external libs like Pydantic, etc. 7 | """ 8 | -------------------------------------------------------------------------------- /web_test/alternative/settings/source.py: -------------------------------------------------------------------------------- 1 | import os 2 | from_env = os.getenv 3 | 4 | 5 | def from_json(file: str): 6 | def source(key, default=None): 7 | try: 8 | import json 9 | parsed = json.load(open(file)) 10 | except Exception: 11 | return default 12 | else: 13 | return parsed.get(key) 14 | 15 | return source 16 | -------------------------------------------------------------------------------- /web_test/alternative/settings/sourced.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional 2 | 3 | 4 | class Settings: # todo: Consider even simpler impl based on dataclasses 5 | """ 6 | USAGE 7 | ===== 8 | 9 | from web_test.assist.settings import sourced 10 | 11 | class Settings(sourced.Settings): 12 | 13 | @sourced.default(6.0) 14 | def timeout(self): pass 15 | 16 | @sourced.default(True) 17 | def save_page_source_on_failure(self): pass 18 | 19 | @sourced.default("yashaka") 20 | def author(self): pass 21 | 22 | 23 | import os 24 | config = Settings( 25 | lambda key, _: json.load(open(file)).get(key), 26 | os.getenv, 27 | # the last one takes precedence 28 | # you also can find some predefined sources at ./source.py 29 | ) 30 | 31 | ===== 32 | NOTES 33 | ===== 34 | inspired by http://owner.aeonbits.org/docs/usage/ from Java world 35 | probably in Python there should be nicer ways to achieve same, 36 | like in pydantic, environ-config, etc. 37 | let's think corresponding improvement;) 38 | """ 39 | 40 | def __init__( 41 | self, 42 | source: Callable[[str, Optional[str]], Optional[str]] = lambda _: None, 43 | *more: Callable[[str, Optional[str]], Optional[str]] 44 | ): 45 | sources = [source, *more] 46 | from functools import reduce 47 | self._source = reduce( 48 | (lambda f, g: lambda key, default: 49 | f(key, g(key, default)) if g else f(key, None)), 50 | sources[::-1], 51 | lambda _, default: default 52 | ) 53 | 54 | @property 55 | def source(self): 56 | return self._source 57 | 58 | 59 | def default(value): 60 | def decorator(method): 61 | 62 | import functools 63 | 64 | @functools.wraps(method) 65 | def fun(self: Settings): 66 | maybe_sourced = self.source(method.__name__, None) 67 | 68 | sourced_or_value = \ 69 | maybe_sourced if maybe_sourced is not None \ 70 | else value 71 | 72 | original_type = type(value) 73 | 74 | return original_type(sourced_or_value) 75 | 76 | return property(fun) 77 | 78 | return decorator 79 | -------------------------------------------------------------------------------- /web_test/app.py: -------------------------------------------------------------------------------- 1 | from web_test.pages.ecosia import Ecosia 2 | from web_test.pages.github import Github 3 | from web_test.pages.google import Google 4 | 5 | """ 6 | This module is optional. 7 | It is needed to implement an ApplicationManager pattern 8 | Usually it makes sense to call it `app.py`, 9 | but in the context of this template project, our app is "all web"... 10 | Hence we could call it simply `web.py`. 11 | If `pages` name is also good to our taste, 12 | we could move all this content simply into web_test.pages.__init__.py 13 | Find your style on your own;) 14 | 15 | So, the idea is to provide a one entry point to all your model entities, 16 | that you might implement following some kind of PageObject pattern variations 17 | (PageModule/StepsModule/PageObject/StepsObject/etc) 18 | 19 | So you can import just this entry point in your test: 20 | 21 | from web_test.pages import web 22 | 23 | and then fluently access any page: 24 | 25 | web.ecosia 26 | # ... 27 | web.searchencrypt 28 | # ... 29 | web.duckduckgo 30 | 31 | instead of direct import: 32 | 33 | from web_test.pages.ecosia import ecosia 34 | from web_test.pages.searchencrypt import searchencrypt 35 | from web_test.pages.duckduckgo import duckduckgo 36 | 37 | ecosia 38 | # ... 39 | searchencrypt 40 | # ... 41 | duckduckgo 42 | 43 | Probably instead of: 44 | 45 | web_test/app.py 46 | 47 | you can use any of: 48 | 49 | web_test/pages/app.py 50 | web_test/pages/__init__.py 51 | 52 | we type hint variables below to allow better IDE support, 53 | e.g. for Quick Fix feature... 54 | """ 55 | ecosia: Ecosia = Ecosia() 56 | google: Google = Google() 57 | github: Github = Github() 58 | """ 59 | we need type hints above 60 | - to make Autocomplete and Quick Fix features work 61 | - at least in some versions of PyCharm 62 | """ 63 | 64 | from web_test.pages import searchencrypt 65 | from web_test.pages import python_org 66 | """ 67 | searchencrypt is kind of "PageModule" not "PageObject" 68 | that's why we don't have to introduce a new variable for page's object 69 | just an import is enough 70 | 71 | There is one nuance though... 72 | If we want the IDE in case of "quick fixing imports" to 73 | show for us ability to directly import searchencrypt from app.py 74 | then we might have to do something like this in some versions of your IDEs: 75 | 76 | from web_test.pages import searchencrypt as _searchencrypt 77 | searchencrypt = _searchencrypt 78 | 79 | But probably you will never need it;) 80 | Hence keep things simple;) 81 | """ 82 | -------------------------------------------------------------------------------- /web_test/assist/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | allure, 3 | python, 4 | selene, 5 | webdriver_manager, 6 | project, 7 | ) 8 | -------------------------------------------------------------------------------- /web_test/assist/allure/__init__.py: -------------------------------------------------------------------------------- 1 | from . import report, aaa, gherkin 2 | -------------------------------------------------------------------------------- /web_test/assist/allure/aaa.py: -------------------------------------------------------------------------------- 1 | from web_test.assist.allure import gherkin 2 | 3 | 4 | def arrange(description: str): 5 | return gherkin.given(description) 6 | 7 | 8 | def act(description: str): 9 | return gherkin.when(description) 10 | 11 | 12 | def assert_(description: str): 13 | return gherkin.then(description) 14 | -------------------------------------------------------------------------------- /web_test/assist/allure/gherkin.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from web_test.assist.allure import report 4 | 5 | 6 | def _step(description: Optional[str] = None): 7 | def decorated(fn): 8 | if description: 9 | fn.__name__ = description.replace(' ', '_') # todo: improve (leave it with spaces?) 10 | return report.step(fn, display_context=False)() 11 | return decorated 12 | 13 | 14 | def given(precondition: Optional[str] = None): 15 | return _step(precondition) 16 | 17 | 18 | def when(act: Optional[str] = None): 19 | return _step(act) 20 | 21 | 22 | def then(assertion: Optional[str] = None): 23 | return _step(assertion) 24 | -------------------------------------------------------------------------------- /web_test/assist/allure/report.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import re 3 | import inspect 4 | from functools import wraps, reduce 5 | 6 | from allure_commons import plugin_manager 7 | from allure_commons.utils import uuid4, represent 8 | 9 | 10 | def _humanify(string_with_underscores, /): 11 | return re.sub(r'_+', ' ', string_with_underscores).strip() # todo: improve ;) 12 | 13 | 14 | def _fn_params_to_ordered_dict(func, *args, **kwargs): 15 | spec = inspect.getfullargspec(func) 16 | 17 | # given pos_or_named = list of pos_only args and pos_or_named/standard args 18 | pos_or_named_ordered_names = list(spec.args) 19 | pos_without_defaults_dict = dict(zip(spec.args, args)) 20 | if spec.args and spec.args[0] in ['cls', 'self']: 21 | pos_without_defaults_dict.pop(spec.args[0], None) 22 | 23 | received_args_amount = len(args) 24 | pos_or_named_not_set = spec.args[received_args_amount:] 25 | pos_defaults_dict = \ 26 | dict(zip(pos_or_named_not_set, spec.defaults or [])) 27 | 28 | varargs = args[len(spec.args):] 29 | varargs_dict = \ 30 | {spec.varargs: varargs} if (spec.varargs and varargs) else \ 31 | {} 32 | pos_or_named_or_vargs_ordered_names = \ 33 | pos_or_named_ordered_names + [spec.varargs] if varargs_dict else \ 34 | pos_or_named_ordered_names 35 | 36 | pos_or_named_or_vargs_or_named_only_ordered_names = ( 37 | pos_or_named_or_vargs_ordered_names 38 | + list(spec.kwonlyargs) 39 | ) 40 | 41 | items = { 42 | **pos_without_defaults_dict, 43 | **pos_defaults_dict, 44 | **varargs_dict, 45 | **(spec.kwonlydefaults or {}), 46 | **kwargs, 47 | }.items() 48 | 49 | sorted_items = sorted( 50 | map(lambda kv: (kv[0], represent(kv[1])), items), 51 | key= 52 | lambda x: pos_or_named_or_vargs_or_named_only_ordered_names.index(x[0]) 53 | ) 54 | 55 | return collections.OrderedDict(sorted_items) 56 | 57 | 58 | def step( 59 | title_or_callable=None, 60 | display_params=True, 61 | params_separator=', ', 62 | derepresent_params=False, 63 | display_context=True, 64 | translations=(), 65 | ): # todo: add prefixes like gherkin, controlled by setting;) 66 | if callable(title_or_callable): 67 | func = title_or_callable 68 | name: str = title_or_callable.__name__ 69 | display_name = _humanify(name) # todo: move to StepContext 70 | return StepContext( 71 | display_name, 72 | {}, 73 | display_params=display_params, 74 | params_separator=params_separator, 75 | derepresent_params=derepresent_params, 76 | display_context=display_context, 77 | translations=translations, 78 | )(func) 79 | else: 80 | return StepContext( 81 | title_or_callable, 82 | {}, 83 | display_params=display_params, 84 | params_separator=params_separator, 85 | derepresent_params=derepresent_params, 86 | display_context=display_context, 87 | translations=translations, 88 | ) 89 | 90 | 91 | class StepContext: 92 | 93 | def __init__( 94 | self, 95 | title, 96 | params, 97 | display_params=True, 98 | params_separator=', ', 99 | derepresent_params=False, 100 | display_context=True, 101 | translations=( 102 | (':--(', ':--)'), 103 | (':--/', ':--D'), 104 | ), 105 | ): 106 | self.maybe_title = title 107 | self.params = params 108 | self.uuid = uuid4() 109 | self.display_params = display_params 110 | self.params_separator = params_separator 111 | self.derepresent_params = derepresent_params 112 | self.display_context = display_context 113 | self.translations = translations 114 | 115 | def __enter__(self): 116 | plugin_manager.hook.start_step( 117 | uuid=self.uuid, 118 | title=self.maybe_title or '', 119 | params=self.params) 120 | 121 | def __exit__(self, exc_type, exc_val, exc_tb): 122 | plugin_manager.hook.stop_step( 123 | uuid=self.uuid, 124 | title=self.maybe_title or '', 125 | exc_type=exc_type, 126 | exc_val=exc_val, 127 | exc_tb=exc_tb) 128 | 129 | def __call__(self, func): 130 | @wraps(func) 131 | def impl(*args, **kw): 132 | __tracebackhide__ = True 133 | 134 | # params_dict = func_parameters(func, *args, **kw) 135 | params_dict = _fn_params_to_ordered_dict(func, *args, **kw) 136 | 137 | def described(item): 138 | (name, value) = item 139 | spec = inspect.getfullargspec(func) 140 | is_pos_or_named_passed_as_arg = \ 141 | name in dict(zip(spec.args, args)).keys() 142 | # has_defaults = spec.defaults or spec.kwonlydefaults 143 | # is_pos_or_named_passed_as_kwarg = \ 144 | # name in etc.list_intersection(spec.args, list(kw.keys())) 145 | return str(value) if is_pos_or_named_passed_as_arg \ 146 | else f'{_humanify(name)} {value}' 147 | 148 | params = list(map(described, list(params_dict.items()))) 149 | 150 | def derepresent(string): 151 | return string[1:-1] 152 | params_string = self.params_separator.join( 153 | list(map(derepresent, params)) if self.derepresent_params 154 | else params 155 | ) 156 | params_values = list(params_dict.values()) 157 | 158 | def title_to_display(): 159 | return self.maybe_title or _humanify(func.__name__) 160 | 161 | def params_to_display(): 162 | if not params_values: 163 | return '' 164 | was_fn_called_with_some_args = args or kw 165 | if len(params_values) == 1 and was_fn_called_with_some_args: 166 | item = next(iter(params_dict.items())) 167 | if item[0] in kw.keys(): 168 | return f' {item[0]} {item[1]}' 169 | else: 170 | return ' ' + params_values[0] 171 | return ((': ' if title_to_display() else '') 172 | + params_string) 173 | 174 | def context(): 175 | # todo: refactor naming and make idiomatic 176 | def is_method(fn): 177 | spec = inspect.getfullargspec(fn) 178 | return (args 179 | and spec.args 180 | and spec.args[0] in ['cls', 'self']) 181 | 182 | maybe_module_name = \ 183 | func.__module__.split('.')[-1] if not is_method(func) \ 184 | else None 185 | 186 | instance = args[0] if is_method(func) else None 187 | instance_desc = str(instance) 188 | maybe_instance_name = \ 189 | instance_desc if 'at 0x' not in instance_desc \ 190 | else None 191 | class_name = instance and instance.__class__.__name__ 192 | 193 | context_name = maybe_module_name or maybe_instance_name or class_name 194 | 195 | if not context_name: 196 | return '' 197 | 198 | return f' [{context_name}]' # todo: make ` [...]` configurable;) 199 | 200 | name_to_display = ( 201 | title_to_display() 202 | + (params_to_display() if self.display_params else '') 203 | + (context() if self.display_context else '') 204 | ) 205 | 206 | 207 | 208 | translated_name = reduce( 209 | lambda text, item: text.replace(item[0], item[1]), 210 | self.translations, 211 | name_to_display 212 | ) if self.translations else name_to_display 213 | 214 | with StepContext(translated_name, params_dict): 215 | return func(*args, **kw) 216 | 217 | # todo: consider supporting the following original params rendering 218 | # with StepContext(self.title.format(*args, **params), params): 219 | # return func(*args, **kw) 220 | 221 | return impl 222 | -------------------------------------------------------------------------------- /web_test/assist/project.py: -------------------------------------------------------------------------------- 1 | 2 | def abs_path_from_project(relative_path: str): 3 | import web_test 4 | from pathlib import Path 5 | return Path( 6 | web_test.__file__ 7 | ).parent.parent.joinpath(relative_path).absolute().__str__() 8 | -------------------------------------------------------------------------------- /web_test/assist/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/web_test/assist/python/__init__.py -------------------------------------------------------------------------------- /web_test/assist/python/etc.py: -------------------------------------------------------------------------------- 1 | def list_intersection(one: list, another: list, /): 2 | return list(set(one) & set(another)) 3 | -------------------------------------------------------------------------------- /web_test/assist/python/fp.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | def pipe(*functions): 5 | """ 6 | pipes functions one by one in the provided order 7 | i.e. applies arg1, then arg2, then arg3, and so on 8 | if any arg is None, just skips it 9 | """ 10 | return functools.reduce( 11 | lambda f, g: lambda x: f(g(x)) if g else f(x), 12 | functions[::-1], 13 | lambda x: x) if functions else None -------------------------------------------------------------------------------- /web_test/assist/python/monkey.py: -------------------------------------------------------------------------------- 1 | # implemented based on examples from Guido van Rossum 2 | # from https://mail.python.org/pipermail/python-dev/2008-January/076194.html 3 | 4 | 5 | def patch_method_in(cls): 6 | """ 7 | To use: 8 | from import 9 | 10 | @monkey.patch_method() 11 | def (self, args): 12 | return 13 | 14 | This adds to 15 | """ 16 | def decorator(func): 17 | setattr(cls, func.__name__, func) 18 | return func 19 | return decorator 20 | 21 | 22 | def patch_class(name, bases, namespace): 23 | """ 24 | To use: 25 | 26 | from import 27 | 28 | class (): 29 | __metaclass__ = monkey.patch_class 30 | def (...): ... 31 | def (...): ... 32 | ... 33 | 34 | This adds , , etc. to , and makes 35 | a local alias for . 36 | """ 37 | assert len(bases) == 1, "Exactly one base class required" 38 | base = bases[0] 39 | for name, value in namespace.iteritems(): 40 | if name != "__metaclass__": 41 | setattr(base, name, value) 42 | return base 43 | -------------------------------------------------------------------------------- /web_test/assist/selene/__init__.py: -------------------------------------------------------------------------------- 1 | from . import report, shared 2 | -------------------------------------------------------------------------------- /web_test/assist/selene/report.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from typing import Tuple, Iterable, ContextManager, Protocol, Dict, Any 3 | 4 | 5 | class _ContextManagerFactory(Protocol): 6 | def __call__( 7 | self, *, title: str, params: Dict[str, Any], **kwargs 8 | ) -> ContextManager: 9 | ... 10 | 11 | 12 | def log_with( 13 | *, 14 | context: _ContextManagerFactory, 15 | translations: Iterable[Tuple[str, str]] = (), 16 | ): 17 | """ 18 | returns decorator factory with logging to Alure-like ContextManager 19 | with added list of translations 20 | to decorate Selene's waiting via config._wait_decorator 21 | """ 22 | 23 | def decorator_factory(wait): 24 | def decorator(for_): 25 | def decorated(fn): 26 | 27 | title = f'{wait.entity}: {fn}' 28 | 29 | def translate(initial: str, item: Tuple[str, str]): 30 | old, new = item 31 | return initial.replace(old, new) 32 | 33 | translated_title = reduce( 34 | translate, 35 | translations, 36 | title, 37 | ) 38 | 39 | with context(title=translated_title, params={}): 40 | return for_(fn) 41 | 42 | return decorated 43 | 44 | return decorator 45 | 46 | return decorator_factory 47 | -------------------------------------------------------------------------------- /web_test/assist/selene/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/web_test/assist/selene/shared/__init__.py -------------------------------------------------------------------------------- /web_test/assist/selene/shared/hook.py: -------------------------------------------------------------------------------- 1 | import allure 2 | from selene.core.exceptions import TimeoutException 3 | from selene.support.shared import browser 4 | 5 | 6 | def attach_snapshots_on_failure(error: TimeoutException) -> Exception: 7 | """ 8 | An example of selene hook_wait_failure that attaches snapshots to failed test step. 9 | It is actually might not needed, 10 | because using pytest_runtest_makereport hook 11 | you can achieve similar 12 | by attaching screenshots to the test body itself, 13 | that is more handy during analysis of test report 14 | 15 | but if you need it, you can use it by adding to your browser setup fixture:: 16 | 17 | import web_test 18 | browser.config.hook_wait_failure = \ 19 | web_test.assist.selene.shared.hook.attach_snapshots_on_failure 20 | 21 | otherwise, you can skip it;) 22 | """ 23 | last_screenshot = browser.config.last_screenshot 24 | if last_screenshot: 25 | allure.attach.file(source=last_screenshot, 26 | name='screenshot on failure', 27 | attachment_type=allure.attachment_type.PNG) 28 | 29 | last_page_source = browser.config.last_page_source 30 | if last_page_source: 31 | allure.attach.file(source=last_page_source, 32 | name='page source on failure', 33 | attachment_type=allure.attachment_type.HTML) 34 | return error 35 | -------------------------------------------------------------------------------- /web_test/assist/selenium/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/web_test/assist/selenium/__init__.py -------------------------------------------------------------------------------- /web_test/assist/selenium/typing.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from selenium.webdriver.remote.webdriver import WebDriver 4 | from selenium.webdriver.opera.options import Options as OperaOptions 5 | from selenium.webdriver.edge.options import Options as EdgeOptions 6 | import selenium 7 | 8 | WebDriverOptions = Union[ 9 | selenium.webdriver.ChromeOptions, 10 | selenium.webdriver.FirefoxOptions, 11 | selenium.webdriver.IeOptions, 12 | EdgeOptions, 13 | OperaOptions, 14 | ] 15 | -------------------------------------------------------------------------------- /web_test/assist/webdriver_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from . import set_up, supported 2 | -------------------------------------------------------------------------------- /web_test/assist/webdriver_manager/set_up.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Callable, Optional 2 | 3 | from selenium import webdriver 4 | from selenium.webdriver.remote.webdriver import WebDriver 5 | from webdriver_manager.chrome import ChromeDriverManager 6 | from webdriver_manager.firefox import GeckoDriverManager 7 | from webdriver_manager.microsoft import EdgeChromiumDriverManager, IEDriverManager 8 | from webdriver_manager.opera import OperaDriverManager 9 | from webdriver_manager.core.utils import ChromeType 10 | 11 | 12 | from web_test.assist.selenium.typing import WebDriverOptions 13 | from . import supported 14 | 15 | installers: Dict[ 16 | supported.BrowserName, 17 | Callable[[Optional[WebDriverOptions]], WebDriver] 18 | ] = { 19 | supported.chrome: 20 | lambda opts: webdriver.Chrome( 21 | ChromeDriverManager().install(), 22 | options=opts, 23 | ), 24 | supported.chromium: 25 | lambda opts: webdriver.Chrome( 26 | ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install(), 27 | options=opts, 28 | ), 29 | supported.firefox: 30 | lambda opts: webdriver.Firefox( 31 | executable_path=GeckoDriverManager().install(), 32 | options=opts, 33 | ), 34 | supported.ie: 35 | lambda opts: webdriver.Ie( 36 | IEDriverManager().install(), 37 | options=opts, 38 | ), 39 | supported.edge: 40 | lambda ____: webdriver.Edge( 41 | EdgeChromiumDriverManager().install(), 42 | ), 43 | supported.opera: 44 | lambda opts: webdriver.Opera( 45 | executable_path=OperaDriverManager().install(), 46 | options=opts, 47 | ), 48 | } 49 | 50 | 51 | def local( 52 | name: supported.BrowserName = 'chrome', # todo: consider change default to ... and then get it from options if passed 53 | options: WebDriverOptions = None 54 | ) -> WebDriver: 55 | return installers[name](options) 56 | -------------------------------------------------------------------------------- /web_test/assist/webdriver_manager/supported.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Final 2 | 3 | BrowserName = Literal['chrome', 'chromium', 'firefox', 'ie', 'edge', 'opera'] 4 | 5 | chrome: Final[BrowserName] = 'chrome' 6 | chromium: Final[BrowserName] = 'chromium' 7 | firefox: Final[BrowserName] = 'firefox' 8 | ie: Final[BrowserName] = 'ie' 9 | edge: Final[BrowserName] = 'edge' 10 | opera: Final[BrowserName] = 'opera' 11 | 12 | -------------------------------------------------------------------------------- /web_test/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/web_test/pages/__init__.py -------------------------------------------------------------------------------- /web_test/pages/ecosia.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | The previous line is needed to type hint classes that are defined later 5 | like Results class below 6 | """ 7 | 8 | from web_test.assist.allure.report import step 9 | from selene import by, have 10 | from selene.support.shared import browser 11 | 12 | """ 13 | Instead of "class with methods + object" below 14 | You can simply use raw functions inside the python module. 15 | Hence, you represent pages with modules over objects. 16 | The implementation will be more simple, aka KISS 17 | But you might miss some features of OOP, object properties, etc... 18 | that you might need with the time 19 | 20 | And also, you might miss the general fluent oop style, 21 | allowing you to write something like: 22 | 23 | page.table.row(1).cell(2).input.set_value('foo') 24 | 25 | For example, below we use this when `duckduckgo.results` property 26 | returns Results object of another "page", 27 | allowing "fluent chainable style". 28 | 29 | But maybe you don't need all these "additions", 30 | or you can use OOP-style pageobjects 31 | only for reusable generic widgets, 32 | like: dropdowns, datapickers, tables, etc. 33 | 34 | Also take into account your audience: 35 | - if a lot of manual or junior test engineers will write tests 36 | - then the lesser features you use the better 37 | - then probably sticking to one style everywhere is better 38 | - if you know what you do, and your audience is mature 39 | - then probably it's good to start from simplest solution 40 | - and use advanced features case by case 41 | when you really need them 42 | """ 43 | 44 | 45 | class Ecosia: 46 | 47 | @step 48 | def open(self): 49 | browser.open('https://www.ecosia.org/') 50 | 51 | @step 52 | def search(self, text): 53 | browser.element(by.name('q')).type(text).press_enter() 54 | 55 | @property 56 | def results(self) -> Results: 57 | return Results() 58 | 59 | 60 | class Results: 61 | 62 | def __init__(self): 63 | self.elements = browser.all('.result') 64 | 65 | @step 66 | def should_have_size_at_least(self, amount) -> Results: 67 | self.elements.should(have.size_greater_than_or_equal(amount)) 68 | return self 69 | 70 | @step 71 | def should_have_text(self, index, value) -> Results: 72 | self.elements[index].should(have.text(value)) 73 | return self 74 | 75 | @step 76 | def follow_link(self, index): 77 | self.elements[index].element('a').click() 78 | 79 | 80 | ecosia: Ecosia = Ecosia() 81 | """ 82 | This object here is needed for faster access to the page object. 83 | Hence you don't need to import class and create and object for it 84 | you can import object directly. 85 | 86 | Probably if use use app.py module collecting all such objects in one place 87 | that surves and root entry point to your application model 88 | then you don't need this object defined here. Then you can simple remove it. 89 | """ 90 | -------------------------------------------------------------------------------- /web_test/pages/github.py: -------------------------------------------------------------------------------- 1 | from selene import have 2 | from selene.support.shared import browser 3 | 4 | from web_test.assist.allure.report import step 5 | 6 | 7 | class Github: 8 | @step 9 | def should_be_on(self, title_text): 10 | browser.should(have.title_containing(title_text)) 11 | 12 | 13 | github: Github = Github() 14 | -------------------------------------------------------------------------------- /web_test/pages/google.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from web_test.assist.allure.report import step 4 | 5 | """ 6 | The previous line is needed to type hint classes that are defined later 7 | like Results class below 8 | """ 9 | 10 | 11 | from selene import by, have 12 | from selene.support.shared import browser 13 | 14 | 15 | class Google: 16 | """ 17 | This is a simpler example of PageObject pattern appliance. 18 | Here we use one pageobject instead of two 19 | 20 | Also, here we extensively use "fluent" style of "PageObject", 21 | aka Fluent PageObject pattern, when each method return an object 22 | of kind of "next page"... But this "next page" nuance is tricky... 23 | Read more in follow_result_link docstrings;) 24 | """ 25 | def __init__(self): 26 | self.results = browser.all('#search .g') 27 | 28 | @step 29 | def open(self) -> Google: 30 | browser.open('https://google.com/ncr') 31 | return self 32 | 33 | @step 34 | def search(self, text) -> Google: 35 | browser.element(by.name('q')).type(text).press_enter() 36 | return self 37 | 38 | @step 39 | def should_have_result(self, index, text) -> Google: 40 | self.results[index].should(have.text(text)) 41 | return self 42 | 43 | @step 44 | def should_have_results_amount_at_least(self, number) -> Google: 45 | self.results.should(have.size_greater_than_or_equal(number)) 46 | return self 47 | 48 | @step 49 | def follow_result_link(self, text): 50 | """ 51 | Here we could return "next page object", 52 | following so called Fluent PageObject pattern 53 | but usually it might lead to confusion in such cases. 54 | For example, what if we fail to follow the link 55 | and this is "as expected", e.g. according to our 56 | "negative" test case conditions. 57 | Then it's logically to expect "same pageobject" not "next one" 58 | Now we have two potential state as a result of this method execution. 59 | And it's not clear what to return;) 60 | So better to return "void"/None in such cases. 61 | Usually Fluent PageObject makes sense only in cases 62 | with only 1 possible result state, for example: 63 | - returning self (we for sure stay on the same page) 64 | - returning "sub-page-object" i.e. object of component on the page 65 | (it's always there) 66 | """ 67 | self.results.element_by(have.text(text)).element('a').click() 68 | -------------------------------------------------------------------------------- /web_test/pages/pypi.py: -------------------------------------------------------------------------------- 1 | from selene.support.shared import browser 2 | 3 | url = 'https://pypi.org/' 4 | 5 | search = browser.element('#search') 6 | results = browser.all('.package-snippet') -------------------------------------------------------------------------------- /web_test/pages/python_org.py: -------------------------------------------------------------------------------- 1 | from selene import have 2 | from selene.support.shared import browser 3 | from web_test.assist.allure.report import step 4 | 5 | """ 6 | The file is named with _org in the end just to make it more explicit – 7 | that this official python site page 8 | """ 9 | 10 | _url = 'https://www.python.org/' 11 | 12 | _results = browser.all('.list-recent-events>li') 13 | 14 | 15 | @step 16 | def open(): 17 | browser.open(_url) 18 | 19 | 20 | @step 21 | def should_be_opened(): 22 | browser.should(have.url(_url)) 23 | 24 | 25 | @step 26 | def search(text): 27 | browser.element('#id-search-field').type(text).press_enter() 28 | 29 | 30 | @step 31 | def should_have_result(index, text): 32 | _results[index].should(have.text(text)) 33 | -------------------------------------------------------------------------------- /web_test/pages/searchencrypt.py: -------------------------------------------------------------------------------- 1 | from selene import by, have 2 | from selene.support.shared import browser 3 | 4 | from web_test.assist.allure.report import step 5 | 6 | """ 7 | This is a simplest possible implementation of pages model. 8 | It is based on simple features of "Modular Paradigm" (over OOP). 9 | It's dead simple, aka KISS, which has a lot of benefits, like 10 | ability to quickly train a junior team in extending automation coverage. 11 | 12 | Though it lacks some "oop style" features, like "chainable fluent" style. 13 | I.e. instead of 14 | 15 | web.google\ 16 | .search('selene python')\ 17 | .should_have_results_amount_at_least(5) 18 | 19 | you have to write: 20 | 21 | web.google.search('selene python')\ 22 | web.google.should_have_results_amount_at_least(5) 23 | 24 | Technically it's possible to make the "fluent" style work, by something like this: 25 | 26 | import sys 27 | self = sys.modules[__name__] 28 | 29 | def search(text): 30 | # impl... 31 | return self 32 | 33 | But... The autocomplete will not work in such case... 34 | Hence, better not to over-complicate;) 35 | """ 36 | 37 | 38 | results = browser.all('.web-result') 39 | 40 | 41 | @step 42 | def visit(): 43 | """ 44 | Also, here... we have to rename open to visit, 45 | in order to eliminate potential conflicts 46 | with python built in `open` function 47 | """ 48 | browser.open('https://www.searchencrypt.com') 49 | 50 | 51 | @step 52 | def search(text): 53 | browser.element(by.name('q')).type(text) 54 | submit = browser.element('.fas.fa-search') 55 | submit.click() 56 | 57 | 58 | @step 59 | def should_have_result(index, text): 60 | results[index].should(have.text(text)) 61 | 62 | 63 | @step 64 | def should_have_results_amount_at_least(number): 65 | results.should(have.size_greater_than_or_equal(number)) 66 | 67 | 68 | @step 69 | def follow_result_link(text): 70 | results.element_by(have.text(text)).element('a').click() 71 | browser.switch_to_next_tab() 72 | -------------------------------------------------------------------------------- /web_test/test_markers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yashaka/python-web-test/0630a61ba338195028753954e73218f5219e2c02/web_test/test_markers/__init__.py -------------------------------------------------------------------------------- /web_test/test_markers/mark.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is to integrate allure markers (labels/tags) into pytest markers. 3 | So we can label test cases and achieve two things at once: 4 | - log them correspondingly in allure report 5 | - filter test cases when running via `pytest -m` option 6 | 7 | When extending this module with more markers, 8 | ensure you update the pytest.ini file correspondingly;) 9 | 10 | Additional Resources: 11 | - https://docs.qameta.io/allure/#_pytest 12 | - https://docs.qameta.io/allure/#_tags 13 | - https://docs.pytest.org/en/latest/example/markers.html 14 | - https://github.com/pytest-dev/pytest-rerunfailures 15 | """ 16 | 17 | import pytest 18 | import allure 19 | import functools 20 | 21 | 22 | def pending(test_fn): # todo: consider impl as pytest fixture 23 | def decorated(*args, **kwargs): 24 | test_fn(*args, **kwargs) 25 | pytest.skip('as pending') 26 | return decorated 27 | 28 | 29 | import functools 30 | 31 | 32 | @functools.wraps(pytest.mark.flaky) 33 | def flaky(func=..., *, reruns: int = 0, reruns_delay: int = 0, condition=True): 34 | """ 35 | alias to pytest.mark.flaky(reruns, reruns_delay) 36 | from pytest-rerunfailures plugin 37 | (no need to mention in pytest.ini) 38 | """ 39 | 40 | @functools.wraps(pytest.mark.flaky) 41 | def allurish_decorator(func_): 42 | return pytest.mark.flaky( 43 | reruns=reruns, 44 | reruns_delay=reruns_delay, 45 | condition=condition, 46 | )(allure.tag('flaky')(func_)) 47 | 48 | return allurish_decorator(func) if callable(func) else allurish_decorator 49 | 50 | 51 | class suite: 52 | @staticmethod 53 | @functools.wraps(pytest.mark.smoke) 54 | def smoke(func): 55 | return pytest.mark.smoke(allure.suite('smoke')(func)) 56 | 57 | 58 | class tag: 59 | @staticmethod 60 | @functools.wraps(pytest.mark.in_progress) 61 | def in_progress(func): 62 | return pytest.mark.in_progress(allure.tag('in_progress')(func)) 63 | 64 | @staticmethod 65 | @functools.wraps(pytest.mark.fast) 66 | def fast(func): 67 | return pytest.mark.fast(allure.tag('fast')(func)) 68 | --------------------------------------------------------------------------------