···2121 // methods are defined by the provider type augmentation
2222}
23232424-export interface ScreenshotOptions {
2424+export interface ScreenshotOptions extends SelectorOptions {
2525+ /**
2626+ * The HTML element to screeshot.
2727+ */
2528 element?: Element | Locator
2629 /**
2730 * Path relative to the current test file.
···157160 comparatorOptions?: ScreenshotComparatorRegistry[ComparatorName]
158161 screenshotOptions?: Omit<
159162 ScreenshotOptions,
160160- 'element' | 'base64' | 'path' | 'save' | 'type'
163163+ 'element' | 'base64' | 'path' | 'save' | 'type' | 'strict' | 'timeout'
161164 >
162165 /**
163166 * Time to wait until a stable screenshot is found.
···168171 * @default 5000
169172 */
170173 timeout?: number
174174+ /**
175175+ * Allow only a single element with the same locator.
176176+ *
177177+ * If Vitest finds multiple elements, it will throw an error immediately without retrying.
178178+ * @default true
179179+ */
180180+ strict?: boolean
171181}
172182173183export interface UserEvent {
···522532523533export interface FrameLocator extends LocatorSelectors {}
524534535535+export interface SelectorOptions {
536536+ /**
537537+ * How long to wait until a single element is found. By default, this has the same timeout as the test.
538538+ *
539539+ * Vitest will try to find the element in ever increasing intervals: 0, 20, 50, 100, 100, 500.
540540+ */
541541+ timeout?: number
542542+ /**
543543+ * Allow only a single element with the same locator.
544544+ *
545545+ * If Vitest finds multiple elements, it will throw an error immediately without retrying.
546546+ * @default true
547547+ */
548548+ strict?: boolean
549549+}
550550+525551export interface Locator extends LocatorSelectors {
526552 /**
527553 * Selector string that will be used to locate the element by the browser provider.
···689715 * @see {@link https://vitest.dev/api/browser/locators#filter}
690716 */
691717 filter(options: LocatorOptions): Locator
718718+ /**
719719+ * This method returns an element matching the locator.
720720+ * Unlike [`.element()`](https://vitest.dev/api/browser/locators#element),
721721+ * this method will wait and retry until a matching element appears in the DOM,
722722+ * using increasing intervals (0, 20, 50, 100, 100, 500ms).
723723+ *
724724+ * **WARNING:**
725725+ *
726726+ * This is an escape hatch for library authors and 3d-party APIs that do not support locators directly.
727727+ * If you are interacting with the element, use builtin methods instead.
728728+ * @since 4.1.0
729729+ * @see {@link https://vitest.dev/api/browser/locators#findelement}
730730+ */
731731+ findElement(options?: SelectorOptions): Promise<HTMLElement | SVGElement>
692732}
693733694734export interface UserEventTabOptions {
695735 shift?: boolean
696736}
697737698698-export interface UserEventTypeOptions {
738738+export interface UserEventTypeOptions extends SelectorOptions {
699739 skipClick?: boolean
700740 skipAutoClose?: boolean
701741}
+57
docs/api/browser/locators.md
···935935page.getByText('Hello USA').elements() // ✅ []
936936```
937937938938+### findElement <Version>4.1.0</Version> {#findelement}
939939+940940+```ts
941941+function findElement(
942942+ options?: SelectorOptions
943943+): Promise<HTMLElement | SVGElement>
944944+```
945945+946946+::: danger WARNING
947947+This is an escape hatch for cases where you need the raw DOM element — for example, to pass it to a third-party library like FormKit that doesn't accept Vitest locators. If you are interacting with the element yourself, use other [builtin methods](#methods) instead.
948948+:::
949949+950950+This method returns an element matching the locator. Unlike [`.element()`](#element), this method will wait and retry until a matching element appears in the DOM, using increasing intervals (0, 20, 50, 100, 100, 500ms).
951951+952952+If _no element_ is found before the timeout, an error is thrown. By default, the timeout matches the test timeout.
953953+954954+If _multiple elements_ match the selector and `strict` is `true` (the default), an error is thrown immediately without retrying. Set `strict` to `false` to return the first matching element instead.
955955+956956+It accepts options:
957957+958958+- `timeout: number` - How long to wait in milliseconds until at least one element is found. By default, this shares timeout with the test.
959959+- `strict: boolean` - When `true` (default), throws an error if multiple elements match the locator. When `false`, returns the first matching element.
960960+961961+Consider the following DOM structure:
962962+963963+```html
964964+<div>Hello <span>World</span></div>
965965+<div>Hello Germany</div>
966966+<div>Hello</div>
967967+```
968968+969969+These locators will resolve successfully:
970970+971971+```ts
972972+await page.getByText('Hello World').findElement() // ✅ HTMLDivElement
973973+await page.getByText('World').findElement() // ✅ HTMLSpanElement
974974+await page.getByText('Hello Germany').findElement() // ✅ HTMLDivElement
975975+```
976976+977977+These locators will throw an error:
978978+979979+```ts
980980+// multiple elements match, strict mode rejects
981981+await page.getByText('Hello').findElement() // ❌
982982+await page.getByText(/^Hello/).findElement() // ❌
983983+984984+// no matching element before timeout
985985+await page.getByText('Hello USA').findElement() // ❌
986986+```
987987+988988+Using `strict: false` to allow multiple matches:
989989+990990+```ts
991991+// returns the first matching element instead of throwing
992992+await page.getByText('Hello').findElement({ strict: false }) // ✅ HTMLDivElement
993993+```
994994+938995### all
939996940997```ts
···11-import type { Locator, UserEventWheelDeltaOptions, UserEventWheelOptions } from 'vitest/browser'
11+import type { Locator, SelectorOptions, UserEventWheelDeltaOptions, UserEventWheelOptions } from 'vitest/browser'
22import type { BrowserRPC } from '../client'
33import { getBrowserState, getWorkerState } from '../utils'
44-55-const provider = getBrowserState().provider
6475/* @__NO_SIDE_EFFECTS__ */
86export function convertElementToCssSelector(element: Element): string {
···130128 () =>
131129 rpc.triggerCommand<T>(sessionId, command, filepath, args).catch((err) => {
132130 // rethrow an error to keep the stack trace in browser
133133- // const clientError = new Error(err.message)
134131 clientError.message = err.message
135132 clientError.name = err.name
136133 clientError.stack = clientError.stack?.replace(clientError.message, err.message)
···142139143140const now = Date.now
144141145145-export function processTimeoutOptions<T extends { timeout?: number }>(options_?: T): T | undefined {
142142+export function processTimeoutOptions<T extends { timeout?: number }>(options_: T | undefined): T | undefined {
146143 if (
147144 // if timeout is set, keep it
148145 (options_ && options_.timeout != null)
149149- // timeout can only be set for playwright commands
150150- || provider !== 'playwright'
151146 ) {
152147 return options_
153148 }
···209204 return `${JSON.stringify(text)}${exact ? 's' : 'i'}`
210205}
211206212212-export function convertToSelector(elementOrLocator: Element | Locator): string {
207207+const provider = getBrowserState().provider
208208+const kElementLocator = Symbol.for('$$vitest:locator-resolved')
209209+210210+export async function convertToSelector(elementOrLocator: Element | Locator, options?: SelectorOptions): Promise<string> {
213211 if (!elementOrLocator) {
214212 throw new Error('Expected element or locator to be defined.')
215213 }
···217215 return convertElementToCssSelector(elementOrLocator)
218216 }
219217 if (isLocator(elementOrLocator)) {
220220- return elementOrLocator.selector
218218+ if (provider === 'playwright' || kElementLocator in elementOrLocator) {
219219+ return elementOrLocator.selector
220220+ }
221221+ const element = await elementOrLocator.findElement(options)
222222+ return convertElementToCssSelector(element)
221223 }
222224 throw new Error('Expected element or locator to be an instance of Element or Locator.')
223225}