Playwright Python 自动化测试:零基础半个月

背景:前端开发,公司项目要写ui自动化,没测试经验,过了一遍Python语法,然后开始了自动化测试的学习,这里记录一下使用中用的到的知识点

引言

在较长的产品周期中,自动化测试是确保应用质量和性能的关键步骤。这里记录了一个前端开发者从零开始,学习Python和Playwright来构建自动化测试的过程。Playwright 是一个强大的自动化测试工具,支持多种浏览器,本文将从基础安装到脚本编写的每一个步骤进行详细介绍,重点是示例。

建议:等你遇到问题再来查,或许会帮助你解决

测试大佬勿喷,欢迎指正

playWright 自带的元素获取工具

效果展示:

20240423-175716.gif

工具源文档

playwright codegen 是 Playwright 测试框架的一个功能,它可以自动记录您在浏览器中的操作并生成相应的测试代码。这是一个非常有用的工具,尤其是对于快速开始编写测试或者为现有的用户操作创建自动化测试脚本。

使用 playwright codegen 的步骤如下:

安装 Playwright:如果您还没有安装 Playwright,可以通过下面命令安装

1
复制代码npm install playwright

运行 Codegen:在命令行中输入 npx playwright codegen 后跟您想要测试的网站的 URL。例如:

1
arduino复制代码npx playwright codegen https://www.baidu.com

如果您不提供 URL,codegen 会打开一个空白的浏览器窗口,您可以在其中输入您想要测试的网站的 URL。

记录操作: 在打开的浏览器窗口中执行您想要测试的操作。Playwright 会监视您的操作并生成相应的测试代码。

生成断言: 您可以通过点击 Playwright Inspector 窗口中的图标来生成断言,然后点击页面上的元素来对其进行断言。

停止记录: 完成操作后,点击“停止记录”按钮。然后,您可以使用“复制”按钮将生成的代码复制到您的编辑器中。

编辑和保存测试: 您可以在编辑器中进一步编辑和完善生成的测试代码,然后将其保存为测试文件。 playwright codegen 是快速生成测试代码的强大工具,它可以节省时间并确保测试准确录制了用户的实际操作。

元素获取示例

语法page.locator()

page.locator(".menu-delete-modal") 是 Playwright 测试库中的一个语法,用于定位页面上的元素,并且可以链式调用多种操作。这里,.menu-delete-modal 是一个CSS类,它会选择页面上所有具有 menu-delete-modal 类的元素。page.locator 方法返回一个 Locator 对象,代表了页面上的一个或多个DOM元素,您可以对这些元素执行各种操作,比如点击、填写表单等。

以下是一些 Python 示例,展示了获取元素的常用方法page.locator()

  1. 定位元素:使用 CSS 选择器或 XPath 来定位页面上的元素。
1
bash复制代码page.locator('.example').click()  # 点击类名为example的元素
  1. 文本内容定位:通过元素的文本内容来定位。
1
bash复制代码page.locator('text="登录"').click()  # 点击文本为“登录”的元素
  1. 属性定位:通过元素的属性来定位。
1
bash复制代码page.locator('[name="email"]').fill('example@example.com')  # 填写name属性为email的输入框
  1. 等待元素:等待元素出现在页面上。
1
ini复制代码page.locator('.loading').wait_for(state="hidden")  # 等待加载元素消失,默认等待元素出现
  1. 获取元素属性:获取元素的属性值。
1
ini复制代码href = page.locator('.link').get_attribute('href')  # 获取链接的href属性
  1. 处理多个元素:对页面上的一组元素进行操作。
1
2
3
ini复制代码items = page.locator('.list-item').element_handles()
for item in items:
   item.click()  # 点击每个列表项
  1. 元素的父级元素: 拿到父级,获取父级下其他数据是。
1
ini复制代码parent_element = page.locator('text="登录"').locator('xpath=..')

示例-> 获取类名 menu-delete-modal 弹框下一个按钮

1
arduino复制代码page.locator(".menu-delete-modal").get_by_role("button", name="确定")

示例-> 使用locator选择器来点击一个确定按钮

1
scss复制代码page.locator(".menu-delete-modal").click()

