├── __init__.py ├── testlib ├── __init__.py └── ui │ ├── __init__.py │ ├── pages │ ├── __init__.py │ ├── navigation_button.py │ ├── team_form.py │ └── login_form.py │ ├── by.py │ ├── ui.py │ ├── application.py │ └── driver.py ├── .python-version ├── tox.ini ├── config ├── __init__.py ├── routing.py ├── env.py ├── auth.py └── test_run.py ├── .gitignore ├── scripts ├── run_ios_tests.sh ├── run_web_tests.sh ├── run_android_tests.sh └── install_deps.sh ├── Pipfile ├── tests ├── ui │ └── demo_test.py └── conftest.py ├── README.md └── Pipfile.lock /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testlib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testlib/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7.3 2 | -------------------------------------------------------------------------------- /testlib/ui/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from . import auth 2 | from . import test_run 3 | from . import routing 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pytest 2 | .cache/ 3 | .pytest_cache/ 4 | 5 | 6 | # Pycharm 7 | .idea/* 8 | 9 | 10 | *.pyc -------------------------------------------------------------------------------- /scripts/run_ios_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PLATFORM=IOS 4 | 5 | python3 -m pytest tests/ui/ -l -vv --tb=short 6 | -------------------------------------------------------------------------------- /scripts/run_web_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PLATFORM=WEB 4 | 5 | python3 -m pytest tests/ui/ -l -vv --tb=short 6 | -------------------------------------------------------------------------------- /scripts/run_android_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PLATFORM=ANDROID 4 | 5 | python3 -m pytest tests/ui/ -l -vv --tb=short 6 | -------------------------------------------------------------------------------- /config/routing.py: -------------------------------------------------------------------------------- 1 | from . import env 2 | 3 | LOGIN_PAGE_URL = env.get('LOGIN_PAGE_URL', 'http://localhost:8080') 4 | 5 | ORGANIZATION_URL = env.get('ORGANIZATION_URL') 6 | -------------------------------------------------------------------------------- /scripts/install_deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pip3 install pipenv 3 | 4 | # there is an issue https://github.com/pypa/pipenv/issues/2092 5 | export LANG=en_US.UTF-8 6 | export LC_ALL=en_US.UTF-8 7 | 8 | pipenv install --three --ignore-pipfile 9 | -------------------------------------------------------------------------------- /testlib/ui/by.py: -------------------------------------------------------------------------------- 1 | from appium.webdriver.common.mobileby import MobileBy 2 | from selene.support import by 3 | 4 | 5 | def accessibility_id(_id): 6 | return MobileBy.ACCESSIBILITY_ID, _id 7 | 8 | 9 | css = by.css 10 | xpath = by.xpath 11 | -------------------------------------------------------------------------------- /config/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get(key, default=None): 5 | return os.environ.get(key=key, default=default) 6 | 7 | 8 | def get_bool(key, default=None): 9 | value = get(key, default) 10 | return f'{value}'.lower() == 'true' 11 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | selenium = "==3.14.1" 11 | appium-python-client = "*" 12 | selene = "==1.0.0a13" 13 | pytest = "*" 14 | 15 | 16 | [dev-packages] 17 | 18 | pycodestyle = "*" 19 | 20 | 21 | [requires] 22 | 23 | python_version = "3.7" 24 | -------------------------------------------------------------------------------- /testlib/ui/pages/navigation_button.py: -------------------------------------------------------------------------------- 1 | from .. import by, ui 2 | 3 | page = ui.element({ 4 | 'IOS': by.accessibility_id('uptick.DrawerMenu'), 5 | 'WEB': by.css('[data-testid="menu-toggler-block"]'), 6 | 'ANDROID': by.xpath( 7 | '//android.support.v4.widget.DrawerLayout//android.widget.LinearLayout//android.widget.ImageButton' 8 | ) 9 | }) 10 | -------------------------------------------------------------------------------- /config/auth.py: -------------------------------------------------------------------------------- 1 | from . import env 2 | 3 | DEFAULT_LOGIN = env.get('DEFAULT_LOGIN', 'default_login_plz_change_me') 4 | DEFAULT_PASSWORD = env.get('DEFAULT_PASSWORD', 'default_password_plz_change_me') 5 | 6 | USERS = { 7 | 'default': { 8 | 'password': DEFAULT_PASSWORD, 9 | 'email': DEFAULT_LOGIN, 10 | }, 11 | } 12 | 13 | 14 | def get_user(user): 15 | _user = USERS.get(user) 16 | if not _user: 17 | raise ValueError('Undefined user name') 18 | return _user 19 | -------------------------------------------------------------------------------- /testlib/ui/pages/team_form.py: -------------------------------------------------------------------------------- 1 | from .. import by, ui 2 | 3 | url = ui.element({ 4 | 'IOS': by.accessibility_id('teamname-text-input'), 5 | 'ANDROID': by.xpath('//android.support.v4.widget.DrawerLayout//android.widget.EditText') 6 | }) 7 | 8 | next_button = ui.element({ 9 | 'IOS': by.accessibility_id("team-select-next-button"), 10 | 'ANDROID': by.xpath( 11 | '//android.view.ViewGroup/android.view.ViewGroup/android.widget.TextView' 12 | ) 13 | }) 14 | 15 | 16 | def type_url(text): 17 | url.set(text) 18 | next_button.click() 19 | -------------------------------------------------------------------------------- /tests/ui/demo_test.py: -------------------------------------------------------------------------------- 1 | from selene.conditions import visible 2 | 3 | import config 4 | 5 | 6 | def test_login_with_login_and_password(app): 7 | app.visit_page(page_name='login') 8 | 9 | app.login_form.login_with( 10 | email=config.auth.DEFAULT_LOGIN, 11 | password=config.auth.DEFAULT_PASSWORD 12 | ) 13 | 14 | app.navigation_button.page.should_be(visible) 15 | 16 | 17 | def test_login_by_default_user(app): 18 | app.visit_page(page_name='login') 19 | 20 | app.login_as(as_user='default') 21 | 22 | app.navigation_button.page.should_be(visible) 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selene import factory 3 | 4 | import config 5 | from testlib.ui import driver, application 6 | 7 | 8 | @pytest.fixture(scope='session' if config.test_run.ONE_SESSION else 'function') 9 | def app(): 10 | ui_driver = driver.get() 11 | factory.set_shared_driver(ui_driver) 12 | 13 | yield application 14 | 15 | driver.close(ui_driver) 16 | 17 | 18 | @pytest.fixture(autouse=config.test_run.BROWSER_LOGS and config.test_run.IS_WEB) 19 | def browser_log(app): 20 | """Printing browser logs for web tests""" 21 | yield 22 | 23 | print("\n\tBROWSER CONSOLE LOGS:") 24 | for log in app.driver.get_log('browser'): 25 | print(f'\n{log}\n') 26 | -------------------------------------------------------------------------------- /testlib/ui/ui.py: -------------------------------------------------------------------------------- 1 | from selene.support.jquery_style_selectors import s, ss 2 | # from selene.conditions import enabled 3 | import config 4 | 5 | 6 | def locator_for_platform(selectors): 7 | return selectors.get(config.test_run.PLATFORM, 'Undefined Selector') 8 | 9 | 10 | def element(selectors): 11 | return s(locator_for_platform(selectors)) 12 | 13 | 14 | def elements(selectors): 15 | return ss(locator_for_platform(selectors)) 16 | 17 | 18 | # def hacked_click(by_element): 19 | # # 11.3 mac os problems with enabled, but not visible elements 20 | # by_element.should_be(enabled) 21 | # selenium_element = by_element.__delegate__ 22 | # selenium_element.click() 23 | # return by_element 24 | -------------------------------------------------------------------------------- /testlib/ui/pages/login_form.py: -------------------------------------------------------------------------------- 1 | from .. import by, ui 2 | 3 | user_email = ui.element({ 4 | 'IOS': by.accessibility_id('emailInput'), 5 | 'WEB': by.css('[type=login]'), 6 | 'ANDROID': by.xpath('(//android.widget.EditText)[1]') 7 | }) 8 | 9 | user_password = ui.element({ 10 | 'IOS': by.accessibility_id('passwordInput'), 11 | 'WEB': by.css('[type=password]'), 12 | 'ANDROID': by.xpath('(//android.widget.EditText)[2]') 13 | }) 14 | 15 | submit_button = ui.element({ 16 | 'IOS': by.accessibility_id('signin-button'), 17 | 'WEB': by.css('[type="submit"]'), 18 | 'ANDROID': by.xpath('//android.widget.FrameLayout/android.view.ViewGroup[2]//android.widget.TextView') 19 | }) 20 | 21 | 22 | def login_with(email, password): 23 | user_email.set(email) 24 | user_password.set(password) 25 | submit_button.click() 26 | -------------------------------------------------------------------------------- /testlib/ui/application.py: -------------------------------------------------------------------------------- 1 | import config 2 | from selene import browser 3 | from .pages import login_form 4 | from .pages import team_form 5 | from .pages import navigation_button 6 | 7 | login_form = login_form 8 | team_form = team_form 9 | navigation_button = navigation_button 10 | 11 | 12 | def login_as(as_user): 13 | user = config.auth.get_user(as_user) 14 | login_form.login_with(email=user['email'], password=user['password']) 15 | 16 | 17 | def assert_page_name(page_name): 18 | pages = ['login'] 19 | if page_name not in pages: 20 | raise ValueError(f'Undefined {page_name} page name. Should be in {pages} pages') 21 | 22 | 23 | def visit_page(page_name): 24 | page = page_name.lower() 25 | assert_page_name(page) 26 | if config.test_run.IS_WEB: 27 | browser.open_url(config.routing.LOGIN_PAGE_URL) 28 | # uncomment if your mobile app have environment choosing screen 29 | # if page_name.lower() == 'login': 30 | # if config.test_run.IS_MOBILE: 31 | # team_form.type_url(text=config.routing.ORGANIZATION_URL) 32 | -------------------------------------------------------------------------------- /config/test_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from selene import config as _selene_config 3 | 4 | from . import env 5 | 6 | ONE_SESSION = env.get_bool('ONE_SESSION', 'False') 7 | BROWSER_LOGS = env.get_bool('BROWSER_LOGS', 'False') 8 | 9 | _selene_config.timeout = int(env.get('UI_TIMEOUT', 15)) 10 | APPIUM_TIMEOUT = int(env.get('APPIUM_TIMEOUT', 60)) 11 | 12 | PLATFORM = env.get('PLATFORM', 'IOS') 13 | # shortcuts 14 | IS_WEB = PLATFORM == 'WEB' 15 | IS_MOBILE = PLATFORM in ('IOS', 'ANDROID') 16 | IS_ANDROID = PLATFORM == 'ANDROID' 17 | IS_IOS = PLATFORM == 'IOS' 18 | 19 | APPIUM_SERVER = env.get('APPIUM_SERVER', 'http://localhost:4723/wd/hub') 20 | 21 | APP_NAME = { 22 | 'IOS': env.get('IOS_APP_NAME', 'UptickCrmMobile.app'), 23 | 'ANDROID': env.get('ANDROID_APP_NAME', 'app-release.apk') 24 | }.get(PLATFORM) 25 | 26 | _DEFAULT_APP_PATH = str(os.path.join(os.getcwd(), fr"mobile_app/{APP_NAME}")) 27 | MOBILE_APP = env.get('MOBILE_APP', _DEFAULT_APP_PATH) if IS_MOBILE else None 28 | 29 | ANDROID_VERSION = env.get('ANDROID_VERSION', '8.1.0') 30 | ANDROID_DEVICE_NAME = env.get('ANDROID_DEVICE_NAME', 'Pixel_2_XL_API_27') 31 | 32 | IOS_VERSION = env.get('IOS_VERSION', '11.4') 33 | IOS_DEVICE_NAME = env.get('IOS_DEVICE_NAME', 'iPhone X') 34 | -------------------------------------------------------------------------------- /testlib/ui/driver.py: -------------------------------------------------------------------------------- 1 | from appium import webdriver as appium_driver 2 | from selenium import webdriver as selenium_driver 3 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 4 | from webdriver_manager.chrome import ChromeDriverManager 5 | 6 | import config 7 | 8 | _capabilities = { 9 | 'IOS': { 10 | 'version': '1.8.0', 11 | 'platformName': 'iOS', 12 | 'platformVersion': config.test_run.IOS_VERSION, 13 | 'deviceName': config.test_run.IOS_DEVICE_NAME, 14 | 'app': config.test_run.MOBILE_APP, 15 | 'noReset': False, 16 | 'fullReset': False, 17 | 'newCommandTimeout': config.test_run.APPIUM_TIMEOUT, 18 | }, 19 | 'ANDROID': { 20 | 'version': '1.8.0', 21 | 'platformName': 'Android', 22 | 'platformVersion': config.test_run.ANDROID_VERSION, 23 | 'deviceName': config.test_run.ANDROID_DEVICE_NAME, 24 | 'app': config.test_run.MOBILE_APP, 25 | 'noReset': False, 26 | 'fullReset': False, 27 | 'newCommandTimeout': config.test_run.APPIUM_TIMEOUT, 28 | "automationName": "UiAutomator2", 29 | "browserName": "" 30 | }, 31 | 'WEB': { 32 | **DesiredCapabilities.CHROME, 33 | 'loggingPrefs': {'browser': 'ALL'} 34 | } 35 | } 36 | 37 | 38 | def get(): 39 | caps = _capabilities[config.test_run.PLATFORM] 40 | if config.test_run.IS_MOBILE: 41 | return appium_driver.Remote( 42 | command_executor=config.test_run.APPIUM_SERVER, 43 | desired_capabilities=caps 44 | ) 45 | 46 | return selenium_driver.Chrome( 47 | executable_path=ChromeDriverManager().install(), 48 | desired_capabilities=caps 49 | ) 50 | 51 | 52 | def close(driver): 53 | driver.close_app() if config.test_run.IS_MOBILE else driver.quit() 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo example for [presentation](https://2018.heisenbug-piter.ru/en/talks/2018/spb/6plww0slg8akuymkumm4iq/) [[video]](https://www.youtube.com/watch?v=YoMt08OcxMI) 2 | target OS - Mac, so detailed install information provided only for it. 3 | What is it? 4 | Demo login test for "one test for several platforms" approach 5 | 6 | What do you need? 7 | .app or/and .apk applications or/and web versions your application 8 | 9 | # Setup tools (if you don't have some of them) 10 | 1. Install brew `ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 11 | 2. Install Python 3.6+: `brew install python3` 12 | 3. Install npm: `brew install npm` 13 | 4. Install Appium server: `npm install -g appium` or look [here](http://appium.io/) 14 | 5. For better debugging you can install [Appium inspector](https://github.com/appium/appium-desktop/releases) 15 | 6. Install Carthage: `brew install carthage` 16 | 7. Install chrome browser (or use some other - look at [driver configuration](testlib/ui/driver.py)) 17 | 8. Install xcode (for ios simulator) 18 | 9. Install [android studio](https://developer.android.com/studio/) or any android simulator 19 | 10. Install Python dependencies: `. ./scripts/install_deps.sh `(Terminal from root project folder) 20 | 11. I can miss something: you can try for example [this](https://medium.com/@appiummanager/appium-setup-from-scratch-6388f5e6caee) 21 | 22 | # Customize for your application: 23 | 1. Put ".app" or/and ".apk" into [mobile app](mobile_app) directory 24 | 2. Add application names in [config](config/test_run.py) or environment variables: 25 | * `export IOS_APP_NAME=my_app.app` 26 | * `export ANDROID_APP_NAME=my_app.apk` 27 | * `export LOGIN_PAGE_URL=https://my-app.com` for web version 28 | 3. Change locators for (there are some examples): 29 | * [login page](testlib/ui/pages/login_form.py) 30 | * [navigation page](testlib/ui/pages/navigation_button.py) - any element for assert success login 31 | 4. Configure through env variables or [configuration files](config/): 32 | * `export DEFAULT_LOGIN=admin` 33 | * `export DEFAULT_PASSWORD=1234` 34 | 5. If mobile app have environment choosing screen: 35 | * Uncomment [few lines](testlib/ui/application.py) 36 | * Set environment variable ORGANIZATION_URL 37 | * Add locators for [login page](testlib/ui/pages/team_form.py) 38 | 39 | # Customize for your environment: 40 | 1. You also can [configure](config/test_run.py) devices, collecting browser logs and etc 41 | (through ANDROID_VERSION, ANDROID_DEVICE_NAME, IOS_VERSION, IOS_DEVICE_NAME variables) 42 | 43 | # Run tests: 44 | 1. Run appium server `appium` for iOS or Android 45 | 2. Choose platform `export PLATFORM='WEB'` or `IOS` or `ANDROID` 46 | 3. Activate virtual environment with dependencies: `pipenv shell` 47 | 4. Run tests `python3 -m pytest tests/ui` or just: 48 | * `. ./scripts/run_ios_tests.sh` 49 | * `. ./scripts/run_android_tests.sh` 50 | * `. ./scripts/run_web_tests.sh` 51 | 52 | # Useful links: 53 | 1. [Appium-python documentation](https://github.com/appium/python-client) 54 | 2. [Selene](https://github.com/yashaka/selene) 55 | 3. [Pytest](https://docs.pytest.org/en/latest/) 56 | 4. If you want to interact with some elements through pure selenium / appium look at [commented method as example](testlib/ui/ui.py) 57 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "98ab02e89b9b94a121833f2be66bdd5b72bfa090afa85fa95ad1db52dae5bb51" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "appium-python-client": { 20 | "hashes": [ 21 | "sha256:8decd5a03159d6642ceb4040cb585c2ff874954d2ad830daab04defb9143c70e" 22 | ], 23 | "index": "pypi", 24 | "version": "==0.46" 25 | }, 26 | "atomicwrites": { 27 | "hashes": [ 28 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 29 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 30 | ], 31 | "version": "==1.3.0" 32 | }, 33 | "attrs": { 34 | "hashes": [ 35 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 36 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 37 | ], 38 | "version": "==19.1.0" 39 | }, 40 | "backports.functools-lru-cache": { 41 | "hashes": [ 42 | "sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a", 43 | "sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd" 44 | ], 45 | "version": "==1.5" 46 | }, 47 | "certifi": { 48 | "hashes": [ 49 | "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", 50 | "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" 51 | ], 52 | "version": "==2019.6.16" 53 | }, 54 | "chardet": { 55 | "hashes": [ 56 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 57 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 58 | ], 59 | "version": "==3.0.4" 60 | }, 61 | "colorama": { 62 | "hashes": [ 63 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 64 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 65 | ], 66 | "version": "==0.4.1" 67 | }, 68 | "configparser": { 69 | "hashes": [ 70 | "sha256:8be81d89d6e7b4c0d4e44bcc525845f6da25821de80cb5e06e7e0238a2899e32", 71 | "sha256:da60d0014fd8c55eb48c1c5354352e363e2d30bbf7057e5e171a468390184c75" 72 | ], 73 | "version": "==3.7.4" 74 | }, 75 | "crayons": { 76 | "hashes": [ 77 | "sha256:41f0843815a8e3ac6fb445b7970d8b9c766e6f164092d84e7ea809b4c91418ec", 78 | "sha256:8edcadb7f197e25f2cc094aec5bf7f1b6001d3f76c82d56f8d46f6fb1405554f" 79 | ], 80 | "version": "==0.2.0" 81 | }, 82 | "future": { 83 | "hashes": [ 84 | "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" 85 | ], 86 | "version": "==0.17.1" 87 | }, 88 | "idna": { 89 | "hashes": [ 90 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 91 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 92 | ], 93 | "version": "==2.8" 94 | }, 95 | "importlib-metadata": { 96 | "hashes": [ 97 | "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", 98 | "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" 99 | ], 100 | "version": "==0.18" 101 | }, 102 | "more-itertools": { 103 | "hashes": [ 104 | "sha256:3ad685ff8512bf6dc5a8b82ebf73543999b657eded8c11803d9ba6b648986f4d", 105 | "sha256:8bb43d1f51ecef60d81854af61a3a880555a14643691cc4b64a6ee269c78f09a" 106 | ], 107 | "version": "==7.1.0" 108 | }, 109 | "packaging": { 110 | "hashes": [ 111 | "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", 112 | "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" 113 | ], 114 | "version": "==19.0" 115 | }, 116 | "pluggy": { 117 | "hashes": [ 118 | "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", 119 | "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" 120 | ], 121 | "version": "==0.12.0" 122 | }, 123 | "py": { 124 | "hashes": [ 125 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 126 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 127 | ], 128 | "version": "==1.8.0" 129 | }, 130 | "pyparsing": { 131 | "hashes": [ 132 | "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", 133 | "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" 134 | ], 135 | "version": "==2.4.0" 136 | }, 137 | "pytest": { 138 | "hashes": [ 139 | "sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", 140 | "sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" 141 | ], 142 | "index": "pypi", 143 | "version": "==5.0.1" 144 | }, 145 | "requests": { 146 | "hashes": [ 147 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 148 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 149 | ], 150 | "version": "==2.22.0" 151 | }, 152 | "selene": { 153 | "hashes": [ 154 | "sha256:c09f49a8299e8c3c26d7998c5282677fa41177ec3a5f4ff4212724f1754d3422", 155 | "sha256:c9c59bcfd14c66401e81c732fe51cedbe7d181e2f6c4b2d1387b87281e994325" 156 | ], 157 | "index": "pypi", 158 | "version": "==1.0.0a13" 159 | }, 160 | "selenium": { 161 | "hashes": [ 162 | "sha256:ab192cd046164c40fabcf44b47c66c8b12495142f4a69dcc55ea6eeef096e614", 163 | "sha256:fdb6b1143d8899e8a32e358ad05bf5d89a480dbac359dbbd341592aa8696dcd1" 164 | ], 165 | "index": "pypi", 166 | "version": "==3.14.1" 167 | }, 168 | "six": { 169 | "hashes": [ 170 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 171 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 172 | ], 173 | "version": "==1.12.0" 174 | }, 175 | "urllib3": { 176 | "hashes": [ 177 | "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", 178 | "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" 179 | ], 180 | "version": "==1.25.3" 181 | }, 182 | "wcwidth": { 183 | "hashes": [ 184 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 185 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 186 | ], 187 | "version": "==0.1.7" 188 | }, 189 | "webdriver-manager": { 190 | "hashes": [ 191 | "sha256:4e3f522f937fd6ab246d63ee06612287bc122b70bb6034266459de86953b8645" 192 | ], 193 | "version": "==1.8.2" 194 | }, 195 | "zipp": { 196 | "hashes": [ 197 | "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", 198 | "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" 199 | ], 200 | "version": "==0.5.2" 201 | } 202 | }, 203 | "develop": { 204 | "pycodestyle": { 205 | "hashes": [ 206 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 207 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 208 | ], 209 | "index": "pypi", 210 | "version": "==2.5.0" 211 | } 212 | } 213 | } 214 | --------------------------------------------------------------------------------