扫码阅读
手机扫码阅读

《PlayWright全解析——从入门到精通》-4

857 2023-07-17

元素定位

在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(); 

文本定位器建议用在像pdivspan这类非交互式的元素定位上,如果是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(); 
原文链接: https://mp.weixin.qq.com/s?__biz=MzU5ODE2OTc1OQ==&mid=2247496369&idx=1&sn=15cbb906463330685f89577fc34a1966

TestOps 助力提升价值交付质效

27 篇文章
浏览 12.8K
加入社区微信群
与行业大咖零距离交流学习
软件研发质量管理体系建设 白皮书上线