注意:在严格模式,返回的是一个或多个元素,存在差异。如果是一个元素,可以直接执行Locator对象上的元素操作方法,一些静态方法,如:click(), inner_html()…;多个元素的情况不能执行click(),inner_html()…;通过count() 可以查看有多少元素。如图

一个元素:

image-20240409103518052.png

多个元素:

image-20240409103622830.png

想要点击其中一个元素可以使用.nth(index),其中 index 是从 0 开始的索引:

1
2
3
scss复制代码page.locator(".el-button").nth(0).click()  # 点击第一个按钮
page.locator(".el-button").nth(1).click() # 点击第二个按钮
# 以此类推...

如果需要对所有匹配的元素执行操作,可以遍历它们:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码# 方式1
buttons = page.locator(".el-button")
count = buttons.count()
for i in range(count):
buttons.nth(i).click()

# 方式2
button_handles = page.locator(".el-button").element_handles()
# 遍历并点击每个按钮
for button_handle in button_handles:
button_handle.click()
  1. page.get_by_role()

get_by_role 是 Playwright 测试库中的一个功能,它允许您根据元素的 ARIA 角色 来定位页面上的元素。当您使用 get_by_role 时,通常也会传递一个可访问名称,以便精确地定位到特定元素。

例如,如果您想定位并且点击页面上名为“登录”的按钮,可以使用以下代码:

1
arduino复制代码page.locator(".menu-delete-modal").get_by_role("button", name="确定").click()

这里按钮在浏览器的元素界面是button下包裹 span,确定文字在span内,如下:

image-20240409102104163.png

name的值采用的是模糊匹配,name为:确,也可以正常执行

对前端开发者来说,直接使用 HTML 元素和类名选择器更直观和方便。ARIA 角色主要是为了提高网页的无障碍性,帮助使用辅助技术的用户更好地理解和导航网页内容。

示例-> get_by_text 精准获取一个元素

界面:
image-20240411173707556.png

代码如下:

1
arduino复制代码page.locator('.menu-config-dropdown-menu').get_by_text('员工')

执行发现会获取两个元素

{Error}strict mode violation: locator(“.menu-config-dropdown-menu”).get_by_text(“员工”) resolved to 2 elements:


  1. aka get_by_text(“员工”, exact=True)


  2. aka get_by_text(“全部员工”)

想要精准匹配,如下哦

1
ini复制代码page.locator('.menu-config-dropdown-menu').get_by_text('员工', exact=True)

示例-> 获取元素的父级元素

期望的效果是通过标题名称获取到包装元素,最终调用如下:

1
scss复制代码menu_form_design_page.get_component_by_comp_name('单行输入333').click()

场景如下:

image-20240419145205549.png

函数如下:

1
2
3
4
5
6
7
8
9
10
python复制代码def get_component_by_comp_name(self, comp_name: str) -> ComponentWrapper:
"""
# 通过组件名获取组件,返回一个组件
"""
component = self.form_item_list.get_by_text(f'{comp_name}', exact=True).first # 通过名称匹配到标题
if component.count() != 1:
raise Exception(f'未找到组件{comp_name}')
while component.get_attribute('data-ui-test-component-type') is None: # 循环向上找目标父元素
component = component.locator('xpath=..')
return ComponentWrapper(self.page, component) # 自定的一些目标元素包装类,用于封装一些常用属性和方法

ComponentWrapper类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python复制代码from playwright.async_api import Locator, Page

class ComponentWrapper:
def __init__(self, page: Page, component_locator: Locator):
self.page = component_locator.page
self.comp_locator = component_locator
self.comp_type = component_locator.get_attribute("data-ui-test-component-type")
self.comp_name = component_locator.get_attribute("data-ui-test-component-label")
self.copy_btn = component_locator.locator(".svg-x-lib-copy-add").first
self.delete_btn = component_locator.locator(".svg-delete-icon").first
self.delete_dialog = page.locator(".delete-component-modal").first
self.cancel_btn = self.delete_dialog.get_by_role("button", name="取消")
self.confirm_btn = self.delete_dialog.get_by_role("button", name="确认")
self.comp_locator.click = self.component_wrapper_click # 重写click方法

# 删除组件
def delete_comp(self):
self.comp_locator.click()
self.delete_btn.click()
if self.delete_dialog.is_visible():
self.confirm_btn.click()

