├── .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 = '

' % 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 |
--------------------------------------------------------------------------------