diff --git a/packages/browser/matchers.d.ts b/packages/browser/matchers.d.ts index e401207985fc..bc91a219057b 100644 --- a/packages/browser/matchers.d.ts +++ b/packages/browser/matchers.d.ts @@ -17,6 +17,10 @@ declare module 'vitest' { type PromisifyDomAssertion = Promisify> interface ExpectStatic { + /** + * `expect.element(locator)` is a shorthand for `expect.poll(() => locator.element())`. + * You can set default timeout via `expect.poll.timeout` config. + */ element: (element: T, options?: ExpectPollOptions) => PromisifyDomAssertion> } } diff --git a/packages/browser/providers/playwright.d.ts b/packages/browser/providers/playwright.d.ts index 0dc2049f59ce..53f198cdde86 100644 --- a/packages/browser/providers/playwright.d.ts +++ b/packages/browser/providers/playwright.d.ts @@ -16,7 +16,13 @@ declare module 'vitest/node' { context?: Omit< BrowserContextOptions, 'ignoreHTTPSErrors' | 'serviceWorkers' - > + > & { + /** + * The maximum time in milliseconds to wait for `userEvent` action to complete. + * @default 0 (no timeout) + */ + actionTimeout?: number + } } export interface BrowserCommandContext { diff --git a/packages/browser/src/node/commands/clear.ts b/packages/browser/src/node/commands/clear.ts index a55d7c4eea8c..9de4737eeae7 100644 --- a/packages/browser/src/node/commands/clear.ts +++ b/packages/browser/src/node/commands/clear.ts @@ -10,9 +10,7 @@ export const clear: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context const element = iframe.locator(selector) - await element.clear({ - timeout: 1000, - }) + await element.clear() } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser diff --git a/packages/browser/src/node/commands/click.ts b/packages/browser/src/node/commands/click.ts index d3d24bd179b0..075f24adc059 100644 --- a/packages/browser/src/node/commands/click.ts +++ b/packages/browser/src/node/commands/click.ts @@ -11,10 +11,7 @@ export const click: UserEventCommand = async ( const provider = context.provider if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe - await tester.locator(selector).click({ - timeout: 1000, - ...options, - }) + await tester.locator(selector).click(options) } else if (provider instanceof WebdriverBrowserProvider) { const browser = context.browser @@ -53,7 +50,6 @@ export const tripleClick: UserEventCommand = async ( if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe await tester.locator(selector).click({ - timeout: 1000, ...options, clickCount: 3, }) diff --git a/packages/browser/src/node/commands/dragAndDrop.ts b/packages/browser/src/node/commands/dragAndDrop.ts index d961910adcd1..6fc66bbb54f3 100644 --- a/packages/browser/src/node/commands/dragAndDrop.ts +++ b/packages/browser/src/node/commands/dragAndDrop.ts @@ -14,10 +14,7 @@ export const dragAndDrop: UserEventCommand = async ( await frame.dragAndDrop( source, target, - { - timeout: 1000, - ...options_, - }, + options_, ) } else if (context.provider instanceof WebdriverBrowserProvider) { diff --git a/packages/browser/src/node/commands/fill.ts b/packages/browser/src/node/commands/fill.ts index db2b68b4b45d..3010a5ffbb47 100644 --- a/packages/browser/src/node/commands/fill.ts +++ b/packages/browser/src/node/commands/fill.ts @@ -12,7 +12,7 @@ export const fill: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context const element = iframe.locator(selector) - await element.fill(text, { timeout: 1000, ...options }) + await element.fill(text, options) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser diff --git a/packages/browser/src/node/commands/hover.ts b/packages/browser/src/node/commands/hover.ts index fee141acc949..9d90d21df604 100644 --- a/packages/browser/src/node/commands/hover.ts +++ b/packages/browser/src/node/commands/hover.ts @@ -9,10 +9,7 @@ export const hover: UserEventCommand = async ( options = {}, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { - await context.iframe.locator(selector).hover({ - timeout: 1000, - ...options, - }) + await context.iframe.locator(selector).hover(options) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts index f3685d0ab5bf..2f739e2efc27 100644 --- a/packages/browser/src/node/commands/screenshot.ts +++ b/packages/browser/src/node/commands/screenshot.ts @@ -30,7 +30,6 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async ( const { element: selector, ...config } = options const element = context.iframe.locator(`${selector}`) const buffer = await element.screenshot({ - timeout: 1000, ...config, path: savePath, }) diff --git a/packages/browser/src/node/commands/select.ts b/packages/browser/src/node/commands/select.ts index 92121f685872..22da023a1781 100644 --- a/packages/browser/src/node/commands/select.ts +++ b/packages/browser/src/node/commands/select.ts @@ -26,10 +26,7 @@ export const selectOptions: UserEventCommand = async return elementHandler })) as (readonly string[]) | (readonly ElementHandle[]) - await selectElement.selectOption(values, { - timeout: 1000, - ...options, - }) + await selectElement.selectOption(values, options) } else if (context.provider instanceof WebdriverBrowserProvider) { const values = userValues as any as [({ index: number })] diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index fa9bf02b678f..f01e3cf37f3e 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -31,7 +31,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { private options?: { launch?: LaunchOptions - context?: BrowserContextOptions + context?: BrowserContextOptions & { actionTimeout?: number } } public contexts = new Map() @@ -108,8 +108,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } const browser = await this.openBrowser() + const { actionTimeout, ...contextOptions } = this.options?.context ?? {} const options = { - ...this.options?.context, + ...contextOptions, ignoreHTTPSErrors: true, serviceWorkers: 'allow', } satisfies BrowserContextOptions @@ -117,6 +118,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { options.viewport = null } const context = await browser.newContext(options) + if (actionTimeout) { + context.setDefaultTimeout(actionTimeout) + } this.contexts.set(contextId, context) return context } @@ -187,7 +191,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { async openPage(contextId: string, url: string, beforeNavigate?: () => Promise) { const browserPage = await this.openBrowserPage(contextId) await beforeNavigate?.() - await browserPage.goto(url) + await browserPage.goto(url, { timeout: 0 }) } async getCDPSession(contextId: string) { diff --git a/test/browser/fixtures/timeout/timeout.test.ts b/test/browser/fixtures/timeout/timeout.test.ts new file mode 100644 index 000000000000..2ea9982805f6 --- /dev/null +++ b/test/browser/fixtures/timeout/timeout.test.ts @@ -0,0 +1,21 @@ +import { page } from '@vitest/browser/context'; +import { afterEach, expect, test } from 'vitest'; + +afterEach(() => { + document.body.innerHTML = '' +}) + +test('click default', async () => { + document.body.innerHTML = '
hello
' + await page.getByText('world').click() +}) + +test('click override', async () => { + document.body.innerHTML = '
hello
' + await page.getByText('world').click({ timeout: 345 }) +}) + +test('element', async () => { + document.body.innerHTML = '
hello
' + await expect.element(page.getByText('world')).toBeVisible() +}) diff --git a/test/browser/fixtures/timeout/vitest.config.ts b/test/browser/fixtures/timeout/vitest.config.ts new file mode 100644 index 000000000000..997fa84abcc4 --- /dev/null +++ b/test/browser/fixtures/timeout/vitest.config.ts @@ -0,0 +1,27 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const provider = process.env.PROVIDER || 'playwright' +const name = + process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') + +export default defineConfig({ + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + test: { + browser: { + enabled: true, + provider, + name, + providerOptions: { + context: { + actionTimeout: 500 + } + } + }, + expect: { + poll: { + timeout: 500 + } + } + }, +}) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 545ad88116d9..4ccc02129dbe 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises' import { beforeAll, describe, expect, onTestFailed, test } from 'vitest' -import { browser, runBrowserTests } from './utils' +import { browser, provider, runBrowserTests } from './utils' describe('running browser tests', async () => { let stderr: string @@ -153,3 +153,17 @@ test('user-event', async () => { } `) }) + +test('timeout', async () => { + const { stderr } = await runBrowserTests({ + root: './fixtures/timeout', + }) + expect(stderr).toContain('Matcher did not succeed in 500ms') + if (provider === 'playwright') { + expect(stderr).toContain('locator.click: Timeout 500ms exceeded.') + expect(stderr).toContain('locator.click: Timeout 345ms exceeded.') + } + if (provider === 'webdriverio') { + expect(stderr).toContain('Cannot find element with locator') + } +})