···142142 * @see {@link https://webdriver.io/docs/api/element/clearValue} WebdriverIO API
143143 * @see {@link https://testing-library.com/docs/user-event/utility/#clear} testing-library API
144144 */
145145- clear: (element: Element | Locator) => Promise<void>
145145+ clear: (element: Element | Locator, options?: UserEventClearOptions) => Promise<void>
146146 /**
147147 * Sends a `Tab` key event. Uses provider's API under the hood.
148148 * @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API
···171171 * @see {@link https://playwright.dev/docs/api/class-locator#locator-set-input-files} Playwright API
172172 * @see {@link https://testing-library.com/docs/user-event/utility#upload} testing-library API
173173 */
174174- upload: (element: Element | Locator, files: File | File[] | string | string[]) => Promise<void>
174174+ upload: (element: Element | Locator, files: File | File[] | string | string[], options?: UserEventUploadOptions) => Promise<void>
175175 /**
176176 * Copies the selected content.
177177 * @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API
···218218export interface UserEventHoverOptions {}
219219export interface UserEventSelectOptions {}
220220export interface UserEventClickOptions {}
221221+export interface UserEventClearOptions {}
221222export interface UserEventDoubleClickOptions {}
222223export interface UserEventTripleClickOptions {}
223224export interface UserEventDragAndDropOptions {}
225225+export interface UserEventUploadOptions {}
224226225227export interface LocatorOptions {
226228 /**
···358360 * Clears the input element content
359361 * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-clear}
360362 */
361361- clear(): Promise<void>
363363+ clear(options?: UserEventClearOptions): Promise<void>
362364 /**
363365 * Moves the cursor position to the selected element
364366 * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-hover}
···391393 * Change a file input element to have the specified files. Uses provider's API under the hood.
392394 * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-upload}
393395 */
394394- upload(files: File | File[] | string | string[]): Promise<void>
396396+ upload(files: File | File[] | string | string[], options?: UserEventUploadOptions): Promise<void>
395397396398 /**
397399 * Make a screenshot of an element matching the locator.
+2-1
packages/browser/matchers.d.ts
···1919 interface ExpectStatic {
2020 /**
2121 * `expect.element(locator)` is a shorthand for `expect.poll(() => locator.element())`.
2222- * You can set default timeout via `expect.poll.timeout` config.
2222+ * You can set default timeout via `expect.poll.timeout` option in the config.
2323+ * @see {@link https://vitest.dev/api/expect#poll}
2324 */
2425 element: <T extends Element | Locator>(element: T, options?: ExpectPollOptions) => PromisifyDomAssertion<Awaited<Element | null>>
2526 }
···292292## userEvent.clear
293293294294```ts
295295-function clear(element: Element | Locator): Promise<void>
295295+function clear(element: Element | Locator, options?: UserEventClearOptions): Promise<void>
296296```
297297298298This method clears the input element content.
···451451function upload(
452452 element: Element | Locator,
453453 files: string[] | string | File[] | File,
454454+ options?: UserEventUploadOptions,
454455): Promise<void>
455456```
456457
+2-2
docs/guide/browser/locators.md
···33outline: [2, 3]
44---
5566-# Locators <Version>2.1.0</Version>
66+# Locators
7788A locator is a representation of an element or a number of elements. Every locator is defined by a string called a selector. Vitest abstracts this selector by providing convenient methods that generate those selectors behind the scenes.
99···505505### clear
506506507507```ts
508508-function clear(): Promise<void>
508508+function clear(options?: UserEventClearOptions): Promise<void>
509509```
510510511511Clears the input element content.
···175175176176 expect(stderr).toMatch(/bundled-lib\/src\/b.js:2:(8|18)/)
177177 expect(stderr).toMatch(/bundled-lib\/src\/index.js:5:(15|17)/)
178178- expect(stderr).toMatch(/test\/failing.test.ts:25:(2|8)/)
178178+179179+ if (provider === 'playwright') {
180180+ // page.getByRole('code').click()
181181+ expect(stderr).toContain('locator.click: Timeout')
182182+ // playwright error is proxied from the server to the client and back correctly
183183+ expect(stderr).toContain('waiting for locator(\'[data-vitest="true"]\').contentFrame().getByRole(\'code\')')
184184+ expect(stderr).toMatch(/test\/failing.test.ts:27:(33|39)/)
185185+ // await expect.element().toBeVisible()
186186+ expect(stderr).toContain('Cannot find element with locator: getByRole(\'code\')')
187187+ expect(stderr).toMatch(/test\/failing.test.ts:31:(49|61)/)
188188+ }
189189+190190+ // index() is called from a bundled file
191191+ expect(stderr).toMatch(/test\/failing.test.ts:36:(2|8)/)
179192 })
180193181194 test('popup apis should log a warning', () => {
···210223 const { stderr } = await runBrowserTests({
211224 root: './fixtures/timeout',
212225 })
213213- expect(stderr).toContain('Matcher did not succeed in 500ms')
226226+ expect(stderr).toContain('Matcher did not succeed in time.')
214227 if (provider === 'playwright') {
215228 expect(stderr).toContain('locator.click: Timeout 500ms exceeded.')
216229 expect(stderr).toContain('locator.click: Timeout 345ms exceeded.')
+13-2
test/browser/test/failing.test.ts
···11-import { page } from '@vitest/browser/context'
11+import { page, server } from '@vitest/browser/context'
22import { index } from '@vitest/bundled-lib'
33-import { expect, it } from 'vitest'
33+import { describe, expect, it } from 'vitest'
44import { throwError } from '../src/error'
5566document.body.innerHTML = `
···1919 page.getByRole('button').dblClick()
2020 page.getByRole('button').click()
2121 page.getByRole('button').tripleClick()
2222+})
2323+2424+describe.runIf(server.provider === 'playwright')('timeouts are failing correctly', () => {
2525+ it('click on non-existing element fails', async () => {
2626+ await new Promise(r => setTimeout(r, 100))
2727+ await page.getByRole('code').click()
2828+ }, 1000)
2929+3030+ it('expect.element on non-existing element fails', async () => {
3131+ await expect.element(page.getByRole('code')).toBeVisible()
3232+ }, 1000)
2233})
23342435it('correctly prints error from a bundled file', () => {
+5-5
test/core/test/expect-poll.test.ts
···3737 await expect(async () => {
3838 await expect.poll(() => false, { timeout: 100, interval: 10 }).toBe(true)
3939 }).rejects.toThrowError(expect.objectContaining({
4040- message: 'Matcher did not succeed in 100ms',
4040+ message: 'Matcher did not succeed in time.',
4141 stack: expect.stringContaining('expect-poll.test.ts:38:68'),
4242 cause: expect.objectContaining({
4343 message: 'expected false to be true // Object.is equality',
···6060 vi.useFakeTimers()
6161 await expect(async () => {
6262 await expect.poll(() => false, { timeout: 100 }).toBe(true)
6363- }).rejects.toThrowError('Matcher did not succeed in 100ms')
6363+ }).rejects.toThrowError('Matcher did not succeed in time.')
6464 vi.useRealTimers()
6565 const diff = Date.now() - now
6666 expect(diff >= 100).toBe(true)
···9191 await expect(() =>
9292 expect.poll(() => 1, { timeout: 100, interval: 10 }).not.toBeDefined(),
9393 ).rejects.toThrowError(expect.objectContaining({
9494- message: 'Matcher did not succeed in 100ms',
9494+ message: 'Matcher did not succeed in time.',
9595 cause: expect.objectContaining({
9696 message: 'expected 1 to be undefined',
9797 }),
···100100 await expect(() =>
101101 expect.poll(() => undefined, { timeout: 100, interval: 10 }).toBeDefined(),
102102 ).rejects.toThrowError(expect.objectContaining({
103103- message: 'Matcher did not succeed in 100ms',
103103+ message: 'Matcher did not succeed in time.',
104104 cause: expect.objectContaining({
105105 message: 'expected undefined to be defined',
106106 }),
···140140 await expect(async () => {
141141 await expect.poll(fn, { interval: 10, timeout: 100 }).toBe(1)
142142 }).rejects.toThrowError(expect.objectContaining({
143143- message: 'Matcher did not succeed in 100ms',
143143+ message: 'Matcher did not succeed in time.',
144144 cause: expect.objectContaining({
145145 // makes sure cause message reflects the last attempt value
146146 message: 'expected 3 to be 1 // Object.is equality',
···11import type { SerializedConfig, WorkerGlobalState } from 'vitest'
22-import type { BrowserRPC } from './client'
22+import type { CommandsManager } from './tester/utils'
3344export async function importId(id: string): Promise<any> {
55 const name = `/@id/${id}`.replace(/\\/g, '/')
···2626 return getBrowserState().config
2727}
28282929-export function ensureAwaited<T>(promise: () => Promise<T>): Promise<T> {
2929+export function ensureAwaited<T>(promise: (error?: Error) => Promise<T>): Promise<T> {
3030 const test = getWorkerState().current
3131 if (!test || test.type !== 'test') {
3232 return promise()
···4848 return {
4949 then(onFulfilled, onRejected) {
5050 awaited = true
5151- return (promiseResult ||= promise()).then(onFulfilled, onRejected)
5151+ return (promiseResult ||= promise(sourceError)).then(onFulfilled, onRejected)
5252 },
5353 catch(onRejected) {
5454- return (promiseResult ||= promise()).catch(onRejected)
5454+ return (promiseResult ||= promise(sourceError)).catch(onRejected)
5555 },
5656 finally(onFinally) {
5757- return (promiseResult ||= promise()).finally(onFinally)
5757+ return (promiseResult ||= promise(sourceError)).finally(onFinally)
5858 },
5959 [Symbol.toStringTag]: 'Promise',
6060 } satisfies Promise<T>
···102102 throw new Error('Worker state is not found. This is an issue with Vitest. Please, open an issue.')
103103 }
104104 return state
105105-}
106106-107107-/* @__NO_SIDE_EFFECTS__ */
108108-export function convertElementToCssSelector(element: Element): string {
109109- if (!element || !(element instanceof Element)) {
110110- throw new Error(
111111- `Expected DOM element to be an instance of Element, received ${typeof element}`,
112112- )
113113- }
114114-115115- return getUniqueCssSelector(element)
116116-}
117117-118118-function escapeIdForCSSSelector(id: string) {
119119- return id
120120- .split('')
121121- .map((char) => {
122122- const code = char.charCodeAt(0)
123123-124124- if (char === ' ' || char === '#' || char === '.' || char === ':' || char === '[' || char === ']' || char === '>' || char === '+' || char === '~' || char === '\\') {
125125- // Escape common special characters with backslashes
126126- return `\\${char}`
127127- }
128128- else if (code >= 0x10000) {
129129- // Unicode escape for characters outside the BMP
130130- return `\\${code.toString(16).toUpperCase().padStart(6, '0')} `
131131- }
132132- else if (code < 0x20 || code === 0x7F) {
133133- // Non-printable ASCII characters (0x00-0x1F and 0x7F) are escaped
134134- return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
135135- }
136136- else if (code >= 0x80) {
137137- // Non-ASCII characters (0x80 and above) are escaped
138138- return `\\${code.toString(16).toUpperCase().padStart(2, '0')} `
139139- }
140140- else {
141141- // Allowable characters are used directly
142142- return char
143143- }
144144- })
145145- .join('')
146146-}
147147-148148-function getUniqueCssSelector(el: Element) {
149149- const path = []
150150- let parent: null | ParentNode
151151- let hasShadowRoot = false
152152- // eslint-disable-next-line no-cond-assign
153153- while (parent = getParent(el)) {
154154- if ((parent as Element).shadowRoot) {
155155- hasShadowRoot = true
156156- }
157157-158158- const tag = el.tagName
159159- if (el.id) {
160160- path.push(`#${escapeIdForCSSSelector(el.id)}`)
161161- }
162162- else if (!el.nextElementSibling && !el.previousElementSibling) {
163163- path.push(tag.toLowerCase())
164164- }
165165- else {
166166- let index = 0
167167- let sameTagSiblings = 0
168168- let elementIndex = 0
169169-170170- for (const sibling of parent.children) {
171171- index++
172172- if (sibling.tagName === tag) {
173173- sameTagSiblings++
174174- }
175175- if (sibling === el) {
176176- elementIndex = index
177177- }
178178- }
179179-180180- if (sameTagSiblings > 1) {
181181- path.push(`${tag.toLowerCase()}:nth-child(${elementIndex})`)
182182- }
183183- else {
184184- path.push(tag.toLowerCase())
185185- }
186186- }
187187- el = parent as Element
188188- };
189189- return `${getBrowserState().provider === 'webdriverio' && hasShadowRoot ? '>>>' : ''}${path.reverse().join(' > ')}`
190190-}
191191-192192-function getParent(el: Element) {
193193- const parent = el.parentNode
194194- if (parent instanceof ShadowRoot) {
195195- return parent.host
196196- }
197197- return parent
198198-}
199199-200200-export class CommandsManager {
201201- private _listeners: ((command: string, args: any[]) => void)[] = []
202202-203203- public onCommand(listener: (command: string, args: any[]) => void): void {
204204- this._listeners.push(listener)
205205- }
206206-207207- public async triggerCommand<T>(command: string, args: any[]): Promise<T> {
208208- const state = getWorkerState()
209209- const rpc = state.rpc as any as BrowserRPC
210210- const { sessionId } = getBrowserState()
211211- const filepath = state.filepath || state.current?.file?.filepath
212212- if (this._listeners.length) {
213213- await Promise.all(this._listeners.map(listener => listener(command, args)))
214214- }
215215- return rpc.triggerCommand<T>(sessionId, command, filepath, args)
216216- }
217105}
+4
packages/runner/src/types/tasks.ts
···247247 * Test context that will be passed to the test function.
248248 */
249249 context: TestContext & ExtraContext
250250+ /**
251251+ * The test timeout in milliseconds.
252252+ */
253253+ timeout: number
250254}
251255252256/**
···1919 await browser.$(selector).setValue(text)
2020 }
2121 else {
2222- throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`)
2222+ throw new TypeError(`Provider "${context.provider.name}" does not support filling inputs`)
2323 }
2424}
+1-1
packages/browser/src/node/commands/keyboard.ts
···202202203203function selectAll() {
204204 const element = document.activeElement as HTMLInputElement
205205- if (element && element.select) {
205205+ if (element && typeof element.select === 'function') {
206206 element.select()
207207 }
208208}
+1-1
packages/browser/src/node/commands/type.ts
···4545 text,
4646 () => browser.execute(() => {
4747 const element = document.activeElement as HTMLInputElement
4848- if (element) {
4848+ if (element && typeof element.select === 'function') {
4949 element.select()
5050 }
5151 }),
+1-1
packages/vitest/src/integrations/chai/poll.ts
···9191 const rejectWithCause = (cause: any) => {
9292 reject(
9393 copyStackTrace(
9494- new Error(`Matcher did not succeed in ${timeout}ms`, {
9494+ new Error('Matcher did not succeed in time.', {
9595 cause,
9696 }),
9797 STACK_TRACE_ERROR,