# 点击方法
def component_wrapper_click(self):
self.comp_locator.evaluate("element => {element.click()}") # !!可以执行JavaScript代码,前端觉得很合理🦫

元素操作

示例-> 拖拽元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ini复制代码# 向指定子表组件中添加指定类型组件,传入指定子表名称和组件类型列表
def add_component_to_specified_son_table(self, son_table_name: str, component_enum_list: list[OriginComp], callback_fn: callable = None):
# 根据名称获目标表格组件
son_table = self.get_component_by_comp_name(son_table_name)
# 悬停在目标配置区域,滚动鼠标到子表组件的位置,之前发现的一个问题,如果子表格组件不在可视区域内,会导致无法拖拽某些组件会有问题
self.form_item_list.hover()
self.page.mouse.wheel(
delta_y=son_table.comp_locator.evaluate("(el) => el.getBoundingClientRect()")['y'],
delta_x=0
)
# 断言子表格组件的数量为1,如果不为1,说明定位到的不是唯一的子表格组件
assert son_table.comp_locator.count() == 1
# 获取原始组件列表,被拖拽内容
component_type_list = Components.comp_enum_list_to_type_list(component_enum_list)
component_list = self.get_origin_components(component_type_list)
for component in component_list:
component.hover()
self.page.wait_for_timeout(500)
self.page.mouse.down()
# 获取子表格组件的矩形区域
form_panel_form_widget_son_table_rect = son_table.comp_locator.locator(
'> .form-widget-son-table .grid-draggable > .drag-area'
).evaluate("(el) => el.getBoundingClientRect()")
# 计算矩形区域的x和y坐标,相对于页面左上角
form_panel_form_widget_son_table_rect_x = form_panel_form_widget_son_table_rect['x'] + 50
form_panel_form_widget_son_table_rect_y = form_panel_form_widget_son_table_rect['y'] + 80

# 移动鼠标到计算出的y坐标位置,分两步也是因为有些组件从左上角开始拖拽会有问题,所以先向下,再向右
self.page.mouse.move(
x=0,
y=form_panel_form_widget_son_table_rect_y,
steps=10
)
self.page.mouse.move(
x=form_panel_form_widget_son_table_rect_x,
y=form_panel_form_widget_son_table_rect_y,
steps=10
)
self.page.wait_for_timeout(500)
self.page.mouse.up()
# 完成一次拖拽操作,之后执行回调函数。我在Python里面写JavaScript代码,前端觉得很合理🦫
if callback_fn:
callback_fn()

示例-> Python playWright 执行浏览器JavaScript脚本代码

  1. 点击元素
1
2
3
4
5
python复制代码def click_element(self, element: Locator):
"""
# 点击元素
"""
element.evaluate("element => {element.click()}")
  1. 有的时候playWright的click()方法会有问题,点击不到目标元素,可以使用JavaScript代码来执行点击操作

下面是一个组件的封装类,里面覆盖了点击元素的方法,使用了evaluate方法执行JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码from playwright.async_api import Locator, Page
class ElementManager:
def __init__(self, page: Page, element_locator: Locator):
self.page = element_locator.page
self.elem_locator = element_locator
self.elem_type = element_locator.get_attribute("data-test-elem-type")
self.elem_label = element_locator.get_attribute("data-test-elem-name")
self.copy_element = element_locator.locator(".icon-copy-element").first
self.remove_element = element_locator.locator(".icon-remove").first
# 重写点击事件
self.elem_locator.click = self.element_click_action

# 元素点击动作
def element_click_action(self):
self.elem_locator.evaluate("element => {element.click()}")

附件上传

1
2
3
4
5
6
7
8
9
10
python复制代码class AddImportTemplateUploadInput:
__init__(self, page: Page):
self.page = page
self.add_import_template_upload_input = self.page.locator(
".el-upload > .el-upload__input").first

# 上传execl模版,参数为文件路径
def upload_excel_template(self, file_path):
self.add_import_template_upload_input.set_input_files(file_path) // 找到input元素,然后设置文件路径
self.page.wait_for_timeout(1000)

元素断言

常用语法

以下是一些Playwright测试框架中常用的断言方法:

  1. to_be_visible():验证元素是否可见。这是最常用的断言之一,因为它可以快速检查元素的显示状态。
