├── .gitignore ├── README.md ├── common └── tools.py ├── config └── dev.yaml ├── conftest.py ├── fixtures └── dev │ └── baidu_search.yaml ├── page └── baidu │ ├── baidu_search2_page.py │ └── baidu_search_page.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini └── testcase ├── baidu ├── baidu_search2_test.py ├── baidu_search_test.py └── baidu_search_with_multi-browser_test.py └── shopxo └── admin_login_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # 缓存目录 2 | __pycache__/ 3 | 4 | # pre-commit目录 5 | .mypy_cache/ 6 | .pytest_cache/ 7 | 8 | # 日志目录 9 | output/ 10 | 11 | # 其他 12 | .idea 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playwright test 2 | 3 | 本测试项目依赖于pytest,可参考以下中文文档 4 | 5 | playwright 文档 6 | https://pypi.org/project/playwright/ 7 | https://playwright.dev/python/docs/intro/ 8 | 9 | pytest-playwright 文档 10 | https://pypi.org/project/pytest-playwright/ 11 | 12 | 本项目参考了虫师 & Yusuke Iwaki 的项目 13 | https://github.com/defnngj/playwright-pro 14 | https://zenn.dev/yusukeiwaki/articles/cfda648dc170e5 15 | 16 | 17 | ## 1. 安装 18 | ### 1.1 clone 或 下载 19 | ```bash 20 | # clone 项目 21 | git clone https://github.com/tomoyachen/playwright-test.git 22 | ``` 23 | 24 | ### 1.2 安装依赖 25 | ```bash 26 | # 本地安装 poetry 27 | pip install poetry 28 | 29 | # 创建虚拟环境并安装依赖 (在项目根目录执行) 30 | poetry install 31 | 32 | # PlayWright 安装浏览器驱动 33 | poetry run playwright install 34 | ``` 35 | 36 | 37 | ## 2. 执行测试 38 | ## 2.1 PyCharm 执行 39 | 查看虚拟环境路径 40 | ``` 41 | poetry shell 42 | ``` 43 | 44 | PyCharm 配置运行环境 45 | Preferences -> Project Interpreter -> Show All -> + (添加) -> Existing environment -> … (浏览) -> 选择刚才创建的虚拟环境目录 46 | 47 | PyCharm 设置 Pytest 为 默认测试运行器 48 | Preferences -> Tools -> Python Integrated Tools -> Testing -> pytest 49 | 50 | PyCharm 执行 测试用例 51 | 配置成功后,符合命名规则的测试类与测试方法可以在行数列快速执行 52 | 53 | ## 2.2 命令行执行 54 | 命令: 55 | 56 | pytest 用例路径 -参数 --参数 57 | 58 | 59 | 例: 60 | ```bash 61 | 例(虚拟环境): 62 | pytest testcase -s -v 63 | 64 | 例(本地环境) 65 | poetry run pytest testcase --ignore=testcase/aaa/bbb -s -v 66 | ``` 67 | 68 | 常用参数: 69 | * -s 显示打印内容 70 | * -v 显示详细执行过程 71 | * --browser 指定浏览器 chromium, webkit, firefox(pytest-playwright) 72 | * --headed 有头模式执行,不传此参数就是无头浏览器执行(pytest-playwright) 73 | 74 | pytest.ini 文件中,addopts 可以配置默认附带参数 75 | 76 | 77 | ## 3. 配置信息 78 | 79 | 业务相关配置在 `config` 目录下的 yaml 文件中 80 | * test 方法 或 fixture 方法中,使用 env fixture 来读取配置信息 81 | * 其他代码中,使用Tools.get_config() 来读取配置信息 82 | 83 | Pytest 配置 在pytest.ini 文件中 84 | 85 | ## 4. 预检查【扩展】 86 | ### 4.1 pre-commit 87 | 88 | 每次git commit都会自动检查,并且会自动修复一部分格式问题,通过检查才会提交成功 89 | * 记得 git add . 来更新被检测文件 90 | 91 | ```bash 92 | #第一次需要手动执行如下内容 93 | pre-commit install #安装git hook脚本 94 | pre-commit run --all-files #运行所配置的所有规则,使其起作用 95 | ``` 96 | 97 | 98 | ## 5. 其他 99 | ### 5.1 Playwrigth 录制功能 100 | ```bash 101 | # 快速启动录制工具 102 | python -m playwright codegen 103 | 104 | # 指定输出 py 文件、指定 baseUrl 105 | python -m playwright codegen --target python -o testcase/sample.py https://www.baidu.com/ 106 | 107 | # 查看帮助 108 | python -m playwright codegen --help 109 | ``` 110 | 111 | ### 5.2 已知的编码问题 112 | mac、linux 和 windows 系统下 对 pytest.int 文件中的中文解码方式不同。 113 | mac、linux 使用 utf-8,windows 使用 ASCII 114 | 最简单的方式就是不使用中文 -------------------------------------------------------------------------------- /common/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import yaml 4 | 5 | 6 | class Tools: 7 | def __init__(self, cookies=None): 8 | self.cookies = cookies 9 | 10 | @staticmethod 11 | def get_file_dir(file): 12 | o_path = os.getcwd() 13 | separator = '\\' if 'Windows' in platform.system() else '/' 14 | str = o_path 15 | str = str.split(separator) 16 | while len(str) > 0: 17 | spath = separator.join(str) + separator + file 18 | leng = len(str) 19 | if os.path.exists(spath): 20 | return os.path.dirname(spath) 21 | str.remove(str[leng - 1]) 22 | 23 | @staticmethod 24 | def get_root_dir(): 25 | return Tools.get_file_dir('conftest.py') 26 | 27 | @staticmethod 28 | def set_env(key, value): 29 | os.environ[key] = str(value) 30 | 31 | @staticmethod 32 | def get_env(key=None): 33 | tmp_system_env = {} 34 | for tmp_key in os.environ: 35 | tmp_value = os.environ[tmp_key] 36 | if tmp_value == "True": 37 | tmp_value = True 38 | elif tmp_value == "False": 39 | tmp_value = False 40 | tmp_system_env[tmp_key] = tmp_value 41 | if key: 42 | return tmp_system_env.get(key) 43 | return tmp_system_env 44 | 45 | @staticmethod 46 | def get_config(key=None): 47 | env = Tools.get_env("TEST_ENV") 48 | with open(os.path.join(Tools.get_root_dir(), 'config', f"{env}.yaml"), encoding="UTF-8") as f: 49 | config = yaml.load(f.read(), Loader=yaml.SafeLoader) 50 | if key is not None: 51 | return config[key] 52 | return config 53 | 54 | @staticmethod 55 | def get_fixtures(filename, key=None): 56 | env = Tools.get_env("TEST_ENV") 57 | with open(os.path.join(Tools.get_root_dir(), 'fixtures', f"{env}", f"{filename}.yaml"), encoding="UTF-8") as f: 58 | fixtures = yaml.load(f.read(), Loader=yaml.SafeLoader) 59 | if key is not None: 60 | return fixtures[key] 61 | return fixtures -------------------------------------------------------------------------------- /config/dev.yaml: -------------------------------------------------------------------------------- 1 | # 域名 2 | base_url: https://www.baidu.com -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from py.xml import html 4 | import time 5 | # from playwright.sync_api import sync_playwright 6 | from playwright.async_api import Page, Browser 7 | 8 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | OUTPUT_DIR = os.path.join(ROOT_DIR, "output", time.strftime("%Y%m%d%H%M%S")) 10 | 11 | 12 | @pytest.fixture(scope="class") 13 | def page(browser: Browser): 14 | # 用例每执行一个class前new page,执行结束后close() 15 | page:Page = browser.new_page(locale="zh-CN") 16 | # 设定浏览器地区为中文 17 | # ref: https://playwright.bootcss.com/docs/api/class-browser#browsernewpageoptions 18 | 19 | yield page 20 | 21 | page.close() 22 | 23 | @pytest.fixture(scope="session", autouse=True) 24 | def init_system_env(request): 25 | """ 26 | 自定义初始化系统环境变量 27 | :param request: 28 | :return: 29 | """ 30 | from common.tools import Tools 31 | # 读取命令行入参,写入系统本地环境变量 32 | run_env = request.config.getoption('environment') if request.config.getoption('environment') else 'dev' 33 | Tools.set_env("TEST_ENV", run_env) 34 | 35 | 36 | @pytest.fixture(scope="session") 37 | def env(request): 38 | """ 39 | 配置信息 fixture 对象,可以在 test 方法 & fixture 里使用 40 | :param request: 41 | :return: 42 | """ 43 | import yaml 44 | run_env = request.config.getoption('environment') if request.config.getoption('environment') else 'dev' 45 | # 这里没有用 Tools.get_config() 是因为执行顺序或速度的原因,init_system_env 还没执行,会报错。 46 | config_path = os.path.join(request.config.rootdir, "config", f"{run_env}.yaml") 47 | with open(config_path, encoding="UTF-8") as f: 48 | env_config = yaml.load(f.read(), Loader=yaml.SafeLoader) 49 | yield env_config 50 | 51 | # 骚操作,不建议这么写。用法是 fixtures("search_baidu")['kerwords'] 52 | # @pytest.fixture(scope="class") 53 | # def fixtures(): 54 | # """ 55 | # 测试数据,存放在fixtures目录下,这里是我习惯性引入 Cypress的概念,实际就是测试数据 56 | # :return: 57 | # """ 58 | # def _fixtures(filename: str): 59 | # from common.tools import Tools 60 | # return Tools.get_fixtures(filename) 61 | # 62 | # yield _fixtures 63 | 64 | # ref: https://github.com/microsoft/playwright-pytest 65 | @pytest.fixture(scope="session") 66 | def browser_context_args(browser_context_args): 67 | """ 68 | pytest-playwrigt 内置钩子 69 | :param browser_context_args: 70 | :param tmpdir_factory: 71 | :return: 72 | """ 73 | return { 74 | **browser_context_args, 75 | "record_video_dir": os.path.join(OUTPUT_DIR, "videos") #开始录制时并不知道测试结果正确与否,所以成功失败都会录制 76 | } 77 | 78 | def pytest_addoption(parser): 79 | """ 80 | 接收命令行参数 81 | :param parser: 82 | :return: 83 | """ 84 | parser.addoption("--env", action="store", dest="environment", default="dev", help="environment: dev, dev-01, prod") 85 | 86 | 87 | def pytest_configure(config): 88 | """ 89 | 初始化配置,最先执行 90 | :param config: 91 | :return: 92 | """ 93 | 94 | # 添加接口地址与项目名称 95 | config._metadata["项目名称"] = "xxxx" 96 | config._metadata["项目地址"] = "https://xxxx.xxxx.com/aaaa/api-test" 97 | 98 | 99 | @pytest.mark.hookwrapper 100 | def pytest_runtest_makereport(item): 101 | """ 102 | 用于向测试用例中添加用例的开始时间、内部注释,和失败截图等. 103 | :param item: 104 | """ 105 | 106 | pytest_html = item.config.pluginmanager.getplugin('html') 107 | callers = yield 108 | report = callers.get_result() 109 | report.description = description_html(item.function.__doc__) 110 | report.extra = [] 111 | 112 | if "page" not in item.funcargs: 113 | return "page not in item.funcargs" 114 | page = item.funcargs["page"] 115 | if report.when == 'call': 116 | xfail = hasattr(report, 'wasxfail') 117 | if (report.skipped and xfail) or (report.failed and not xfail): 118 | case_name = report.nodeid.replace("::", "_") + ".png" 119 | image_relative_path = os.path.join("images", case_name.replace('testcase/', '')) 120 | image_absolute_path = os.path.join(OUTPUT_DIR, image_relative_path) 121 | capture_screenshots(image_absolute_path, page) 122 | if image_relative_path: 123 | html = '
screenshot
' % image_relative_path 125 | report.extra.append(pytest_html.extras.html(html)) 126 | 127 | # TODO: 使用 allure 以更好的呈现截图与视频 128 | # ref: https://zenn.dev/yusukeiwaki/articles/cfda648dc170e5 129 | # @pytest.mark.hookwrapper 130 | # def pytest_runtest_makereport(item, call): 131 | # """ 132 | # 用于向测试用例中添加用例的开始时间、内部注释,和失败截图等. 133 | # :param item: 134 | # """ 135 | # if call.when == "call": 136 | # # 失败的情况下 137 | # if call.excinfo is not None and "page" in item.funcargs: 138 | # from playwright.async_api import Page 139 | # page: Page = item.funcargs["page"] 140 | # 141 | # # allure.attach( 142 | # # page.screenshot(type='png'), 143 | # # name=f"{slugify(item.nodeid)}.png", 144 | # # attachment_type=allure.attachment_type.PNG 145 | # # ) 146 | # 147 | # video_path = page.video.path() 148 | # page.context.close() # ensure video saved 149 | # # allure.attach( 150 | # # open(video_path, 'rb').read(), 151 | # # name=f"{slugify(item.nodeid)}.webm", 152 | # # attachment_type=allure.attachment_type.WEBM 153 | # # ) 154 | # 155 | # callers = yield 156 | 157 | @pytest.mark.optionalhook 158 | def pytest_html_results_table_header(cells): 159 | """ 160 | 设置用例描述表头 161 | :param cells: 162 | :return: 163 | """ 164 | cells.insert(2, html.th('Description')) 165 | cells.pop() 166 | 167 | 168 | @pytest.mark.optionalhook 169 | def pytest_html_results_table_row(report, cells): 170 | """ 171 | 设置用例描述表格 172 | :param report: 173 | :param cells: 174 | :return: 175 | """ 176 | 177 | cells.insert(2, html.td(report.description if hasattr(report, "description") else "")) 178 | cells.pop() 179 | 180 | 181 | @pytest.mark.optionalhook 182 | def pytest_html_report_title(report): 183 | """ 184 | pytest-html,自定义报告标题 185 | :param report: 186 | :return: 187 | """ 188 | 189 | # 报告名称 190 | report.title = "PlayWright 测试报告" 191 | 192 | # 重写报告地址 193 | # 开启这个之后,无论--html传入什么地址都只会在根目录生成报告 194 | report.logfile = f'{OUTPUT_DIR}/report.html' 195 | 196 | 197 | @pytest.mark.optionalhook 198 | def pytest_html_results_summary(prefix): 199 | """ 200 | pytest-html,自定义 Summary 部分。也可以用于注入一些报告样式。 201 | :return: 202 | """ 203 | prefix.extend([html.p("所属部门: QA")]) 204 | prefix.extend([html.p("测试人员: xxxx")]) 205 | prefix.extend( 206 | [ 207 | html.style( 208 | """ 209 | /* 自定义样式 */ 210 | 211 | body, #results-table { 212 | font-size: 15px; 213 | } 214 | 215 | /* */ 216 | """ 217 | ) 218 | ] 219 | ) 220 | 221 | 222 | def description_html(desc): 223 | """ 224 | 将用例中的描述转成HTML对象 225 | :param desc: 描述 226 | :return: 227 | """ 228 | if desc is None: 229 | return "No case description" 230 | desc_ = "" 231 | for i in range(len(desc)): 232 | if i == 0: 233 | pass 234 | elif desc[i] == '\n': 235 | desc_ = desc_ + ";" 236 | else: 237 | desc_ = desc_ + desc[i] 238 | 239 | desc_lines = desc_.split(";") 240 | desc_html = html.html( 241 | html.head( 242 | html.meta(name="Content-Type", value="text/html; charset=latin1")), 243 | html.body( 244 | [html.p(line) for line in desc_lines])) 245 | return desc_html 246 | 247 | 248 | def capture_screenshots(image_path, page): 249 | """ 250 | 配置用例失败截图路径 251 | :param case_name: 用例名 252 | :return: 253 | """ 254 | 255 | image_dir = os.path.dirname(image_path) 256 | if not os.path.exists(image_dir): 257 | os.makedirs(image_dir) 258 | try: 259 | page.screenshot(path=image_path) 260 | except Exception as e: 261 | print(f'截图失败 {e}') 262 | 263 | 264 | if __name__ == "__main__": 265 | pass 266 | -------------------------------------------------------------------------------- /fixtures/dev/baidu_search.yaml: -------------------------------------------------------------------------------- 1 | # 测试数据 2 | kerwords: "playwright" -------------------------------------------------------------------------------- /page/baidu/baidu_search2_page.py: -------------------------------------------------------------------------------- 1 | from common.tools import Tools 2 | from playwright.async_api import Page 3 | 4 | # 利用 global 简化 page使用 5 | page: Page 6 | 7 | class BaiduSearchPage: 8 | 9 | def __init__(self, page_: Page): 10 | global page 11 | page = page_ 12 | 13 | self.page = page 14 | self.host = Tools.get_config("base_url") 15 | self.path = "/" 16 | self.url = self.host + self.path 17 | self.search_input = "#kw" # 搜索框 18 | self.search_button = "#su" # 搜索按钮 19 | self.settings = "#s-usersetting-top" # 设置 20 | self.search_setting = "#s-user-setting-menu > div > a.setpref" # 搜索设置 21 | self.save_setting = 'text="保存设置"' # 保存设置 22 | 23 | def open(self): 24 | page.goto(self.url) 25 | 26 | def search_keywords(self, keywords: str): 27 | page.type(self.search_input, text=keywords) 28 | page.click(self.search_button) 29 | -------------------------------------------------------------------------------- /page/baidu/baidu_search_page.py: -------------------------------------------------------------------------------- 1 | from common.tools import Tools 2 | from playwright.async_api import Page 3 | 4 | class BaiduSearchPage: 5 | 6 | def __init__(self, page: Page): 7 | self.page = page 8 | 9 | self.host = Tools.get_config("base_url") 10 | self.path = "/" 11 | self.url = self.host + self.path 12 | self.search_input = "#kw" # 搜索框 13 | self.search_button = "#su" # 搜索按钮 14 | self.settings = "#s-usersetting-top" # 设置 15 | self.search_setting = "#s-user-setting-menu > div > a.setpref" # 搜索设置 16 | self.save_setting = 'text="保存设置"' # 保存设置 17 | 18 | def open(self): 19 | self.page.goto(self.url) 20 | 21 | def search_keywords(self, keywords: str): 22 | self.page.type(self.search_input, text=keywords) 23 | self.page.click(self.search_button) 24 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Atomic file writes." 4 | marker = "sys_platform == \"win32\"" 5 | name = "atomicwrites" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | version = "1.4.0" 9 | 10 | [[package]] 11 | category = "main" 12 | description = "Classes Without Boilerplate" 13 | name = "attrs" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "20.3.0" 17 | 18 | [package.extras] 19 | dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 20 | docs = ["furo", "sphinx", "zope.interface"] 21 | tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 22 | tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 23 | 24 | [[package]] 25 | category = "main" 26 | description = "Python package for providing Mozilla's CA Bundle." 27 | name = "certifi" 28 | optional = false 29 | python-versions = "*" 30 | version = "2020.12.5" 31 | 32 | [[package]] 33 | category = "main" 34 | description = "Universal encoding detector for Python 2 and 3" 35 | name = "chardet" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 38 | version = "4.0.0" 39 | 40 | [[package]] 41 | category = "main" 42 | description = "Cross-platform colored terminal text." 43 | marker = "sys_platform == \"win32\"" 44 | name = "colorama" 45 | optional = false 46 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 47 | version = "0.4.4" 48 | 49 | [[package]] 50 | category = "main" 51 | description = "Lightweight in-process concurrent programming" 52 | name = "greenlet" 53 | optional = false 54 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 55 | version = "1.0.0" 56 | 57 | [package.extras] 58 | docs = ["sphinx"] 59 | 60 | [[package]] 61 | category = "main" 62 | description = "Internationalized Domain Names in Applications (IDNA)" 63 | name = "idna" 64 | optional = false 65 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 66 | version = "2.10" 67 | 68 | [[package]] 69 | category = "main" 70 | description = "iniconfig: brain-dead simple config-ini parsing" 71 | name = "iniconfig" 72 | optional = false 73 | python-versions = "*" 74 | version = "1.1.1" 75 | 76 | [[package]] 77 | category = "main" 78 | description = "Core utilities for Python packages" 79 | name = "packaging" 80 | optional = false 81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 82 | version = "20.9" 83 | 84 | [package.dependencies] 85 | pyparsing = ">=2.0.2" 86 | 87 | [[package]] 88 | category = "main" 89 | description = "A high-level API to automate web browsers" 90 | name = "playwright" 91 | optional = false 92 | python-versions = ">=3.7" 93 | version = "1.10.0" 94 | 95 | [package.dependencies] 96 | greenlet = "1.0.0" 97 | pyee = ">=8.0.1" 98 | 99 | [package.dependencies.typing-extensions] 100 | python = "<3.9" 101 | version = "*" 102 | 103 | [[package]] 104 | category = "main" 105 | description = "plugin and hook calling mechanisms for python" 106 | name = "pluggy" 107 | optional = false 108 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 109 | version = "0.13.1" 110 | 111 | [package.extras] 112 | dev = ["pre-commit", "tox"] 113 | 114 | [[package]] 115 | category = "main" 116 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 117 | name = "py" 118 | optional = false 119 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 120 | version = "1.10.0" 121 | 122 | [[package]] 123 | category = "main" 124 | description = "A port of node.js's EventEmitter to python." 125 | name = "pyee" 126 | optional = false 127 | python-versions = "*" 128 | version = "8.1.0" 129 | 130 | [[package]] 131 | category = "main" 132 | description = "Python parsing module" 133 | name = "pyparsing" 134 | optional = false 135 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 136 | version = "2.4.7" 137 | 138 | [[package]] 139 | category = "main" 140 | description = "pytest: simple powerful testing with Python" 141 | name = "pytest" 142 | optional = false 143 | python-versions = ">=3.6" 144 | version = "6.2.3" 145 | 146 | [package.dependencies] 147 | atomicwrites = ">=1.0" 148 | attrs = ">=19.2.0" 149 | colorama = "*" 150 | iniconfig = "*" 151 | packaging = "*" 152 | pluggy = ">=0.12,<1.0.0a1" 153 | py = ">=1.8.2" 154 | toml = "*" 155 | 156 | [package.extras] 157 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 158 | 159 | [[package]] 160 | category = "main" 161 | description = "pytest plugin for URL based testing" 162 | name = "pytest-base-url" 163 | optional = false 164 | python-versions = "*" 165 | version = "1.4.2" 166 | 167 | [package.dependencies] 168 | pytest = ">=2.7.3" 169 | requests = ">=2.9" 170 | 171 | [[package]] 172 | category = "main" 173 | description = "pytest plugin for generating HTML reports" 174 | name = "pytest-html" 175 | optional = false 176 | python-versions = ">=3.6" 177 | version = "3.1.1" 178 | 179 | [package.dependencies] 180 | pytest = ">=5.0,<6.0.0 || >6.0.0" 181 | pytest-metadata = "*" 182 | 183 | [[package]] 184 | category = "main" 185 | description = "pytest plugin for test session metadata" 186 | name = "pytest-metadata" 187 | optional = false 188 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 189 | version = "1.11.0" 190 | 191 | [package.dependencies] 192 | pytest = ">=2.9.0" 193 | 194 | [[package]] 195 | category = "main" 196 | description = "A pytest wrapper with fixtures for Playwright to automate web browsers" 197 | name = "pytest-playwright" 198 | optional = false 199 | python-versions = ">=3.7" 200 | version = "0.1.0" 201 | 202 | [package.dependencies] 203 | playwright = ">=1.10.0" 204 | pytest = "*" 205 | pytest-base-url = "*" 206 | 207 | [[package]] 208 | category = "main" 209 | description = "pytest plugin to re-run tests to eliminate flaky failures" 210 | name = "pytest-rerunfailures" 211 | optional = false 212 | python-versions = ">=3.5" 213 | version = "9.1.1" 214 | 215 | [package.dependencies] 216 | pytest = ">=5.0" 217 | setuptools = ">=40.0" 218 | 219 | [[package]] 220 | category = "main" 221 | description = "YAML parser and emitter for Python" 222 | name = "pyyaml" 223 | optional = false 224 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 225 | version = "5.4.1" 226 | 227 | [[package]] 228 | category = "main" 229 | description = "Python HTTP for Humans." 230 | name = "requests" 231 | optional = false 232 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 233 | version = "2.25.1" 234 | 235 | [package.dependencies] 236 | certifi = ">=2017.4.17" 237 | chardet = ">=3.0.2,<5" 238 | idna = ">=2.5,<3" 239 | urllib3 = ">=1.21.1,<1.27" 240 | 241 | [package.extras] 242 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 243 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 244 | 245 | [[package]] 246 | category = "main" 247 | description = "Python Library for Tom's Obvious, Minimal Language" 248 | name = "toml" 249 | optional = false 250 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 251 | version = "0.10.2" 252 | 253 | [[package]] 254 | category = "main" 255 | description = "Backported and Experimental Type Hints for Python 3.5+" 256 | marker = "python_version <= \"3.8\"" 257 | name = "typing-extensions" 258 | optional = false 259 | python-versions = "*" 260 | version = "3.7.4.3" 261 | 262 | [[package]] 263 | category = "main" 264 | description = "HTTP library with thread-safe connection pooling, file post, and more." 265 | name = "urllib3" 266 | optional = false 267 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 268 | version = "1.26.4" 269 | 270 | [package.extras] 271 | brotli = ["brotlipy (>=0.6.0)"] 272 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 273 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 274 | 275 | [metadata] 276 | content-hash = "7556c92e719939eb6f39536d8794c654eb6ae441f2656b02f78285f404e28569" 277 | python-versions = "^3.8" 278 | 279 | [metadata.files] 280 | atomicwrites = [ 281 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 282 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 283 | ] 284 | attrs = [ 285 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 286 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 287 | ] 288 | certifi = [ 289 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 290 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 291 | ] 292 | chardet = [ 293 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 294 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 295 | ] 296 | colorama = [ 297 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 298 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 299 | ] 300 | greenlet = [ 301 | {file = "greenlet-1.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2"}, 302 | {file = "greenlet-1.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c"}, 303 | {file = "greenlet-1.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203"}, 304 | {file = "greenlet-1.0.0-cp27-cp27m-win32.whl", hash = "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476"}, 305 | {file = "greenlet-1.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b"}, 306 | {file = "greenlet-1.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379"}, 307 | {file = "greenlet-1.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce"}, 308 | {file = "greenlet-1.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c"}, 309 | {file = "greenlet-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5"}, 310 | {file = "greenlet-1.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f"}, 311 | {file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6"}, 312 | {file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683"}, 313 | {file = "greenlet-1.0.0-cp35-cp35m-win32.whl", hash = "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70"}, 314 | {file = "greenlet-1.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770"}, 315 | {file = "greenlet-1.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee"}, 316 | {file = "greenlet-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd"}, 317 | {file = "greenlet-1.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a"}, 318 | {file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d"}, 319 | {file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2"}, 320 | {file = "greenlet-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7"}, 321 | {file = "greenlet-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be"}, 322 | {file = "greenlet-1.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef"}, 323 | {file = "greenlet-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128"}, 324 | {file = "greenlet-1.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7"}, 325 | {file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c"}, 326 | {file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f"}, 327 | {file = "greenlet-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c"}, 328 | {file = "greenlet-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85"}, 329 | {file = "greenlet-1.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7"}, 330 | {file = "greenlet-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06"}, 331 | {file = "greenlet-1.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36"}, 332 | {file = "greenlet-1.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"}, 333 | {file = "greenlet-1.0.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196"}, 334 | {file = "greenlet-1.0.0-cp38-cp38-win32.whl", hash = "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e"}, 335 | {file = "greenlet-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c"}, 336 | {file = "greenlet-1.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243"}, 337 | {file = "greenlet-1.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df"}, 338 | {file = "greenlet-1.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218"}, 339 | {file = "greenlet-1.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7"}, 340 | {file = "greenlet-1.0.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664"}, 341 | {file = "greenlet-1.0.0-cp39-cp39-win32.whl", hash = "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139"}, 342 | {file = "greenlet-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0"}, 343 | {file = "greenlet-1.0.0.tar.gz", hash = "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8"}, 344 | ] 345 | idna = [ 346 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 347 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 348 | ] 349 | iniconfig = [ 350 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 351 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 352 | ] 353 | packaging = [ 354 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 355 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 356 | ] 357 | playwright = [ 358 | {file = "playwright-1.10.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:554763ac930eb1a9173cfb2f158cda9f008619a82556f4da259171b96f8d3414"}, 359 | {file = "playwright-1.10.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:c2ec61af91b87294c2dd79313b2fc32b5b810e886d4450bac8e4f116e17fb75e"}, 360 | {file = "playwright-1.10.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:934e9c6ae6b2e8f8f71cbe30d3a702d16afd129fab30418351ce72c31b0c4ad0"}, 361 | {file = "playwright-1.10.0-py3-none-win32.whl", hash = "sha256:67c28b654c1cc750f1831bdc20cc8036cd2a3420410e490bfd4015adff838ed8"}, 362 | {file = "playwright-1.10.0-py3-none-win_amd64.whl", hash = "sha256:3a2ae1173eb0844de995f8305106d1195f9cf86f7292bbeffd8feec8810a94dc"}, 363 | ] 364 | pluggy = [ 365 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 366 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 367 | ] 368 | py = [ 369 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 370 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 371 | ] 372 | pyee = [ 373 | {file = "pyee-8.1.0-py2.py3-none-any.whl", hash = "sha256:383973b63ad7ed5e3c0311f8b179c52981f9e7b3eaea0e9a830d13ec34dde65f"}, 374 | {file = "pyee-8.1.0.tar.gz", hash = "sha256:92dacc5bd2bdb8f95aa8dd2585d47ca1c4840e2adb95ccf90034d64f725bfd31"}, 375 | ] 376 | pyparsing = [ 377 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 378 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 379 | ] 380 | pytest = [ 381 | {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, 382 | {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, 383 | ] 384 | pytest-base-url = [ 385 | {file = "pytest-base-url-1.4.2.tar.gz", hash = "sha256:7f1f32e08c2ee751e59e7f5880235b46e83496adc5cba5a01ca218c6fe81333d"}, 386 | {file = "pytest_base_url-1.4.2-py2.py3-none-any.whl", hash = "sha256:8b6523a1a3af73c317bdae97b722dfb55a7336733d1ad411eb4a4931347ba77a"}, 387 | ] 388 | pytest-html = [ 389 | {file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"}, 390 | {file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"}, 391 | ] 392 | pytest-metadata = [ 393 | {file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"}, 394 | {file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"}, 395 | ] 396 | pytest-playwright = [ 397 | {file = "pytest-playwright-0.1.0.tar.gz", hash = "sha256:2ecf06645a355f86220e0082a09c65301f1144921983cf455dbb660d68cb6258"}, 398 | {file = "pytest_playwright-0.1.0-py3-none-any.whl", hash = "sha256:81181d00ea72b4fe39469848fb6f2b0c204b75342fdb70fa1f9ce3f7968ed6e4"}, 399 | ] 400 | pytest-rerunfailures = [ 401 | {file = "pytest-rerunfailures-9.1.1.tar.gz", hash = "sha256:1cb11a17fc121b3918414eb5eaf314ee325f2e693ac7cb3f6abf7560790827f2"}, 402 | {file = "pytest_rerunfailures-9.1.1-py3-none-any.whl", hash = "sha256:2eb7d0ad651761fbe80e064b0fd415cf6730cdbc53c16a145fd84b66143e609f"}, 403 | ] 404 | pyyaml = [ 405 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 406 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 407 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 408 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 409 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 410 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 411 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 412 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 413 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 414 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 415 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 416 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 417 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 418 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 419 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 420 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 421 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 422 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 423 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 424 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 425 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 426 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 427 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 428 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 429 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 430 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 431 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 432 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 433 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 434 | ] 435 | requests = [ 436 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 437 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 438 | ] 439 | toml = [ 440 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 441 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 442 | ] 443 | typing-extensions = [ 444 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 445 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 446 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 447 | ] 448 | urllib3 = [ 449 | {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, 450 | {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, 451 | ] 452 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "playwright-test" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["tomoyachen "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | playwright = "^1.10.0" 10 | pytest-playwright = "^0.1.0" 11 | pytest-html = "^3.1.1" 12 | pytest-rerunfailures = "^9.1.1" 13 | pyyaml = "^5.4.1" 14 | 15 | [tool.poetry.dev-dependencies] 16 | 17 | [build-system] 18 | requires = ["poetry>=0.12"] 19 | build-backend = "poetry.masonry.api" 20 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # 默认命令行参数(暂时屏蔽) 3 | addopts = -s -v --env dev --headed --reruns 0 --reruns-delay 1 --html=./output/report.html --self-contained-html 4 | 5 | # 测试路径 6 | testpaths = ./testcase 7 | 8 | # 搜索文件名 9 | python_files = *_test.py 10 | 11 | # 搜索测试类名 12 | python_classes = *Test 13 | 14 | # 搜索测试方法名 15 | python_functions = test_* 16 | 17 | # 指定缓存地址 18 | cache_dir = .pytest_cache 19 | 20 | # 默认不展开 21 | render_collapsed = True 22 | -------------------------------------------------------------------------------- /testcase/baidu/baidu_search2_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from playwright.async_api import Page, Browser 3 | from page.baidu.baidu_search_page import BaiduSearchPage 4 | import time 5 | from common.tools import Tools 6 | 7 | # 利用 global 简化 page 使用 8 | baiduSearchPage: BaiduSearchPage 9 | 10 | class BaiduSearchTest: 11 | 12 | @pytest.fixture(scope="class", autouse=True) 13 | def before(self, page: Page): 14 | global baiduSearchPage 15 | baiduSearchPage = BaiduSearchPage(page) 16 | 17 | 18 | @pytest.fixture() 19 | def fixtures(self): 20 | yield Tools.get_fixtures("baidu_search") 21 | 22 | @staticmethod 23 | def test_baidu_search(page: Page, env: dict, fixtures): 24 | 25 | baiduSearchPage.open() 26 | baiduSearchPage.search_keywords(fixtures['kerwords']) 27 | time.sleep(2) 28 | assert baiduSearchPage.page.title() == f'{fixtures["kerwords"]}_百度搜索' 29 | 30 | if __name__ == '__main__': 31 | pytest.main(["-v", "-s"]) 32 | -------------------------------------------------------------------------------- /testcase/baidu/baidu_search_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from playwright.async_api import Page, Dialog 3 | from page.baidu.baidu_search_page import BaiduSearchPage 4 | import time 5 | from common.tools import Tools 6 | 7 | # 我希望每个测试方法都启动一次浏览器 8 | # 因为 pytest-playwright 内置的 page fixture作用域是function 9 | # 所有使用内置的 page fixture 时,每次测试方法执行前生成、执行后销毁,即每个测试方法开启一次浏览器 10 | 11 | class BaiduSearchTest: 12 | 13 | # 测试数据都存放在 fixtures 14 | @pytest.fixture() 15 | def fixtures(self): 16 | yield Tools.get_fixtures("baidu_search") 17 | 18 | 19 | @staticmethod 20 | def test_baidu_search(page: Page, env: dict, fixtures): 21 | """ 22 | 名称:百度搜索"playwright" 23 | 步骤: 24 | 1、打开浏览器 25 | 2、输入"playwright"关键字 26 | 3、点击搜索按钮 27 | 检查点: 28 | * 检查页面标题是否相等。 29 | """ 30 | 31 | baiduSearchPage = BaiduSearchPage(page) 32 | baiduSearchPage.open() 33 | baiduSearchPage.search_keywords(fixtures['kerwords']) 34 | time.sleep(2) 35 | assert baiduSearchPage.page.title() == f'{fixtures["kerwords"]}_百度搜索' 36 | assert 1 == 2 37 | 38 | @staticmethod 39 | def test_baidu_search_setting(page: Page): 40 | """ 41 | 名称:百度搜索设置 42 | 步骤: 43 | 1、打开百度浏览器 44 | 2、点击设置链接 45 | 3、在下拉框中"选择搜索" 46 | 4、点击"保存设置" 47 | 5、对弹出警告框保存 48 | 检查点: 49 | * 检查是否弹出提示框 50 | """ 51 | baiduSearchPage = BaiduSearchPage(page) 52 | baiduSearchPage.page.goto(baiduSearchPage.url) 53 | baiduSearchPage.page.click(baiduSearchPage.settings) 54 | baiduSearchPage.page.click(baiduSearchPage.search_setting) 55 | baiduSearchPage.page.click(baiduSearchPage.save_setting) 56 | 57 | def on_dialog(dialog: Dialog): 58 | assert dialog.type == "alert" 59 | assert dialog.message == "已经记录下您的使用偏好" 60 | dialog.accept() 61 | 62 | page.on("dialog", on_dialog) 63 | 64 | 65 | # Network events 66 | # ref: https://playwright.dev/python/docs/network#network-events 67 | @staticmethod 68 | def test_baidu_search_with_network(page: Page, env: dict, fixtures): 69 | """ 70 | 获取请求/ 响应 71 | """ 72 | import re 73 | 74 | page.on("request", 75 | lambda request: print(">>", request.method, request.url) if not re.findall('.*/.[js|css|png]', 76 | request.url) else None) 77 | page.on("response", 78 | lambda response: print("<<", response.status, response.url) if not re.findall('.*/.[js|css|png]', 79 | response.url) else None) 80 | 81 | baiduSearchPage = BaiduSearchPage(page) 82 | baiduSearchPage.open() 83 | 84 | 85 | if __name__ == '__main__': 86 | pytest.main(["-v", "-s"]) 87 | -------------------------------------------------------------------------------- /testcase/baidu/baidu_search_with_multi-browser_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from playwright.async_api import Page, Browser, BrowserContext 3 | from page.baidu.baidu_search_page import BaiduSearchPage 4 | import time 5 | from common.tools import Tools 6 | 7 | 8 | # 利用 global 简化 page 使用 9 | baiduSearchPage: BaiduSearchPage 10 | 11 | class BaiduSearchTest: 12 | 13 | @pytest.fixture(scope="class", autouse=True) 14 | def before(self, page: Page): 15 | global baiduSearchPage 16 | baiduSearchPage = BaiduSearchPage(page) 17 | 18 | 19 | @pytest.fixture() 20 | def fixtures(self): 21 | yield Tools.get_fixtures("baidu_search") 22 | 23 | 24 | @staticmethod 25 | def test_baidu_search_with_multi_browser(page: Page, env: dict, fixtures, browser:Browser): 26 | 27 | baiduSearchPage.open() 28 | baiduSearchPage.search_keywords(fixtures['kerwords']) 29 | time.sleep(2) 30 | assert baiduSearchPage.page.title() == f'{fixtures["kerwords"]}_百度搜索' 31 | 32 | # 新开浏览器做另一件事 33 | # browser.new_page() 方式打开的浏览器是 sessions 不互通的 34 | page2 = browser.new_page(locale="zh-CN") 35 | baiduSearchPage2 = BaiduSearchPage(page2) 36 | baiduSearchPage2.open() 37 | baiduSearchPage2.search_keywords('我是新浏览器') 38 | time.sleep(2) 39 | page2.close() #由于是手动打开的,所以要自己关闭一下 40 | 41 | @staticmethod 42 | def test_baidu_search_with_multi_tab(page: Page, env: dict, fixtures, browser:Browser): 43 | 44 | # 新开浏览器打开2个tab 45 | # context.new_page() 方式打开的浏览器是 sessions 互通的,类似于打开多个选项卡 46 | page3:BrowserContext = browser.new_context(locale="zh-CN") 47 | page3_tab1:Page = page3.new_page() 48 | page3_tab2:Page = page3.new_page() 49 | 50 | page3_tab1.goto("https://www.baidu.com/s?wd=我是窗口 1") 51 | page3_tab2.goto("https://www.baidu.com/s?wd=我是窗口 2") 52 | time.sleep(2) 53 | page3.close() 54 | 55 | 56 | if __name__ == '__main__': 57 | pytest.main(["-v", "-s"]) 58 | -------------------------------------------------------------------------------- /testcase/shopxo/admin_login_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from playwright.async_api import Page 3 | 4 | class AdminLoginTest: 5 | 6 | @pytest.fixture(scope="class", autouse=True) 7 | def before(self, page: Page): 8 | page.goto("http://localhost/shopxo/admin.php") 9 | 10 | @staticmethod 11 | def test_admin_login(page: Page): 12 | """ 13 | 登录后台 14 | """ 15 | # Click [placeholder="请输入用户名"] 16 | page.click("[placeholder=\"请输入用户名\"]") 17 | # Fill [placeholder="请输入用户名"] 18 | page.fill("[placeholder=\"请输入用户名\"]", "admin") 19 | # Fill [placeholder="请输入登录密码"] 20 | page.fill("[placeholder=\"请输入登录密码\"]", "shopxo") 21 | # Click button:has-text("登录") 22 | page.click("button:has-text(\"登录\")") 23 | # Go to http://localhost/shopxo/admin.php?s=/index/index.html 24 | page.goto("http://localhost/shopxo/admin.php?s=/index/index.html") 25 | 26 | assert page.url == 'http://localhost/shopxo/admin.php?s=/index/index.html' 27 | 28 | @staticmethod 29 | def test_admin_click_menu(page: Page): 30 | """ 31 | 登录后台 32 | """ 33 | 34 | # Click text=系统设置 35 | page.click("text=系统设置") 36 | # Click text=站点配置 37 | page.click("text=站点配置") 38 | # Click text=权限控制 39 | page.click("text=权限控制") 40 | # Click text=用户管理 41 | page.click("text=用户管理") 42 | # Click text=商品管理 43 | page.click("text=商品管理") 44 | 45 | assert 1 == 1 46 | 47 | if __name__ == '__main__': 48 | pytest.main(["-v", "-s"]) 49 | 50 | 51 | 52 | --------------------------------------------------------------------------------