diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fe838439fcc4..0f14cb7be3a4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -185,6 +185,11 @@ export default ({ mode }: { mode: string }) => { link: '/guide/browser/interactivity-api', docFooterText: 'Interactivity API | Browser Mode', }, + { + text: 'Locators', + link: '/guide/browser/locators', + docFooterText: 'Locators | Browser Mode', + }, { text: 'Assertion API', link: '/guide/browser/assertion-api', diff --git a/docs/config/index.md b/docs/config/index.md index 3034f22a847d..5d7bf73595be 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1667,6 +1667,17 @@ Should Vitest UI be injected into the page. By default, injects UI iframe during Default iframe's viewport. +#### browser.locators {#browser-locators} + +Options for built-in [browser locators](/guide/browser/locators). + +##### browser.locators.testIdAttribute + +- **Type:** `string` +- **Default:** `data-testid` + +Attribute used to find elements with `getByTestId` locator. + #### browser.screenshotDirectory {#browser-screenshotdirectory} - **Type:** `string` diff --git a/docs/guide/browser/context.md b/docs/guide/browser/context.md index eed2828a333d..5780534c0288 100644 --- a/docs/guide/browser/context.md +++ b/docs/guide/browser/context.md @@ -78,9 +78,32 @@ export const page: { base64: string }> screenshot(options?: ScreenshotOptions): Promise + /** + * Extend default `page` object with custom methods. + */ + extend(methods: Partial): BrowserPage + /** + * Wrap an HTML element in a `Locator`. When querying for elements, the search will always return this element. + */ + elementLocator(element: Element): Locator + + /** + * Locator APIs. See its documentation for more details. + */ + getByRole(role: ARIARole | string, options?: LocatorByRoleOptions): Locator + getByLabelText(text: string | RegExp, options?: LocatorOptions): Locator + getByTestId(text: string | RegExp): Locator + getByAltText(text: string | RegExp, options?: LocatorOptions): Locator + getByPlaceholder(text: string | RegExp, options?: LocatorOptions): Locator + getByText(text: string | RegExp, options?: LocatorOptions): Locator + getByTitle(text: string | RegExp, options?: LocatorOptions): Locator } ``` +::: tip +The `getBy*` API is explained at [Locators API](/guide/browser/locators). +::: + ## `cdp` The `cdp` export returns the current Chrome DevTools Protocol session. It is mostly useful to library authors to build tools on top of it. diff --git a/docs/guide/browser/interactivity-api.md b/docs/guide/browser/interactivity-api.md index 53c06704dbf1..66bd614babba 100644 --- a/docs/guide/browser/interactivity-api.md +++ b/docs/guide/browser/interactivity-api.md @@ -64,7 +64,7 @@ This behaviour is more useful because we do not emulate the keyboard, we actuall ## userEvent.click -- **Type:** `(element: Element, options?: UserEventClickOptions) => Promise` +- **Type:** `(element: Element | Locator, options?: UserEventClickOptions) => Promise` Click on an element. Inherits provider's options. Please refer to your provider's documentation for detailed explanation about how this method works. @@ -87,7 +87,7 @@ References: ## userEvent.dblClick -- **Type:** `(element: Element, options?: UserEventDoubleClickOptions) => Promise` +- **Type:** `(element: Element | Locator, options?: UserEventDoubleClickOptions) => Promise` Triggers a double click event on an element. @@ -112,7 +112,7 @@ References: ## userEvent.tripleClick -- **Type:** `(element: Element, options?: UserEventTripleClickOptions) => Promise` +- **Type:** `(element: Element | Locator, options?: UserEventTripleClickOptions) => Promise` Triggers a triple click event on an element. Since there is no `tripleclick` in browser api, this method will fire three click events in a row, and so you must check [click event detail](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#usage_notes) to filter the event: `evt.detail === 3`. @@ -144,7 +144,7 @@ References: ## userEvent.fill -- **Type:** `(element: Element, text: string) => Promise` +- **Type:** `(element: Element | Locator, text: string) => Promise` Set a value to the `input/textarea/conteneditable` field. This will remove any existing text in the input before setting the new value. @@ -234,7 +234,7 @@ References: ## userEvent.type -- **Type:** `(element: Element, text: string, options?: UserEventTypeOptions) => Promise` +- **Type:** `(element: Element | Locator, text: string, options?: UserEventTypeOptions) => Promise` ::: warning If you don't rely on [special characters](https://testing-library.com/docs/user-event/keyboard) (e.g., `{shift}` or `{selectall}`), it is recommended to use [`userEvent.fill`](#userevent-fill) instead. @@ -267,7 +267,7 @@ References: ## userEvent.clear -- **Type:** `(element: Element) => Promise` +- **Type:** `(element: Element | Locator) => Promise` This method clears the input element content. @@ -294,7 +294,7 @@ References: ## userEvent.selectOptions -- **Type:** `(element: Element, values: HTMLElement | HTMLElement[] | string | string[], options?: UserEventSelectOptions) => Promise` +- **Type:** `(element: Element | Locator, values: HTMLElement | HTMLElement[] | Locator | Locator[] | string | string[], options?: UserEventSelectOptions) => Promise` The `userEvent.selectOptions` allows selecting a value in a ` + + +
+ +``` + +You can locate each element by its implicit role: + +```ts +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 +Roles are matched by string equality, without inheriting from the ARIA role hierarchy. As a result, querying a superclass role like `checkbox` will not include elements with a subclass role like `switch`. + +By default, many semantic elements in HTML have a role; for example, `` has the "radio" role. Non-semantic elements in HTML do not have a role; `
` and `` without added semantics return `null`. The `role` attribute can provide semantics. + +Providing roles via `role` or `aria-*` attributes to built-in elements that already have an implicit role is **highly discouraged** by ARIA guidelines. +::: + +##### Options + +- `exact: boolean` + + Whether the `name` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `name` is a regular expression. Note that exact match still trims whitespace. + + ```tsx + + + page.getByRole('button', { name: 'hello world' }) // ✅ + page.getByRole('button', { name: 'hello world', exact: true }) // ❌ + page.getByRole('button', { name: 'Hello World', exact: true }) // ✅ + ``` + +- `checked: boolean` + + Should checked elements (set by `aria-checked` or ``) be included or not. By default, the filter is not applied. + + See [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked) for more information + + ```tsx + <> + + + page.getByRole('button', { name: 'Click Me!' }) // ✅ + page.getByRole('button', { name: 'click me!' }) // ✅ + page.getByRole('button', { name: 'Click Me?' }) // ❌ + ``` + +- `pressed: boolean` + + Should pressed elements be included or not. By default, the filter is not applied. + + See [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed) for more information + + ```tsx + + + page.getByRole('button', { pressed: true }) // ✅ + page.getByRole('button', { pressed: false }) // ❌ + ``` + +- `selected: boolean` + + Should selected elements be included or not. By default, the filter is not applied. + + See [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected) for more information + + ```tsx + + + page.getByRole('button', { selected: true }) // ✅ + page.getByRole('button', { selected: false }) // ❌ + ``` + +##### See also + +- [List of ARIA roles at MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) +- [List of ARIA roles at w3.org](https://www.w3.org/TR/wai-aria-1.2/#role_definitions) +- [testing-library's `ByRole`](https://testing-library.com/docs/queries/byrole/) + +## getByAltText + +- **Type:** `(text: string | RegExp, options?: LocatorOptions) => Locator` + +Creates a locator capable of finding an element with an `alt` attribute that matches the text. Unlike testing-library's implementation, Vitest will match any element that has a matching `alt` attribute. + +```tsx +Incredibles 2 Poster + +page.getByAltText(/incredibles.*? poster/i) // ✅ +page.getByAltText('non existing alt text') // ❌ +``` + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByAltText`](https://testing-library.com/docs/queries/byalttext/) + +## getByLabelText + +- **Type:** `(text: string | RegExp, options?: LocatorOptions) => Locator` + +Creates a locator capable of finding an element that has an assosiated label. + +The `page.getByLabelText('Username')` locator will find every input in the example bellow: + +```html +// for/htmlFor relationship between label and form element id + + + +// The aria-labelledby attribute with form elements + + + +// Wrapper labels + + +// Wrapper labels where the label text is in another child element + + +// aria-label attributes +// Take care because this is not a label that users can see on the page, +// so the purpose of your input must be obvious to visual users. + +``` + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByLabelText`](https://testing-library.com/docs/queries/bylabeltext/) + +## getByPlaceholder + +- **Type:** `(text: string | RegExp, options?: LocatorOptions) => Locator` + +Creates a locator capable of finding an element that has the specified `placeholder` attribute. Vitest will match any element that has a matching `placeholder` attribute, not just `input`. + +```tsx + + +page.getByPlaceholder('Username') // ✅ +page.getByPlaceholder('not found') // ❌ +``` + +::: warning +It is generally better to rely on a label using [`getByLabelText`](#getbylabeltext) than a placeholder. +::: + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByPlaceholderText`](https://testing-library.com/docs/queries/byplaceholdertext/) + +## getByText + +- **Type:** `(text: string | RegExp, options?: LocatorOptions) => Locator` + +Creates a locator capable of finding an element that contains the specified text. The text will be matched against TextNode's [`nodeValue`](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeValue) or input's value if the type is `button` or `reset`. Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace. + +```tsx +About ℹ️ + +page.getByText(/about/i) // ✅ +page.getByText('about', { exact: true }) // ❌ +``` + +::: tip +This locator is useful for locating non-interactive elements. If you need to locate an interactive element, like a button or an input, prefer [`getByRole`](#getbyrole). +::: + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByText`](https://testing-library.com/docs/queries/bytext/) + +## getByTitle + +- **Type:** `(text: string | RegExp, options?: LocatorOptions) => Locator` + +Creates a locator capable of finding an element that has the specified `title` attribute. Unlike testing-library's `getByTitle`, Vitest cannot find `title` elements within an SVG. + +```tsx + + +page.getByTitle('Delete') // ✅ +page.getByTitle('Create') // ❌ +``` + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByTitle`](https://testing-library.com/docs/queries/bytitle/) + +## getByTestId + +- **Type:** `(text: string | RegExp) => Locator` + +Creates a locator capable of finding an element that matches the specified test id attribute. You can configure the attribute name with [`browser.locators.testIdAttribute`](/config/#browser-locators-testidattribute). + +```tsx +
+ +page.getByTestId('custom-element') // ✅ +page.getByTestId('non-existing-element') // ❌ +``` + +::: warning +It is recommended to use this only after the other locators don't work for your use case. Using `data-testid` attributes does not resemble how your software is used and should be avoided if possible. +::: + +#### Options + +- `exact: boolean` + + Whether the `text` is matched exactly: case-sensetive and whole-string. Disabled by default. This option is ignored if `text` is a regular expression. Note that exact match still trims whitespace. + +#### See also + +- [testing-library's `ByTestId`](https://testing-library.com/docs/queries/bytestid/) + +## Methods + +### click + +- **Type:** `(options?: UserEventClickOptions) => Promise` + +Click on an element. You can use the options to set the cursor position. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('img', { name: 'Rose' }).click() +``` + +- [See more at `userEvent.click`](/guide/browser/interactivity-api#userevent-click) + +### dblClick + +- **Type:** `(options?: UserEventClickOptions) => Promise` + +Triggers a double click event on an element. You can use the options to set the cursor position. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('img', { name: 'Rose' }).dblClick() +``` + +- [See more at `userEvent.dblClick`](/guide/browser/interactivity-api#userevent-dblclick) + +### tripleClick + +- **Type:** `(options?: UserEventClickOptions) => Promise` + +Triggers a triple click event on an element. Since there is no `tripleclick` in browser api, this method will fire three click events in a row. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('img', { name: 'Rose' }).tripleClick() +``` + +- [See more at `userEvent.tripleClick`](/guide/browser/interactivity-api#userevent-tripleclick) + +### clear + +- **Type:** `() => Promise` + +Clears the input element content. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('textbox', { name: 'Full Name' }).clear() +``` + +- [See more at `userEvent.clear`](/guide/browser/interactivity-api#userevent-clear) + +### hover + +- **Type:** `(options?: UserEventHoverOptions) => Promise` + +Moves the cursor position to the selected element. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('img', { name: 'Rose' }).hover() +``` + +- [See more at `userEvent.hover`](/guide/browser/interactivity-api#userevent-hover) + +### unhover + +- **Type:** `(options?: UserEventHoverOptions) => Promise` + +This works the same as [`locator.hover`](#hover), but moves the cursor to the `document.body` element instead. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('img', { name: 'Rose' }).unhover() +``` + +- [See more at `userEvent.unhover`](/guide/browser/interactivity-api#userevent-unhover) + +### fill + +- **Type:** `(text: string, options?: UserEventFillOptions) => Promise` + +Sets the value of the current `input`, `textarea` or `conteneditable` element. + +```ts +import { page } from '@vitest/browser/context' + +await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean') +``` + +- [See more at `userEvent.fill`](/guide/browser/interactivity-api#userevent-fill) + +### dropTo + +- **Type:** `(target: Locator, options?: UserEventDragAndDropOptions) => Promise` + +Drags the current element to the target location. + +```ts +import { page } from '@vitest/browser/context' + +const paris = page.getByText('Paris') +const france = page.getByText('France') + +await paris.dropTo(france) +``` + +- [See more at `userEvent.dragAndDrop`](/guide/browser/interactivity-api#userevent-draganddrop) + +### selectOptions + +- **Type:** `(values: HTMLElement | HTMLElement[] | string | string[], options?: UserEventSelectOptions) => Promise` + +Choose one or more values from a ``) be included or not. By default, the filter is not applied. + * + * See [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked) for more information + */ + checked?: boolean + /** + * Should disabled elements be included or not. By default, the filter is not applied. Note that unlike other attributes, `disable` state is inherited. + * + * See [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled) for more information + */ + disabled?: boolean + /** + * Should expanded elements be included or not. By default, the filter is not applied. + * + * See [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded) for more information + */ + expanded?: boolean + /** + * Should elements that are [normally excluded](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion) from the accessibility tree be queried. By default, only non-hidden elements are matched by role selector. + * + * Note that roles `none` and `presentation` are always included. + * @default false + */ + includeHidden?: boolean + /** + * A number attribute that is usually present for `heading`, `listitem`, `row`, `treeitem` roles with default values for `

-

` elements. By default, the filter is not applied. + * + * See [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level) for more information + */ + level?: number + /** + * Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is + * case-insensitive and searches for a substring, use `exact` to control this behavior. + */ + name?: string | RegExp + /** + * Should pressed elements be included or not. By default, the filter is not applied. + * + * See [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed) for more information + */ + pressed?: boolean + /** + * Should selected elements be included or not. By default, the filter is not applied. + * + * See [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected) for more information + */ + selected?: boolean +} + +interface LocatorScreenshotOptions extends Omit {} + +interface LocatorSelectors { + /** + * Creates a way to locate an element by its [ARIA role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles), [ARIA attributes](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes) and [accessible name](https://developer.mozilla.org/en-US/docs/Glossary/Accessible_name). + * @see {@link https://vitest.dev/guide/browser/locators#getbyrole} + */ + getByRole(role: ARIARole | ({} & string), options?: LocatorByRoleOptions): Locator + /** + * @see {@link https://vitest.dev/guide/browser/locators#getbylabeltext} + */ + getByLabelText(text: string | RegExp, options?: LocatorOptions): Locator + /** + * Creates a locator capable of finding an element with an `alt` attribute that matches the text. Unlike testing-library's implementation, Vitest will match any element that has an `alt` attribute. + * @see {@link https://vitest.dev/guide/browser/locators#getbyalttext} + */ + getByAltText(text: string | RegExp, options?: LocatorOptions): Locator + /** + * Creates a locator capable of finding an element that has the specified placeholder text. Vitest will match any element that has a matching `placeholder` attribute, not just `input`. + * @see {@link https://vitest.dev/guide/browser/locators#getbyplaceholder} + */ + getByPlaceholder(text: string | RegExp, options?: LocatorOptions): Locator + /** + * Creates a locator capable of finding an element that contains the specified text. The text will be matched against TextNode's [`nodeValue`](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeValue) or input's value if the type is `button` or `reset`. + * Matching by text always normalizes whitespace, even with exact match. + * For example, it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace. + * @see {@link https://vitest.dev/guide/browser/locators#getbytext} + */ + getByText(text: string | RegExp, options?: LocatorOptions): Locator + /** + * Creates a locator capable of finding an element that has the specified `title` attribute. Unlike testing-library's `getByTitle`, Vitest cannot find `title` elements within an SVG. + * @see {@link https://vitest.dev/guide/browser/locators#getbytitle} + */ + getByTitle(text: string | RegExp, options?: LocatorOptions): Locator + /** + * Creates a locator capable of finding an element that matches the specified test id attribute. You can configure the attribute name with [`browser.locators.testIdAttribute`](/config/#browser-locators-testidattribute). + * @see {@link https://vitest.dev/guide/browser/locators#getbytestid} + */ + getByTestId(text: string | RegExp): Locator +} + +export interface Locator extends LocatorSelectors { + /** + * Click on an element. You can use the options to set the cursor position. + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-click} + */ + click(options?: UserEventClickOptions): Promise + /** + * Triggers a double click event on an element. You can use the options to set the cursor position. + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-dblclick} + */ + dblClick(options?: UserEventDoubleClickOptions): Promise + /** + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-tripleclick} + */ + tripleClick(options?: UserEventTripleClickOptions): Promise + /** + * Clears the input element content + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-clear} + */ + clear(): Promise + /** + * Moves the cursor position to the selected element + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-hover} + */ + hover(options?: UserEventHoverOptions): Promise + /** + * This works the same as `locator.hover`, but moves the cursor to the `document.body` element instead. + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-unhover} + */ + unhover(options?: UserEventHoverOptions): Promise + /** + * Sets the value of the current `input`, `textarea` or `conteneditable` element. + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-fill} + */ + fill(text: string, options?: UserEventFillOptions): Promise + /** + * Drags the current element to the target location. + * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-dropto} + */ + dropTo(target: Locator, options?: UserEventDragAndDropOptions): Promise + /** + * Choose one or more values from a `