定位器
定位器是表示一个或多个元素的方式。每个定位器都由一个称为选择器的字符串定义。Vitest 通过提供方便的方法在后台生成这些选择器,从而抽象了选择器。
定位器 API 使用了 Playwright 的定位器 的一个分支,称为 Ivya。然而,Vitest 将此 API 提供给每个 provider。
TIP
本页介绍了 API 的使用。为了更好地了解定位器及其用法,请阅读 Playwright 的“定位器”文档。
getByRole
function getByRole(
role: ARIARole | string,
options?: LocatorByRoleOptions,
): Locator通过元素的 ARIA 角色、ARIA 属性 和 可访问名称 创建一种定位元素的方式。
TIP
如果你只查询单个元素(例如使用 getByText('The name')),通常更好的做法是使用 getByRole(expectedRole, { name: 'The name' })。可访问名称查询并不会替代其他查询,例如 *ByAltText 或 *ByTitle。虽然可访问名称可以等于这些属性的值,但它并不能替代这些属性的功能。
考虑以下 DOM 结构。
<h3>Sign up</h3>
<label>
Login
<input type="text" />
</label>
<label>
Password
<input type="password" />
</label>
<br/>
<button>Submit</button>你可以通过每个元素的隐式角色来定位它们:
await expect.element(
page.getByRole('heading', { name: 'Sign up' })
).toBeVisible()
await page.getByRole('textbox', { name: 'Login' }).fill('admin')
await page.getByRole('textbox', { name: 'Password' }).fill('admin')
await page.getByRole('button', { name: /submit/i }).click()WARNING
角色通过字符串相等性进行匹配,不会继承自 ARIA 角色层次结构。因此,查询像 checkbox 这样的超类角色时,不会包含具有子类角色(如 switch)的元素。
默认情况下,许多语义化的 HTML 元素都有一个角色;例如,<input type="radio"> 具有 "radio" 角色。非语义化的 HTML 元素没有角色;没有添加语义的 <div> 和 <span> 返回 null。role 属性可以提供语义。
根据 ARIA 指南,强烈不建议 通过 role 或 aria-* 属性为已经具有隐式角色的内置元素提供角色。
Options
exact: booleanname是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果name是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。tsx<button>Hello World</button> page.getByRole('button', { name: 'hello world' }) // ✅ page.getByRole('button', { name: 'hello world', exact: true }) // ❌ page.getByRole('button', { name: 'Hello World', exact: true }) // ✅checked: boolean是否应包含已选中的元素(由
aria-checked或<input type="checkbox"/>设置)。默认情况下,不会应用此过滤器。更多信息请参阅
aria-checked。tsx<> <button role="checkbox" aria-checked="true" /> <input type="checkbox" checked /> </> page.getByRole('checkbox', { checked: true }) // ✅ page.getByRole('checkbox', { checked: false }) // ❌disabled: boolean是否应包含已禁用的元素。默认情况下,不会应用此过滤器。请注意,与其他属性不同,
disable状态是可继承的。更多信息请参阅
aria-disabled。tsx<input type="text" disabled /> page.getByRole('textbox', { disabled: true }) // ✅ page.getByRole('textbox', { disabled: false }) // ❌expanded: boolean是否应包含展开的元素。默认情况下,不会应用此过滤器。
更多信息请参阅
aria-expanded。tsx<a aria-expanded="true" href="example.com">Link</a> page.getByRole('link', { expanded: true }) // ✅ page.getByRole('link', { expanded: false }) // ❌includeHidden: boolean是否应查询那些通常从可访问性树中排除的元素。默认情况下,只有非隐藏元素会匹配角色选择器。更多信息请参阅 可访问性树中的排除规则。
请注意,角色
none和presentation始终会被包含。tsx<button style="display: none" /> page.getByRole('button') // ❌ page.getByRole('button', { includeHidden: false }) // ❌ page.getByRole('button', { includeHidden: true }) // ✅level: number一个数字属性,通常存在于
heading、listitem、row、treeitem角色中,并且对于<h1>-<h6>元素有默认值。默认情况下,不会应用此过滤器。更多信息请参阅
aria-level。tsx<> <h1>Heading Level One</h1> <div role="heading" aria-level="1">Second Heading Level One</div> </> page.getByRole('heading', { level: 1 }) // ✅ page.getByRole('heading', { level: 2 }) // ❌name: string | RegExp可访问名称。默认情况下,匹配是不区分大小写的,并且会搜索子字符串。使用
exact选项来控制此行为。tsx<button>Click Me!</button> page.getByRole('button', { name: 'Click Me!' }) // ✅ page.getByRole('button', { name: 'click me!' }) // ✅ page.getByRole('button', { name: 'Click Me?' }) // ❌pressed: boolean是否应包含被按下的元素。默认情况下,不会应用此过滤器。
更多信息请参阅
aria-pressed。tsx<button aria-pressed="true">👍</button> page.getByRole('button', { pressed: true }) // ✅ page.getByRole('button', { pressed: false }) // ❌selected: boolean是否应包含被选中的元素。默认情况下,不会应用此过滤器。
更多信息请参阅
aria-selected。tsx<button role="tab" aria-selected="true">Vue</button> page.getByRole('button', { selected: true }) // ✅ page.getByRole('button', { selected: false }) // ❌
更多内容请参阅
getByAltText
function getByAltText(
text: string | RegExp,
options?: LocatorOptions,
): Locator创建一个能够找到具有匹配 alt 属性的元素的定位器。与 testing-library 的实现不同,Vitest 会匹配任何具有匹配 alt 属性的元素。
<img alt="Incredibles 2 Poster" src="/incredibles-2.png" />
page.getByAltText(/incredibles.*? poster/i) // ✅
page.getByAltText('non existing alt text') // ❌Options
exact: booleantext是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。
更多内容请参阅
getByLabelText
function getByLabelText(
text: string | RegExp,
options?: LocatorOptions,
): Locator创建一个能够找到具有关联标签的元素的定位器。
下方示例中,page.getByLabelText('Username') 会一次性选中所有相关输入框。
// for/htmlFor 标签与表单元素 ID 的关系
<label for="username-input">Username</label>
<input id="username-input" />
// 使用 aria-labelledby 属性与表单元素
<label id="username-label">Username</label>
<input aria-labelledby="username-label" />
// 包裹式标签
<label>Username <input /></label>
// 标签文本位于其他子元素中的包裹式标签
<label>
<span>Username</span>
<input />
</label>
// aria-label 属性
// 注意:这不是用户在页面上可见的标签
// 因此输入的目的必须对视觉障碍用户显而易见。
<input aria-label="Username" />Options
exact: booleantext是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。
更多内容请参阅
getByPlaceholder
function getByPlaceholder(
text: string | RegExp,
options?: LocatorOptions,
): Locator创建一个能够找到具有指定 placeholder 属性的元素的定位器。Vitest 会匹配任何具有匹配 placeholder 属性的元素,而不仅仅是 input 元素。
<input placeholder="Username" />
page.getByPlaceholder('Username') // ✅
page.getByPlaceholder('not found') // ❌WARNING
通常情况下,使用 getByLabelText 依赖标签比依赖占位符更好。
Options
exact: booleantext是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。
更多内容请参阅
getByText
function getByText(
text: string | RegExp,
options?: LocatorOptions,
): Locator创建一个能够找到包含指定文本的元素的定位器。文本将与 TextNode 的 nodeValue 或类型为 button 或 reset 的输入值进行匹配。通过文本匹配时总是会规范化空白字符,即使在精确匹配的情况下也是如此。例如,它会将多个空格转换为一个空格,将换行符转换为空格,并忽略前导和尾随的空白字符。
<a href="/about">About ℹ️</a>
page.getByText(/about/i) // ✅
page.getByText('about', { exact: true }) // ❌TIP
此定位器适用于定位非交互式元素。如果你需要定位交互式元素,比如按钮或输入框,建议使用 getByRole。
Options
exact: booleantext是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。
更多内容请参阅
getByTitle
function getByTitle(
text: string | RegExp,
options?: LocatorOptions,
): Locator创建一个能够找到具有指定 title 属性的元素的定位器。与 testing-library 的 getByTitle 不同,Vitest 无法在 SVG 中找到 title 元素。
<span title="Delete" id="2"></span>
page.getByTitle('Delete') // ✅
page.getByTitle('Create') // ❌Options
exact: booleantext是否精确匹配:区分大小写且完全匹配字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空白字符。
更多内容请参阅
getByTestId
function getByTestId(text: string | RegExp): Locator创建一个能够找到与指定测试 ID 属性匹配的元素的定位器。你可以通过 browser.locators.testIdAttribute 配置属性名称。
<div data-testid="custom-element" />
page.getByTestId('custom-element') // ✅
page.getByTestId('non-existing-element') // ❌WARNING
建议仅在其他定位器不适用于你的使用场景时才使用此方法。使用 data-testid 属性并不符合用户实际使用软件的方式,因此如果可能应避免使用。
Options
exact: booleantext是否精确匹配:区分大小写和整个字符串。默认情况下禁用此选项。如果text是正则表达式,则忽略此选项。请注意,精确匹配仍然会修剪空格。
更多内容请参阅
nth
function nth(index: number): Locator此方法返回一个新的定位器,仅匹配多元素查询结果中的特定索引。与 elements()[n] 不同,nth 定位器会重试,直到元素出现为止。
<div aria-label="one"><input/><input/><input/></div>
<div aria-label="two"><input/></div>page.getByRole('textbox').nth(0) // ✅
page.getByRole('textbox').nth(4) // ❌TIP
在使用 nth 之前,你可能会发现使用链式定位器来缩小搜索范围会更有帮助。 有时没有比通过元素位置更好的区分方式;虽然这可能导致测试不稳定,但总比没有强。
page.getByLabel('two').getByRole('input') // ✅ better alternative to page.getByRole('textbox').nth(3)
page.getByLabel('one').getByRole('input') // ❌ too ambiguous
page.getByLabel('one').getByRole('input').nth(1) // ✅ pragmatic compromisefirst
function first(): Locator此方法返回一个新的定位器,仅匹配多元素查询结果中的第一个索引。 它是 nth(0) 的语法糖。
<input/> <input/> <input/>page.getByRole('textbox').first() // ✅last
function last(): Locator此方法返回一个新的定位器,仅匹配多元素查询结果中的最后一个索引。 它是 nth(-1) 的语法糖。
<input/> <input/> <input/>page.getByRole('textbox').last() // ✅and
function and(locator: Locator): Locator此方法创建一个新定位器,需同时匹配父级和传入的定位器。以下示例用于查找具有特定标题的按钮:
page.getByRole('button').and(page.getByTitle('Subscribe'))or
function or(locator: Locator): Locator此方法创建一个新定位器,只需匹配其中一个或同时匹配两个定位器。
WARNING
注意:若定位器匹配到多个元素,调用预期单个元素的方法可能会报错:
<>
<button>Click me</button>
<a href="https://vitest.dev">Error happened!</a>
</>
page.getByRole('button')
.or(page.getByRole('link'))
.click() // ❌ 匹配到多个元素filter
function filter(options: LocatorOptions): Locator此方法会根据选项(例如按文本过滤)缩小定位器范围。可通过链式调用应用多个过滤器。
has
- 类型:
Locator
此选项会限定选择器范围,仅匹配包含符合指定定位器元素的节点。例如对于以下 HTML 结构:
<article>
<div>Vitest</div>
</article>
<article>
<div>Rolldown</div>
</article>我们可以限定定位器范围,仅查找内含 Vitest 文本的 article 元素:
page.getByRole('article').filter({ has: page.getByText('Vitest') }) // ✅WARNING
传入的定位器(示例中的 page.getByText('Vitest'))必须相对于父级定位器(示例中的 page.getByRole('article'))。查询会从父级定位器开始,而非文档根节点。
这意味着不能传入在父级定位器范围之外查询元素的定位器:
page.getByText('Vitest').filter({ has: page.getByRole('article') }) // ❌此示例会失败,因为 article 元素位于包含 Vitest 文本的元素之外。
TIP
此方法支持链式调用以进一步缩小元素定位范围:
page.getByRole('article')
.filter({ has: page.getByRole('button', { name: 'delete row' }) })
.filter({ has: page.getByText('Vitest') })hasNot
- 类型:
Locator
此选项会限定选择器范围,仅匹配不包含指定定位器元素的节点。例如对于以下 HTML 结构:
<article>
<div>Vitest</div>
</article>
<article>
<div>Rolldown</div>
</article>我们可以限定定位器范围,仅查找内部不含 Rolldown 文本的 article 元素:
page.getByRole('article')
.filter({ hasNot: page.getByText('Rolldown') }) // ✅
page.getByRole('article')
.filter({ hasNot: page.getByText('Vitest') }) // ❌WARNING
注意:与 has 选项相同,传入的定位器是针对父级元素而非文档根节点进行查询。
hasText
- 类型:
string | RegExp
此选项会限定选择器范围,仅匹配内部某处包含指定文本的元素。当传入 string 时,匹配不区分大小写且执行子字符串搜索。
<article>
<div>Vitest</div>
</article>
<article>
<div>Rolldown</div>
</article>由于搜索不区分大小写,以下两个定位器将匹配到同一个元素:
page.getByRole('article').filter({ hasText: 'Vitest' }) // ✅
page.getByRole('article').filter({ hasText: 'Vite' }) // ✅hasNotText
- 类型:
string | RegExp
此选项会限定选择器范围,仅匹配内部不包含指定文本的元素。当传入 string 时,匹配不区分大小写且执行子字符串搜索。
Methods
所有方法都是异步的,必须使用 await。自 Vitest 3 起,如果方法没有被 await,测试将会失败。
click
function click(options?: UserEventClickOptions): Promise<void>点击一个元素。你可以使用选项来设置光标位置。
import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).click()dblClick
function dblClick(options?: UserEventDoubleClickOptions): Promise<void>在元素上触发双击事件。你可以使用选项来设置光标位置。
import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).dblClick()tripleClick
function tripleClick(options?: UserEventTripleClickOptions): Promise<void>在元素上触发三连击事件。由于浏览器 API 中没有 tripleclick,此方法会连续触发三次点击事件。
import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).tripleClick()wheel 4.1.0+
function wheel(options: UserEventWheelOptions): Promise<void>Triggers a wheel event on an element. You can use the options to choose a general scroll direction or a precise delta value.
import { page } from 'vitest/browser'
// Scroll right
await page.getByRole('tablist').wheel({ direction: 'right' })clear
function clear(options?: UserEventClearOptions): Promise<void>清除输入元素的内容。
import { page } from 'vitest/browser'
await page.getByRole('textbox', { name: 'Full Name' }).clear()hover
function hover(options?: UserEventHoverOptions): Promise<void>将光标位置移动到选中的元素。
import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).hover()unhover
function unhover(options?: UserEventHoverOptions): Promise<void>这与 locator.hover 的工作方式相同,但将光标移动到 document.body 元素。
import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).unhover()fill
function fill(text: string, options?: UserEventFillOptions): Promise<void>为当前的 input 、textarea 或 contenteditable 元素赋值。
import { page } from 'vitest/browser'
await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean')dropTo
function dropTo(
target: Locator,
options?: UserEventDragAndDropOptions,
): Promise<void>将当前元素拖动到目标位置。
import { page } from 'vitest/browser'
const paris = page.getByText('Paris')
const france = page.getByText('France')
await paris.dropTo(france)selectOptions
function selectOptions(
values:
| HTMLElement
| HTMLElement[]
| Locator
| Locator[]
| string
| string[],
options?: UserEventSelectOptions,
): Promise<void>从 <select> 元素中选择一个或多个值。
import { page } from 'vitest/browser'
const languages = page.getByRole('select', { name: 'Languages' })
await languages.selectOptions('EN')
await languages.selectOptions(['ES', 'FR'])
await languages.selectOptions([
languages.getByRole('option', { name: 'Spanish' }),
languages.getByRole('option', { name: 'French' }),
])screenshot
function screenshot(options: LocatorScreenshotOptions & { save: false }): Promise<string>
function screenshot(options: LocatorScreenshotOptions & { base64: true }): Promise<{
path: string
base64: string
}>
function screenshot(options?: LocatorScreenshotOptions & { base64?: false }): Promise<string>创建与定位器选择器匹配的元素的屏幕截图。
你可以使用 path 选项指定屏幕截图的保存位置,该选项相对于当前测试文件。如果未设置 path 选项,Vitest 将默认使用 browser.screenshotDirectory(默认为 __screenshot__),并结合文件名和测试名来确定屏幕截图的文件路径。
如果你还需要屏幕截图的内容,可以指定 base64: true 以返回屏幕截图的 base64 编码内容以及保存路径。
import { page } from 'vitest/browser'
const button = page.getByRole('button', { name: 'Click Me!' })
const path = await button.screenshot()
const { path, base64 } = await button.screenshot({
path: './button-click-me.png',
base64: true, // 同时返回 base64 字符串
})
// path - 屏幕截图的完整路径
// base64 - 屏幕截图的 base64 编码字符串WARNING 3.2.0+
注意,当 save 设置为 false 时,screenshot 将始终返回 base64 字符串。在此情况下,路径参数也会被忽略。
query
function query(): Element | null此方法返回与定位器选择器匹配的单个元素,如果没有找到元素则返回 null。
如果多个元素匹配该选择器,此方法将抛出错误。如果你需要所有匹配的 DOM 元素,可以使用 .elements();如果你需要匹配选择器的定位器数组,可以使用 .all()。
DANGER
This is an escape hatch for external APIs that do not support locators. Prefer using locator methods instead.
考虑以下 DOM 结构:
<div>Hello <span>World</span></div>
<div>Hello</div>这些定位器将不会抛出错误:
page.getByText('Hello World').query() // ✅ HTMLDivElement
page.getByText('Hello Germany').query() // ✅ null
page.getByText('World').query() // ✅ HTMLSpanElement
page.getByText('Hello', { exact: true }).query() // ✅ HTMLSpanElement这些定位器将抛出错误:
// 返回多个元素
page.getByText('Hello').query() // ❌
page.getByText(/^Hello/).query() // ❌element
function element(): Element此方法返回与定位器选择器匹配的单个元素。
如果 没有元素 匹配该选择器,则会抛出错误。如果你只需要检查元素是否存在,可以考虑使用 .query()。
如果 多个元素 匹配该选择器,则会抛出错误。如果你需要所有匹配的 DOM 元素,可以使用 .elements();如果你需要匹配选择器的定位器数组,可以使用 .all()。
DANGER
This is an escape hatch for external APIs that do not support locators. Prefer using locator methods instead.
It is called automatically when locator is used with expect.element every time the assertion is retried:
await expect.element(page.getByRole('button')).toBeDisabled()考虑以下 DOM 结构:
<div>Hello <span>World</span></div>
<div>Hello Germany</div>
<div>Hello</div>这些定位器将不会抛出错误:
page.getByText('Hello World').element() // ✅
page.getByText('Hello Germany').element() // ✅
page.getByText('World').element() // ✅
page.getByText('Hello', { exact: true }).element() // ✅这些定位器将抛出错误:
// 返回多个元素
page.getByText('Hello').element() // ❌
page.getByText(/^Hello/).element() // ❌
// 不返回任意元素
page.getByText('Hello USA').element() // ❌elements
function elements(): Element[]此方法返回与定位器选择器匹配的元素数组。
此函数不会抛出错误。如果没有元素匹配该选择器,此方法将返回一个空数组。
考虑以下 DOM 结构:
<div>Hello <span>World</span></div>
<div>Hello</div>这些定位器将始终成功:
page.getByText('Hello World').elements() // ✅ [HTMLElement]
page.getByText('World').elements() // ✅ [HTMLElement]
page.getByText('Hello', { exact: true }).elements() // ✅ [HTMLElement]
page.getByText('Hello').elements() // ✅ [HTMLElement, HTMLElement]
page.getByText('Hello USA').elements() // ✅ []all
function all(): Locator[]此方法返回一个与选择器匹配的新定位器数组。
在内部,此方法调用 .elements 并使用 page.elementLocator 包装每个元素。
Properties
selector
selector 是一个字符串,将由浏览器提供程序用于定位元素。Playwright 将使用 playwright 定位器语法,而 preview 和 webdriverio 将使用 CSS。
DANGER
你不应在测试代码中使用此字符串。selector 字符串仅应在使用 Commands API 时使用:
import type { BrowserCommand } from 'vitest/node'
const test: BrowserCommand<string> = function test(context, selector) {
// playwright
await context.iframe.locator(selector).click()
// webdriverio
await context.browser.$(selector).click()
}import { test } from 'vitest'
import { commands, page } from 'vitest/browser'
test('works correctly', async () => {
await commands.test(page.getByText('Hello').selector) // ✅
// vitest will automatically unwrap it to a string
await commands.test(page.getByText('Hello')) // ✅
})length
此属性返回当前定位器匹配的元素数量,等效于调用 locator.elements().length。
参考以下 DOM 结构:
<button>Click Me!</button>
<button>Don't click me!</button>该属性始终会执行成功:
page.getByRole('button').length // ✅ 2
page.getByRole('button', { title: 'Click Me!' }).length // ✅ 1
page.getByRole('alert').length // ✅ 0自定义定位器 3.2.0+ advanced
您可以通过定义定位器工厂对象来扩展内置定位器 API。这些方法将作为 page 对象和所有已创建定位器的方法存在。
当内置定位器无法满足需求时(例如使用自定义 UI 框架时),这些定位器会非常有用。
定位器工厂需要返回一个选择器字符串或定位器本身
TIP
选择器语法与 Playwright 定位器完全一致。建议阅读 Playwright 指南 以更好地理解其工作原理。
import { locators } from 'vitest/browser'
locators.extend({
getByArticleTitle(title) {
return `[data-title="${title}"]`
},
getByArticleCommentsCount(count) {
return `.comments :text("${count} comments")`
},
async previewComments() {
// 可通过 "this" 访问当前定位器
// 注意:若方法在 `page` 上调用,`this` 将指向 `page` 而非定位器!
// not the locator!
if (this !== page) {
await this.click()
}
// ...
}
})
// 使用 TypeScript 时可扩展 LocatorSelectors 接口
// 以在 locators.extend、page.* 和 locator.* 方法中获得自动补全
declare module 'vitest/browser' {
interface LocatorSelectors {
// 若自定义方法返回字符串,将被转换为定位器
// 若返回其他类型,则按原样返回
getByArticleTitle(title: string): Locator
getByArticleCommentsCount(count: number): Locator
// Vitest 将返回 Promise 且不会尝试将其转换为定位器
previewComments(this: Locator): Promise<void>
}
}如果该方法在全局的 page 对象上调用,选择器将作用于整个页面。在下面的例子中,getByArticleTitle 会找到所有具有 data-title 属性且值为 title 的元素。然而,如果该方法在定位器上调用,它将仅作用于该定位器的范围内。
<article data-title="Hello, World!">
Hello, World!
<button id="comments">2 comments</button>
</article>
<article data-title="Hello, Vitest!">
Hello, Vitest!
<button id="comments">0 comments</button>
</article>const articles = page.getByRole('article')
const worldArticle = page.getByArticleTitle('Hello, World!') // ✅
const commentsElement = worldArticle.getByArticleCommentsCount(2) // ✅
const wrongCommentsElement = worldArticle.getByArticleCommentsCount(0) // ❌
const wrongElement = page.getByArticleTitle('No Article!') // ❌
await commentsElement.previewComments() // ✅
await wrongCommentsElement.previewComments() // ❌