《PlayWright全解析——从入门到精通》-4
元素定位
在PlayWright中的元素定位基本跟Selenium是类似的,熟悉CSS选择器定位以及xpath定位的同学可以无缝过渡。当然,PlayWright也有自己定义元素的特色,我们在这里仔细讲讲。
PlayWright内置很多很方便的定位器,我们一个一个来研究。
角色定位器(Role)
这是一个官方最为推荐的定位器,从用户角度讲更直观,易于理解,并且稳定。方法为getByRole
。
假设现在有一个按钮:
1
|
button>Sign inbutton> |
我们可以这样来操作点击:
1
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
getByRole
方法有两个参数:
-
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 -
options
,可选参数,是一个对象,用于确定目标元素的一些属性,可用的属性有以下这些:-
checked
,可选参数,布尔型,对应于aria-checked
的值或者原生的的值
-
disabled
,可选参数,布尔型,对应于aria-disabled
或者disabled
的值。 -
exact
,可选参数,布尔型,如果设置为true,则会精确匹配name
属性,大小写敏感,但是忽略左右两边的空格。如果name
属性配置了正则表达式,则exact
属性被忽略。 -
expanded
,可选参数,布尔型,对应aria-expanded
设置的值,表示是否被展开。 -
includeHidden
,可选参数,布尔型,是否包含隐藏元素,只有被ARIA
设置的元素才可以被选中。 -
level
,可选参数,数字型,只作用于heading
,listitem
,row
,treeitem
这些角色,比如h1
~`h6`。 -
name
,可选参数,字符串或正则表达式类型,用于匹配accessible name
,通常是元素的可见文本。默认情况下忽略大小写,只要有子字符串匹配就认为符合。 -
pressed
,可选参数,布尔型,对应aria-pressed
的值,表示是否按下。 -
selected
,可选参数,布尔型,对应aria-selected
的值,表示是否被选中。
-
官方给出了一些例子:
1 2 3 4 5 6 |
h3>Sign uph3> label> input type="checkbox" /> Subscribe label> br/> button>Submitbutton> |
用角色定位器可以这样操作:
1 2 3 4 5 |
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible(); await page.getByRole('checkbox', { name: 'Subscribe' }).check(); await page.getByRole('button', { name: /submit/i }).click(); |
Label定位器
这个就是普通的label标签定义的文字,通常用在form表单的那些字段的定位,比较方便。看下面的例子:
1
|
label>Password input type="password" />label> |
操作可以这样写:
1
|
await page.getByLabel('Password').fill('secret');
|
placeholder定位器
顾名思义,就是对应元素的placeholder属性,看例子:
1
|
input type="email" placeholder="name@example.com" />
|
操作是这样:
1 2 3 |
await page .getByPlaceholder("name@example.com") .fill("playwright@microsoft.com"); |
文本定位器(Text)
还是看例子:
1
|
span>Welcome, Johnspan> |
匹配文字:
1
|
await expect(page.getByText('Welcome, John')).toBeVisible();
|
精确匹配:
1
|
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
|
正则匹配:
1
|
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
|
文本定位器建议用在像
p
、div
、span
这类非交互式的元素定位上,如果是input
这类交互式的元素,还是用角色定位器。
替换文本定位器(alt text)
顾名思义,针对有alt
属性的元素,一般像img
元素,看例子:
1
|
img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
|
使用方法:
1
|
await page.getByAltText('playwright logo').click();
|
title定位器
针对有title
属性的元素,看例子:
1
|
span title='Issues count'>25 issuesspan> |
使用方法:
1
|
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
|
test-id定位器
这是一个比较特殊的定位器,是专门针对测试的,针对一些元素,可以和开发约定一个叫data-testid
的属性,用于该元素的定位。看例子:
1
|
button data-testid="directions">Itinérairebutton> |
使用方法:
1
|
await page.getByTestId('directions').click();
|
当然,也可以不使用data-testid
这个属性,可以自定义名称,需要在配置文件中这样配置:
1 2 3 4 5 6 7 8 |
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { testIdAttribute: 'data-pw' } }); |
那么这样定义后,就约定了testid
的属性为data-pw
了。再看例子:
1
|
button data-pw="directions">Itinérairebutton> |
用法和原来一样。
css
/xpath
定位
这应该是大家最熟悉的定位方式了,只要是用过selenium的,对这两种定位方式必然倍感亲切。但是根据PlayWright的说法,不管是CSS还是XPATH,都不是推荐的定位方式,因为他们都伴随着很多的不稳定因素,比如少了一层div
啦,调整了顺序啦,都会导致定位不到,而使用角色定位器则不会有这方面的问题。当然这是官方的说辞,如果的你能把css
/xpath
定位写的足够稳定,其实也能起到和角色定位器一样的稳定效果。
先看下用法:
1 2 3 4 5 |
await page.locator('css=button').click(); await page.locator('xpath=//button').click(); await page.locator('button').click(); await page.locator('//button').click(); |
注意,这里用了通用locator方法,里面的css和xpath可以加上关键字也可以省略。xpath必须以
//
或者/
开头。
css
/xpath
定位千万不要写的太长,越长,依赖的中间元素越多,越容易带来定位的不稳定,维护代价也更大。比如下面这些就是很差的写法:
1 2 3 4 5 6 7 |
await page.locator( '#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input' ).click(); await page .locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input') .click(); |
关于影子DOM
中元素的定位
首先理解什么是影子DOM,Shadow DOM(影子DOM)是Web组件的一项技术,它允许你将一个元素的样式和行为封装起来,以便在不同的上下文中(比如外部文档)中使用时,不会和其它元素的样式和行为发生冲突。Shadow DOM的本质是一种类似于虚拟DOM的技术,它将页面中不同的DOM元素封装成一个独立的组件,使得应用更加灵活、可维护性更高。Shadow DOM使用的关键是其隔离性,它为DOM元素提供了一个完全独立的上下文,使得每个DOM元素都能够拥有自己的私有DOM树和样式规则。这样,就能够更加灵活地设计Web应用,同时也能保持应用的高可维护性。
PlayWright默认所有定位器都可以正常操作在影子DOM
中的元素,也就是所谓的穿透影子DOM
,但是需要注意,使用XPATH表达式是不可以穿透影子DOM
的。
来看一个影子DOM
组件的例子:
1 2 3 4 5 |
x-details role=button aria-expanded=true aria-controls=inner-details> div>Titlediv> #shadow-root div id=inner-details>Detailsdiv> x-details> |
注意,不要直接在html中写上这一段,
影子DOM
的元素需要使用Javascript的attachShadow方法将影子DOM
组件添加到一个正常的HTML节点下。
可以点击div
元素:
1
|
await page.getByText('Details').click();
|
也可以点击x-details
元素:
1
|
await page.locator('x-details', { hasText: 'Details' }).click();
|
过滤器
可以采用locator.filter()
方法对定位出的多个元素进行筛选,filter
方法接受一个对象参数,这个对象有两个可选属性:
-
hasText,可以是字符串或正则表达式
-
has,另一个定位器
看例子:
1 2 3 4 5 6 7 8 9 10 |
ul> li> h3>Product 1h3> button>Add to cartbutton> li> li> h3>Product 2h3> button>Add to cartbutton> li> ul> |
用文本来筛选,使用字符串时,是对大小写不敏感的:
1 2 3 4 5 |
await page .getByRole('listitem') .filter({ hasText: 'product 2' }) .getByRole('button', { name: 'Add to cart' }) .click(); |
用正则表达式,对大小写敏感:
1 2 3 4 5 |
await page .getByRole('listitem') .filter({ hasText: /Product 2/ }) .getByRole('button', { name: 'Add to cart' }) .click(); |
用另一个locator:
1 2 3 4 5 |
await page .getByRole('listitem') .filter({ has: page.getByRole('heading', { name: 'Product 2' })}) .getByRole('button', { name: 'Add to cart' }) .click() |
严格的定位要求
在PlayWright中,如果对一个locator进行互动操作,比如click
或者fill
等,必须要求这个locator只定位到一个元素,否则会报错,提示有多个元素。
假设现在页面中有多个button
,那么下面这个操作就会抛出一个异常:
1
|
await page.getByRole('button').click();
|
但是可以通过选择第几个来操作:
1
|
await page.getByRole('button').nth(1).click();
|
注意,
nth
从0开始计数,所以此处表示选择第二个button
点击
但是,如果本身就是多个元素的操作,比如计数,那就不会抛错:
1
|
await page.getByRole('button').count();
|
动作行为
讲讲一些PlayWright中典型的和页面元素的交互动作。注意了,前面我们介绍的locator
对象,实际并不会真正在页面上去查找元素,所以,即使页面上并没有那个元素,用前面的元素定位的方法,获取一个locator
对象,并不会报任何异常。只有在真正发生交互动作的时候,才会真正在页面上查找元素进行交互。这一点是和Selenium的findElement的方法是不同的,findElement会真正在页面上找元素,如果找不到就直接报异常了。
文本输入
使用fill
方法可以进行文本输入,主要针对,
以及有
[contenteditable]
属性的元素:
1 2 |
// Text input await page.getByRole('textbox').fill('Peter'); |
Checkbox和radio
使用locator.setChecked()
或者locator.checked()
这两个方法都可以操作input[type=checkbox]
、input[type=radio]
或者有[role=checkbox]
属性的元素。
1 2 3 4 5 |
await page.getByLabel('I agree to the terms above').check(); expect(await page.getByLabel('Subscribe to newsletter').isChecked()).toBeTruthy(); // f取消选中状态 await page.getByLabel('XL').setChecked(false); |
select控件
使用locator.selectOption()
可以操作元素。
1 2 3 4 5 6 7 8 |
// 根据value单选 await page.getByLabel('Choose a color').selectOption('blue'); // 根据label值单选 await page.getByLabel('Choose a color').selectOption({ label: 'Blue' }); // 多选 await page.getByLabel('Choose multiple colors').selectOption(['red', 'green', 'blue']); |
鼠标点击
基本操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 鼠标左键单击 await page.getByRole('button').click(); // 双击 await page.getByText('Item').dblclick(); // 鼠标右键点击 await page.getByText('Item').click({ button: 'right' }); // Shift键组合鼠标点击 await page.getByText('Item').click({ modifiers: ['Shift'] }); // 鼠标悬停 await page.getByText('Item').hover(); // 点击元素的指定位置 await page.getByText('Item').click({ position: { x: 0, y: 0} }); |
还有当A元素覆盖在另一个B元素上面,导致B元素无法被点中,可以使用强制点击的方式:
1
|
await page.getByRole('button').click({ force: true });
|
还可以使用事件触发的方式实现click
1
|
await page.getByRole('button').dispatchEvent('click');
|
字符输入
方法locator.type()
用于将一个一个字符进行输入,和fill
方法不同,这是模拟了keydown
, keyup
, keypress
这些事件,可以将这些事件触发。
1
|
await page.locator('#area').type('Hello World!');
|
特殊键
可以用locator.press()
方法来实现键盘上一些特殊控制键的按下:
1 2 3 4 5 6 7 8 |
// 回车键 await page.getByText('Submit').press('Enter'); // ctrl+右箭头 await page.getByRole('textbox').press('Control+ArrowRight'); // 键盘上的$键 await page.getByRole('textbox').press('$'); |
可以使用的键有下面这些:
Backquote
,Minus
,Equal
,Backslash
,Backspace
,Tab
,Delete
,Escape
,ArrowDown
,End
,Enter
,Home
,Insert
,PageDown
,PageUp
,ArrowRight
,
ArrowUp
,F1
-F12
,Digit0
-Digit9
,KeyA
-KeyZ
等。
文件上传
可以使用locator.setInputFiles()
方法来设置将要上传的文件,可以多个文件,注意如果使用相对路径,则表示当前的工作目录开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 设置当前目录下的myfile.pdf文件作为上传文件 await page.getByLabel('Upload file').setInputFiles('myfile.pdf'); // 设置多个文件准备上传 await page.getByLabel('Upload files').setInputFiles(['file1.txt', 'file2.txt']); // 移除上传文件列表 await page.getByLabel('Upload file').setInputFiles([]); // 从buffer中读取文件内容作为上传文件。 await page.getByLabel('Upload file').setInputFiles({ name: 'file.txt', mimeType: 'text/plain', buffer: Buffer.from('this is test') }); |
当完成上传文件的设置后,就可以触发或者直接点击上传按钮了。这里
locator.setInputFiles()
操作的控件,必须是元素。
聚焦元素
可以使用locator.focus()
方法聚焦元素
1
|
await page.getByLabel('Password').focus();
|
拖拽
整个拖拽过程其实分为四步:
鼠标移动到需要拖动的元素上方
点住鼠标左键
移动鼠标到目标位置(元素)
松开鼠标左键
可以使用locator.dragTo()
方法来实现拖拽:
1
|
await page.locator('#item-to-be-dragged').dragTo(page.locator('#item-to-drop-at'));
|
当然,也可以自己实现这样的拖拽过程:
1 2 3 4 |
await page.locator('#item-to-be-dragged').hover(); await page.mouse.down(); await page.locator('#item-to-drop-at').hover(); await page.mouse.up(); |