定位器
定位器用于表示单个或多个元素。每个定位器都由一个选择器字符串定义。Vitest 提供了便捷的方法在后台生成选择器,从而简化了这一过程。
定位器 API 使用 Playwright 的定位器 的一个分支,称为 Ivya。然而,Vitest 为每个 provider 提供此 API,而不仅限于 Playwright。
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-*
属性为本身已具有隐式角色的内置元素提供角色。
选项
exact: boolean
name
是否精确匹配:区分大小写且匹配整个字符串。默认禁用。如果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
是否应包含或不包含禁用元素。默认情况下,不应用此筛选器。请注意,与其他属性不同,
disabled
状态是可继承的。有关更多信息,请参阅
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'); // ❌
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果text
是正则表达式,则此选项将被忽略。请注意,精确匹配时仍会去除空格。
另请参阅
getByLabelText
function getByLabelText(
text: string | RegExp,
options?: LocatorOptions
): Locator;
创建一个定位器,能够找到具有关联标签的元素。
page.getByLabelText('Username')
定位器将找到以下示例中的所有输入:
// `label` 标签和表单元素 `id` 之间的 `for`/`htmlFor` 关系
<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" />
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果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
的标签,而不是仅依赖占位符。
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果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
。
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果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'); // ❌
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果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
属性不符合你的软件使用方式,应尽可能避免。
选项
exact: boolean
text
是否精确匹配:区分大小写和整个字符串。默认禁用。如果text
是正则表达式,则此选项将被忽略。请注意,精确匹配时仍会去除空格。
另请参阅
nth
function nth(index: number): Locator;
此方法返回一个新的定位器,该定位器仅匹配多元素查询结果中的特定索引。它是零基的,nth(0)
选择第一个元素。与 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'); // ✅ 优于 page.getByRole('textbox').nth(3)
page.getByLabel('one').getByRole('input'); // ❌ 太模糊
page.getByLabel('one').getByRole('input').nth(1); // ✅ 务实的折衷
first
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
时,匹配不区分大小写并搜索子字符串。
方法
所有方法都是异步的,必须被等待。从 Vitest 3 开始,如果方法未被等待,测试将失败。
click
function click(options?: UserEventClickOptions): Promise<void>;
点击一个元素。你可以使用选项设置光标位置。
import { page } from '@vitest/browser/context';
await page.getByRole('img', { name: 'Rose' }).click();
dblClick
function dblClick(options?: UserEventDoubleClickOptions): Promise<void>;
触发元素的双击事件。你可以使用选项设置光标位置。
import { page } from '@vitest/browser/context';
await page.getByRole('img', { name: 'Rose' }).dblClick();
tripleClick
function tripleClick(options?: UserEventTripleClickOptions): Promise<void>;
触发元素的三击事件。由于浏览器 API 中没有 tripleclick
,此方法将连续触发三次点击事件。
import { page } from '@vitest/browser/context';
await page.getByRole('img', { name: 'Rose' }).tripleClick();
clear
function clear(options?: UserEventClearOptions): Promise<void>;
清除输入元素内容。
import { page } from '@vitest/browser/context';
await page.getByRole('textbox', { name: 'Full Name' }).clear();
hover
function hover(options?: UserEventHoverOptions): Promise<void>;
将光标位置移动到选定的元素。
import { page } from '@vitest/browser/context';
await page.getByRole('img', { name: 'Rose' }).hover();
unhover
function unhover(options?: UserEventHoverOptions): Promise<void>;
这与 locator.hover
的工作方式相同,但将光标移动到 document.body
元素上。
import { page } from '@vitest/browser/context';
await page.getByRole('img', { name: 'Rose' }).unhover();
fill
function fill(text: string, options?: UserEventFillOptions): Promise<void>;
设置当前 input
、textarea
或 contenteditable
元素的值。
import { page } from '@vitest/browser/context';
await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean');
dropTo
function dropTo(
target: Locator,
options?: UserEventDragAndDropOptions
): Promise<void>;
将当前元素拖动到目标位置。
import { page } from '@vitest/browser/context';
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/context';
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
以将其与屏幕截图保存的文件路径一并返回。
import { page } from '@vitest/browser/context';
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 编码字符串
警告 3.2.0+
请注意,如果 save
设置为 false
,screenshot
将始终返回 base64 字符串。 在这种情况下,path
也将被忽略。
query
function query(): Element | null;
此方法返回与定位器选择器匹配的单个元素,如果未找到元素则返回 null
。
如果多个元素匹配选择器,此方法将抛出错误。当你需要所有匹配的 DOM 元素时,请使用 .elements()
;当你需要匹配该选择器的定位器数组时,请使用 .all()
。
考虑以下 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()
。
TIP
如果你需要将其传递给外部库,此方法会很有用。当定位器与 expect.element
一起使用时,每次断言重试时都会自动调用此方法:
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').element(); // ✅ [HTMLElement, HTMLElement]
page.getByText('Hello USA').elements(); // ✅ []
all
function all(): Locator[];
此方法返回与选择器匹配的新定位器数组。
在内部,此方法调用 .elements
并使用 page.elementLocator
包装每个元素。
属性
selector
selector
是一个字符串,浏览器提供程序将使用它来定位元素。Playwright 将使用 playwright
定位器语法,而 preview
和 webdriverio
将使用 CSS 选择器。
DANGER
你不应该在测试代码中直接使用此字符串。selector
字符串只应在处理命令 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/context';
test('works correctly', async () => {
await commands.test(page.getByText('Hello').selector); // ✅
// vitest 会自动将其解包为字符串
await commands.test(page.getByText('Hello')); // ✅
});
自定义定位器 3.2.0+ 高级
你可以通过定义定位器工厂对象来扩展内置定位器 API。这些方法将作为 page
对象和任何已创建的定位器上的方法存在。
如果内置定位器不够用,这些定位器会很有用。例如,当你为 UI 使用自定义框架时尤其如此。
定位器工厂需要返回一个选择器字符串或一个定位器本身。
TIP
选择器语法与 Playwright 定位器相同。请阅读他们的指南以更好地理解如何使用它们。
import { locators } from '@vitest/browser/context';
locators.extend({
getByArticleTitle(title) {
return `[data-title="${title}"]`;
},
getByArticleCommentsCount(count) {
return `.comments :text("${count} comments")`;
},
async previewComments() {
// 你可以通过“this”访问当前定位器
// 请注意,如果该方法是在 `page` 上调用的,则 `this` 将是 `page`,
// 而不是当前定位器!
if (this !== page) {
await this.click();
}
// ...
},
});
// 如果你正在使用 TypeScript,你可以扩展 LocatorSelectors 接口
// 以便在 locators.extend、page.* 和 locator.* 方法中获得自动补全
declare module '@vitest/browser/context' {
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(); // ❌