Playwright 实战技巧大揭秘 Playwright快速上手指南
wptr33 2024-11-14 19:21 21 浏览
一:实施的背景
相信使用过Selenium WebDriver的小伙伴对其最大的诟病有3点,一是浏览器的driver和版本对应问题,第二是Selenium的执行速度,最后一个槽点是对页面元素文本值的断言非常不便。在我们长期维护大量UI自动化测试用例的过程中这两个痛点会让我们耗费不少精力和时间。
二:PlayWright最佳实践
2.1为何要选择PlayWright?
对Selenium的痛点,PlayWright给出了完美的解决方案!
PlayWright的架构:
Playwright使用 Chrome DevTools 协议与 Chromium 通信。一旦触发测试,client端代码将被转换为JSON格式,然后使用websocket协议发送到服务器。
Playwright通过单个 websocket 协议连接传达所有请求,该连接将保持不变,直到所有测试执行完成。由于命令是在单个连接上发送的,因此测试失败或不稳定的可能性较小,并且命令可以快速执行。
Playwright启动速度比较快,拥有更多的定位方式,不需要安装驱动,能够在多个平台上运行,提供录制功能实现录制用例视屏,使用上来说Playwright也比较容易,无需过多封装即可直接使用.
2.2 环境部署
安装playwright前先配置好node和python环境,之后通过pip来安装playwright和其他库。
以Mac OS为例,执行如下命令:
安装playwright库
pip install playwright
然后安装browsers(会安装chromium,firefox,webkit浏览器)
playwright install
如果只想安装指定的browser,则执行如下命令
playwright install chromium
2.3 重要的名词
在开始尝试使用playwright之前,需要先了解它的几个重要概念。
Browser:是一个浏览器实例,代表一个浏览器会话。它是一个全局的上下文,可以包含多个 BrowserContext。
使用:创建浏览器实例管理浏览器生命周期:可以启动浏览器、关闭浏览器等。
from playwright.sync_api import Playwright
def test_api_show(playwright: Playwright):
browser = playwright.chromium.launch()
BrowserContext:是浏览器中的一个隔离环境,可以包含多个 Page。每个 BrowserContext有自己的浏览器存储,例如 cookies 和本地存储。
使用:创建新的上下文,管理上下文。可以添加页面、关闭页面等。
from playwright.sync_api import Playwright
def test_api_show(playwright: Playwright):
browser = playwright.chromium.launch()
context = browser.new_context()
Page:是浏览器中的一个标签页,可以进行页面导航、操作 DOM 元素、捕获页面截图等。
使用:导航页面,操作 DOM 元素,捕获页面截图,处理页面事件。
from playwright.sync_api import Playwright
def test_api_show(playwright: Playwright):
browser = playwright.chromium.launch()
context = browser.new_context()
page = context.new_page()
page.goto("")
2.4 元素定位
元素定位是PlayWright的核心部分,我们会详细演示常用方法的使用。
get_by_placeholder:根据页面元素的placeholder属性值定位
以上图为例,我们想定位<销售机会名称>输入框可以使用get_by_placeholder方法。
page.get_by_placeholder("销售机会名称").fill("商机名称")
get_by_role:允许通过元素的ARIA角色、ARIA属性和可访问名称来定位它们。
初次使用get_by_role方法可能会有点懵,因为不知道元素的role该写为什么!
PlayWright官网上给出了role的类型:
role "alert" | "alertdialog" | "application" | "article" | "banner" | "blockquote" | "button" | "caption" | "cell" | "checkbox" | "code" | "columnheader" | "combobox" | "complementary" | "contentinfo" | "definition" | "deletion" | "dialog" | "directory" | "document" | "emphasis" | "feed" | "figure" | "form" | "generic" | "grid" | "gridcell" | "group" | "heading" | "img" | "insertion" | "link" | "list" | "listbox" | "listitem" | "log" | "main" | "marquee" | "math" | "meter" | "menu" | "menubar" | "menuitem" | "menuitemcheckbox" | "menuitemradio" | "navigation" | "none" | "note" | "option" | "paragraph" | "presentation" | "progressbar" | "radio" | "radiogroup" | "region" | "row" | "rowgroup" | "rowheader" | "scrollbar" | "search" | "searchbox" | "separator" | "slider" | "spinbutton" | "status" | "strong" | "subscript" | "superscript" | "switch" | "tab" | "table" | "tablist" | "tabpanel" | "term" | "textbox" | "time" | "timer" | "toolbar" | "tooltip" | "tree" | "treegrid" | "treeitem"
在上图销售机会新建页右上角,有<取消>和<保存>两个button组件。定位它们,可以用get_by_role。
page.get_by_role("button", name="保存").click()
上图中,销售阶段的下拉框选项值是一个div元素,除了xpath或者css-selector方式外很难想到其他发方式来定位到它。但是在PlayWright中,我们可以使用get_by_role来处理它,具体的代码如下:
//点击下拉框选项值的父级元素,即:div。
page.locator(parent_locator_path).locator("visible=true").click()
// 直接处理选项值,根据has_text=””来选中下拉选项值
page.get_by_role("listitem").filter(has_text=item_name).click()
get_by_text:根据包含的文本值定位元素
page.locator('//div[@x-placement="bottom-start"]').get_by_text(model_name, exact=True).locator("visible=true").click()
locator:可以在定位器的子树中找到一个与指定选择器匹配的元素。它还可以过滤选项值。
locator可以接受任何形式定位方式,也就是说locator_path可以是id,xpath,css,role等。
locator(locator_path).locator("visible=true").click()
page.locator("locator_path", has_text="")
page.locator("locator_path", has_not_text="")
索引nth
大多数时候,我们会遇到在一个页面上存在多个类似元素的情况。如下图,页面中存在多个类似的元素,这种业务场景下,我们可以考虑先定位一组元素然后根据返回的元素列表的索引来定位到具体的组件。
relation_list =page.locator('//div[@class="field-input-block__suffix"]').locator("visible=true")
relation_list.nth(element_index).click()
filter: 这个方法根据选项缩小现有定位器的范围,例如按文本过滤。它可以被链接起来进行多次过滤。
page.get_by_role("listitem").filter(has_text=item_name).click()
2.5 交互动作
在UI自动化测试工作中,我们经常用到的页面交互无非是点击,输入,刷新这三个动作。
PlayWright对上面三种交互动作都提供了支持。
click:点击元素
page.get_by_placeholder(place_holder, exact=True).click()
fill:输入文本
page.locator(locator_path).locator("visible=true").fill(input_context)
reload():重载页面
page.reload()
select_option:从下拉选项值中选择
官网示例的页面元素
<select multiple>
<option value="red">Red</div>
<option value="green">Green</div>
<option value="blue">Blue</div>
</select>
使用select_option的示例:
element.select_option("blue")
element.select_option(label="blue")
element.select_option(value=["red", "green", "blue"])
set_checked:设置复选框或单选按钮元素的状态
page.get_by_role("checkbox").set_checked(True)
2.6 强大的断言
PlayWright自带断言功能,并且我们可以很方便地获取到页面里各种容器元素下所有的文本值。
all_inner_texts/inner_text
# 获取容器内的文本值
def get_inner_text_on_list(self, data_key_name):
# 根据列表页的主键获取一行数据的文本值(返回的是str,也是奇葩)
text_list = self.page.get_by_role("row", name=data_key_name).locator("visible=true").inner_text()
return text_list
断言expect
# 判断系统里一闪而过的tips文案
def assert_action_success(self):
expect(self.page.locator('//p[@class="el-message__content"]')).to_contain_text("成功")
2.7 PageObject模式封装
PO模式不作过多介绍了,相信读者朋友们或多或少都有了解。下面直接上代码。
封装playwright的基础操作
Base包里封装了页面元素的基本操作,包含了元素定位,点击输入,断言等通用功能点的封装。
# -*- encoding = utf-8 -*-
# Author:晴空-姜林斌
# Date:2024-06-06
from playwright.sync_api import sync_playwright, expect
web_host = "https://appwebfront.xbongbong.com/"
class Base:
def __init__(self):
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.launch(headless=False)
self.context = self.browser.new_context()
self.page = self.context.new_page()
# 地址路由
self.url_dict = {"MULTI_PROD_WEB_MARKET_LIST": "/#/market-manage/market-activity?subBusinessType=8100&appId=185800&menuId=2000341&saasMark=1&distributorMark=0",
"MULTI_PROD_WEB_CLUE_LIST": "/#/market-manage/sales-leads?subBusinessType=8001&appId=185800&menuId=2000343&saasMark=1&distributorMark=0"}
# 根据placeholder定位字段并输入内容
def input_by_placeholder(self, place_holder, send_keys):
self.page.get_by_placeholder(place_holder, exact=True).fill(send_keys)
# 根据placeholder定位并点击
def click_by_placeholder(self, place_holder):
self.page.get_by_placeholder(place_holder, exact=True).click()
# 输入编号信息(系统中的编号-流水号字段)
def input_serial(self, send_keys):
self.page.get_by_placeholder("流水号", exact=True).clear()
self.page.get_by_placeholder("流水号", exact=True).fill(send_keys)
# locator操作控件并输入内容 支持css-selector xpath id格式
def input_by_locator(self, locator_path, input_context):
self.page.locator(locator_path).locator("visible=true").fill(input_context)
# 根据locator定位组件并点击
def click_by_locator(self, locator_path):
self.page.locator(locator_path).locator("visible=true").click()
self.sleep(500)
# 点击button组件
def click_button(self, button_name):
self.page.get_by_role("button", name=button_name, exact=True).click()
if str(button_name) in "保存修改删除":
self.sleep(2000)
else:
self.sleep(1000)
# 根据下列选项的值选择
def set_opportunity_stage(self, item_name):
self.click_by_locator("//form/div[7]/div/div/div/div[1]/span")
self.page.get_by_role("listitem").filter(has_text=item_name).click()
# 设置任务的所属阶段
def set_task_stage(self, item_name):
self.click_by_locator("//form/div[8]/div/div/div/div/input")
self.page.get_by_role("listitem").filter(has_text=item_name).click()
# 设置风险的所属阶段
def set_risk_stage(self, item_name):
# 设置任务的所属阶段
self.click_by_locator("//form/div[8]/div/div/div/div[1]/input")
self.page.get_by_role("listitem").filter(has_text=item_name).click()
# 设置企微销售机会的阶段
def set_wechat_opportunity_stage(self, item_name):
self.click_by_locator("//form/div[7]/div/div/div/div[1]/span")
self.page.get_by_role("listitem").filter(has_text=item_name).click()
# 关闭详情页
def close_detail(self):
self.click_by_locator('//div[@class="title__close-btn"]')
# 新建页切换模板
def switch_model(self, model_name):
self.click_by_placeholder("请选择模板")
self.page.locator('//div[@x-placement="bottom-start"]').get_by_text(model_name, exact=True).locator("visible=true").click()
self.sleep(1000)
# 选择单选
def select_single_member(self, add_index, member_name):
radio_button_list = self.page.locator('//div[@class="user-radio-btn"]').locator("visible=true")
radio_button_list.nth(add_index).click()
self.page.locator('//div[@class="person-tabs"]').get_by_placeholder("请输入内容").fill(member_name)
self.sleep(1000)
self.page.locator('//div[@class="user-tab-content__user"]/div/label/span[1]').click()
self.sure_on_dialog()
# 服务云 独用的成员单选
def single_team_for_service_cloud(self, add_index, member_name):
radio_button_list = self.page.locator('//div[@class="user-radio-btn-block"]').locator("visible=true")
radio_button_list.nth(add_index).click()
self.page.locator('//div[@class="person-tabs"]').get_by_placeholder("请输入内容").fill(member_name)
self.sleep(1000)
self.page.locator('//div[@class="user-tab-content__user"]/div/label/span[1]').click()
self.sure_on_dialog()
# 点击超链接
def partial_link(self, link_text):
locator_path = 'a:has-text("' + str(link_text) + '")'
self.page.locator(locator_path).locator("visible=true").click()
self.sleep(1000)
# 单据详情页 更多-关联新建
def new_on_detail(self, business_name):
link_business_locator = 'li:has-text("' + str(business_name) + '")'
self.page.get_by_role("button", name="", exact=True).click()
self.page.locator(link_business_locator).click()
# 单据新建/编辑页 选择所有关联产品
def select_all_products(self):
self.page.locator("span:has-text('添加产品')").locator("visible=true").click()
self.page.locator("//table/thead/tr/th[1]/div/label/span").locator("visible=true").click()
self.click_button("确定")
# 单据详情页查看 更多-tab栏
def view_more_tab_on_detail(self, tab_name):
self.page.locator('//div[@class="detail-tabs-more-button"]').locator("visible=true").click()
self.page.wait_for_timeout(1000)
tab_path = "li:has-text('" + str(tab_name) + "')"
self.page.locator(tab_path).locator("visible=true").click()
# 详情页的复制/编辑/删除
def copy_and_edit_on_detail(self, action_name):
button_list = self.page.locator('//div[@class="detail-permission-bts"]/div').locator('[type="button"]').all()
for button_element in button_list:
if button_element.inner_text().__contains__(action_name):
button_element.click()
self.sleep(1000)
break
else:
continue
# 列表搜索
def search_on_list(self, search_condition):
self.page.locator('//div[@class="search"]/div/input').fill(search_condition)
self.page.locator('//div[@class="search"]/div/div').locator("visible=true").click()
# 列表页新建
def new_on_list(self):
self.click_button("新建")
# 详情页的复制-编辑-删除
def common_action_on_detail(self, action_name):
locator_name = " " + str(action_name)
self.page.get_by_role("button", name=locator_name).locator("visible=true").click()
# 跳转到指定列表
def go(self, url_key):
list_url = self.url_dict[url_key]
self.page.goto(web_host + str(list_url))
self.sleep(3000)
# 强制等待,单位毫秒
def sleep(self, wait_duration):
self.page.wait_for_timeout(wait_duration)
# 刷新页面
def refresh(self):
self.page.reload(timeout=10000)
# 关联数据选择 支持系统和自定义
def select_relation_data(self, model, element_index, relation_data_name):
if str(model) == "saas":
saas_relation_list = self.page.locator('//div[@class="field-input-block__suffix"]').locator("visible=true")
saas_relation_list.nth(element_index).click()
# 搜索关联数据
self.page.get_by_placeholder("搜索", exact=True).locator("visible=true").fill(relation_data_name)
self.sleep(1000)
self.page.locator('//table[@class="el-table__body"]/tbody/tr/td[1]/div/label').locator(
"visible=true").click()
self.sure_on_dialog()
elif str(model) == "paas":
paas_relation_list = self.page.locator('//div[@class="relation-data-input__right"]').locator("visible=true")
paas_relation_list.nth(element_index).click()
# 搜索关联数据
self.page.get_by_placeholder("请输入内容", exact=True).locator("visible=true").fill(relation_data_name)
self.sleep(1000)
self.page.locator('//table[@class="el-table__body"]/tbody/tr/td[1]/div/label').locator(
"visible=true").click()
self.sure_on_dialog()
# 非nvwa业务模块的关联数据选择
def set_not_nvwa_relation_data(self, element_index, relation_data_name):
relation_list = self.page.locator('//div[@class="field-input-block__suffix"]').locator("visible=true")
relation_list.nth(element_index).click()
# 搜索关联数据
self.page.get_by_placeholder("搜索", exact=True).locator("visible=true").fill(relation_data_name)
self.sleep(1000)
self.page.locator('//table[@class="el-table__body"]/tbody/tr/td[1]/div/span/label').locator(
"visible=true").click()
self.sure_on_dialog()
# 在列表全选后进行批量删除/彻底删除
def batch_del_on_list(self, batch_action_name):
# 列表页全选
self.page.locator('#select-all').locator("visible=true").click()
self.click_button(batch_action_name)
self.sure_on_dialog()
# 在系统的对话框中点击确定
def sure_on_dialog(self):
self.click_button("确定")
self.sleep(1000)
# 切换详情页tab
def switch_tab(self, tab_name):
self.sleep(1000)
tab_list = self.page.locator('//div[@role="tablist"]').locator('//div[@role="tab"]').locator(
"visible=true").all()
for tab in tab_list:
if str(tab.inner_text()).__contains__(tab_name):
tab.click()
self.sleep(1000)
break
else:
continue
# 获取容器内的文本值
def get_inner_text_on_list(self, data_key_name):
# 根据列表页的主键获取一行数据的文本值(返回的是str,也是奇葩)
text_list = self.page.get_by_role("row", name=data_key_name).locator("visible=true").inner_text()
return text_list
# 获取基本信息栏容器内的文本值
def get_inner_text_on_basic(self):
basic_text = self.page.locator("div.base-detail__bottom--left > form").locator("visible=true").inner_text()
return basic_text
# 获取单据详情页-负责团队tab信息
def get_sales_team_info(self):
team_text = self.page.locator('//div[@class="sale-team-detail__body"]').locator("visible=true").inner_text()
return team_text
# 获取tab栏下的内容 model分为base和more分别对应基本tab和更多tab
def get_inner_text_of_tab(self, model, tab_name):
if str(model) == "base":
self.switch_tab(tab_name)
elif str(model) == "more":
self.view_more_tab_on_detail(tab_name)
else:
pass
# 获取tab栏下的inner-text
tab_context = self.page.locator('//div[@class="detail-tab-containt"]').locator("visible=true").inner_text()
return tab_context
# 获取编辑页的inner-text
def get_inner_text_of_update(self):
update_page_text = self.page.locator('//div[@class="edit-dialog__content"]').locator(
"visible=true").inner_text()
return update_page_text
# 判断新建/编辑/删除成功
def assert_action_success(self):
action_tips_list = self.page.locator('//p[@class="el-message__content"]').locator("visible=true")
expect(action_tips_list.nth(0)).to_contain_text("成功")
测试用例:
考虑到实际的业务场景,笔者在该项目里并没有封装具体的页面而是封装好了base通用操作操作后直接使用了,如果读者朋友们有需要可以自行封装页面的操作。
ding_prod_web是具体的UI自动化测试用例的目录,具体的结构如下图。
2.8 集成到Jenkins
在Jenkins里配置自由风格的Job
构建步骤是执行sh
python3 -m pytest /Users/sunnysky/PythonProj/PlayWrightWeb/ding_prod_web --alluredir=allure-results
构建后的操作配置为Allure Report,以下是生成的Allure报告
三:展望未来
人工智能技术将对自动化测试产生深远的影响。人工智能技术通过大数据、AI和机器学习,机器将学会如何测试;这将对自动化测试进一步提升测试的有效性。自动化测试的自愈技术、大数据预测、图象识别等技术,将使得测试更加智能化、精准化。
结合了LLM的AI框架开始小荷才露尖尖角:https://github.com/Skyvern-AI/skyvern。
作为测试人员,我们需要不断学习新技术,适应新变化,以确保我们的测试工作能够为软件质量提供坚实的保障。
希望我的分享能为大家带来一些启发和帮助。让我们共同期待自动化测试技术更加美好的未来!
相关推荐
- Python自动化脚本应用与示例(python办公自动化脚本)
-
Python是编写自动化脚本的绝佳选择,因其语法简洁、库丰富且跨平台兼容性强。以下是Python自动化脚本的常见应用场景及示例,帮助你快速上手:一、常见自动化场景文件与目录操作...
- Python文件操作常用库高级应用教程
-
本文是在前面《Python文件操作常用库使用教程》的基础上,进一步学习Python文件操作库的高级应用。一、高级文件系统监控1.1watchdog库-实时文件系统监控安装与基本使用:...
- Python办公自动化系列篇之六:文件系统与操作系统任务
-
作为高效办公自动化领域的主流编程语言,Python凭借其优雅的语法结构、完善的技术生态及成熟的第三方工具库集合,已成为企业数字化转型过程中提升运营效率的理想选择。该语言在结构化数据处理、自动化文档生成...
- 14《Python 办公自动化教程》os 模块操作文件与文件夹
-
在日常工作中,我们经常会和文件、文件夹打交道,比如将服务器上指定目录下文件进行归档,或将爬虫爬取的数据根据时间创建对应的文件夹/文件,如果这些还依靠手动来进行操作,无疑是费时费力的,这时候Pyt...
- python中os模块详解(python os.path模块)
-
os模块是Python标准库中的一个模块,它提供了与操作系统交互的方法。使用os模块可以方便地执行许多常见的系统任务,如文件和目录操作、进程管理、环境变量管理等。下面是os模块中一些常用的函数和方法:...
- 21-Python-文件操作(python文件的操作步骤)
-
在Python中,文件操作是非常重要的一部分,它允许我们读取、写入和修改文件。下面将详细讲解Python文件操作的各个方面,并给出相应的示例。1-打开文件...
- 轻松玩转Python文件操作:移动、删除
-
哈喽,大家好,我是木头左!Python文件操作基础在处理计算机文件时,经常需要执行如移动和删除等基本操作。Python提供了一些内置的库来帮助完成这些任务,其中最常用的就是os模块和shutil模块。...
- Python 初学者练习:删除文件和文件夹
-
在本教程中,你将学习如何在Python中删除文件和文件夹。使用os.remove()函数删除文件...
- 引人遐想,用 Python 获取你想要的“某个人”摄像头照片
-
仅用来学习,希望给你们有提供到学习上的作用。1.安装库需要安装python3.5以上版本,在官网下载即可。然后安装库opencv-python,安装方式为打开终端输入命令行。...
- Python如何使用临时文件和目录(python目录下文件)
-
在某些项目中,有时候会有大量的临时数据,比如各种日志,这时候我们要做数据分析,并把最后的结果储存起来,这些大量的临时数据如果常驻内存,将消耗大量内存资源,我们可以使用临时文件,存储这些临时数据。使用标...
- Linux 下海量文件删除方法效率对比,最慢的竟然是 rm
-
Linux下海量文件删除方法效率对比,本次参赛选手一共6位,分别是:rm、find、findwithdelete、rsync、Python、Perl.首先建立50万个文件$testfor...
- Python 开发工程师必会的 5 个系统命令操作库
-
当我们需要编写自动化脚本、部署工具、监控程序时,熟练操作系统命令几乎是必备技能。今天就来聊聊我在实际项目中高频使用的5个系统命令操作库,这些可都是能让你效率翻倍的"瑞士军刀"。一...
- Python常用文件操作库使用详解(python文件操作选项)
-
Python生态系统提供了丰富的文件操作库,可以处理各种复杂的文件操作需求。本教程将介绍Python中最常用的文件操作库及其实际应用。一、标准库核心模块1.1os模块-操作系统接口主要功能...
- 11. 文件与IO操作(文件io和网络io)
-
本章深入探讨Go语言文件处理与IO操作的核心技术,结合高性能实践与安全规范,提供企业级解决方案。11.1文件读写11.1.1基础操作...
- Python os模块的20个应用实例(python中 import os模块用法)
-
在Python中,...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)