1
2
scss复制代码# 验证元素是否隐藏
expect(page.locator('#element')).to_be_visible()
  1. to_have_text(text):验证元素是否包含特定的文本内容。这个断言也很常用,因为它可以确认元素中的文本是否符合预期。
1
2
bash复制代码# 验证元素是否包含特定的文本内容
expect(page.locator('#element')).to_have_text('Welcome, user!')
  1. to_be_editable():验证元素是否可编辑。这个断言对于表单元素来说非常重要,确保用户可以输入数据。
1
2
scss复制代码# 验证元素是否可编辑
expect(page.locator('#input')).to_be_editable()
  1. to_have_class(class_name):验证元素是否具有特定的类名。类名通常与元素的视觉和功能特性相关联。
1
2
bash复制代码# 验证元素是否具有特定的类名
expect(page.locator('#element')).to_have_class('active')
  1. to_have_attribute(name, value):验证元素是否具有特定的属性和值。属性值可以提供关于元素状态的重要信息。
1
2
csharp复制代码# 验证元素是否具有特定的属性和值
await expect(page.locator('#element')).to_have_attribute('class', '')
  1. to_have_css(property, value):验证元素是否具有特定的CSS属性和值。CSS属性影响元素的布局和外观。
1
2
bash复制代码# 验证元素是否具有特定的CSS属性和值
expect(page.locator('#element')).to_have_css('display', 'none')
  1. to_have_js_property(name, value):验证元素是否具有特定的JavaScript属性和值。这些属性可能会影响元素的行为和状态。
1
2
python复制代码# 验证元素是否具有特定的JavaScript属性和值
expect(page.locator('#checkbox')).to_have_js_property('checked', True)
  1. to_have_id(id):验证元素是否具有特定的ID。ID是元素的唯一标识符,通常用于精确定位。
1
2
bash复制代码# 验证元素是否具有特定的ID
expect(page.locator('#element')).to_have_id('unique-id')
  1. to_have_count(count):验证页面上特定元素的数量是否符合预期。这个断言用于检查元素集合的大小。
1
2
scss复制代码# 验证页面上特定元素的数量是否符合预期
expect(page.locator('.item')).to_have_count(4)
  1. to_be_focused():验证元素是否获得焦点。焦点状态对于交互元素来说非常重要。
1
2
scss复制代码# 验证元素是否获得焦点
expect(page.locator('#input')).to_be_focused()
  1. to_be_hidden():验证元素是否隐藏。这与to_be_visible()相对,用于检查元素是否未显示。
1
2
scss复制代码# 验证元素是否隐藏
expect(page.locator('#element')).to_be_hidden()
  1. to_be_empty():验证容器元素是否为空。这个断言用于确认容器类元素中没有子元素。
1
2
scss复制代码# 验证容器元素是否为空
expect(page.locator('#container')).to_be_empty()

判断 是否有 某个元素

1
2
3
4
5
6
python复制代码async def element_exists(page, selector):
return await page.locator(selector).count() > 0

# 使用示例
exists = await element_exists(page, '#element-id')
print('元素存在:', exists)

工具方法

示例-> Python中的指定文件夹压缩,类似于鼠标右键 -> 压缩该文件夹📂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lua复制代码def compress_directory(file_path, zip_file_path):
# 判断file_path是否存在
if not os.path.exists(file_path):
raise Exception(f'Error: {file_path} not found')
if os.path.exists(zip_file_path):
os.remove(zip_file_path)

with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
base_path = os.path.abspath(file_path)
for root, dirs, files in os.walk(file_path):
for file in files:
if file.startswith('.~'): // 跳过某些文件
continue
if file.startswith('~'): // 跳过某些文件
continue
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, base_path)
zipf.write(file_path, arcname=relative_path)

使用示例:

1
2
3
ini复制代码file_path = '/Users/xxx/Documents/tests/app_test_assets/xxx系统场景测试模版'
zip_file_path = '/Users/xxx/Documents/tests/app_test_assets/zip/xxx系统场景测试模版.zip'
compress_directory(file_path, zip_file_path)

避坑📢:mac解压会自动包裹一层文件夹

小白经验,欢迎指正

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%