[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

chore: times option

Raul Macarie (Jul 21, 2026, 4:03 PM +0200) 447d5444 cf61c2a0

+114 -111
+42 -70
packages/browser/context.d.ts
··· 446 446 export type UserEventWheelOptions = UserEventWheelDeltaOptions | UserEventWheelDirectionOptions 447 447 448 448 /** 449 - * Base options shared by all pointer event configurations. 450 - * 451 - * @since 5.0.0 452 - */ 453 - export interface UserEventPointerBaseOptions { 454 - /** 455 - * The keys to press while interacting with the pointer. 456 - * 457 - * @default undefined 458 - */ 459 - keys?: string 460 - /** 461 - * The button to use for the pointer event. 462 - * 463 - * @default 'left' 464 - */ 465 - button?: 'left' | 'right' | 'middle' 466 - /** 467 - * The action to perform with the pointer event. 468 - * 469 - * @default undefined 470 - */ 471 - action?: 'down' | 'up' | 'click' 472 - 473 - } 474 - 475 - /** 476 - * Options for triggering pointer events using coordinates. 477 - * 478 - * @since 5.0.0 479 - */ 480 - export interface UserEventPointerCoordinatesOptions extends UserEventPointerBaseOptions { 481 - /** 482 - * The coordinates to interact with. 483 - */ 484 - coordinates: { x: number; y: number } 485 - target?: undefined 486 - } 487 - 488 - /** 489 - * Options for triggering pointer events using a target element. 490 - * 491 - * @since 5.0.0 492 - */ 493 - export interface UserEventPointerTargetOptions extends UserEventPointerBaseOptions { 494 - /** 495 - * The target element to interact with. 496 - */ 497 - target: Element | Locator 498 - coordinates?: undefined 499 - } 500 - 501 - /** 502 449 * Options for triggering pointer events. 503 450 * 504 451 * Specify pointer position using either `coordinates` for precise pixel values, or `target` for element-based interaction. These are mutually exclusive. 505 452 * 506 453 * @since 5.0.0 507 454 */ 508 - export type UserEventPointerOptions = UserEventPointerCoordinatesOptions | UserEventPointerTargetOptions 509 - 510 - /** 511 - * Serialized options for triggering pointer events using a target element. 512 - * 513 - * @internal 514 - */ 515 - export interface SerializedUserEventPointerTargetOptions extends UserEventPointerTargetOptions { 516 - target: SerializedLocator 517 - } 518 - 519 - /** 520 - * Serialized options for triggering pointer events. 521 - * 522 - * @internal 523 - */ 524 - export type SerializedUserEventPointerOptions = UserEventPointerCoordinatesOptions | SerializedUserEventPointerTargetOptions 455 + export type UserEventPointerOptions = { 456 + /** 457 + * The keys to press while interacting with the pointer. 458 + * 459 + * @default undefined 460 + */ 461 + keys?: string 462 + /** 463 + * The button to use for the pointer event. 464 + * 465 + * @default 'left' 466 + */ 467 + button?: 'left' | 'right' | 'middle' 468 + } & ({ 469 + /** 470 + * The action to perform with the pointer event. 471 + * 472 + * @default undefined 473 + */ 474 + action?: 'down' | 'up' 475 + times?: undefined 476 + } | { 477 + action: 'click' 478 + times?: number 479 + }) & ({ 480 + /** 481 + * The coordinates to interact with. 482 + */ 483 + coordinates: { x: number; y: number } 484 + offset?: undefined 485 + target?: undefined 486 + } | { 487 + coordinates?: undefined 488 + /** 489 + * A point to use relative to the top-left corner of the element's padding box. If not specified, uses some visible point of the element. 490 + */ 491 + offset?: { x: number; y: number } 492 + /** 493 + * The target element to interact with. 494 + */ 495 + target: Element | Locator 496 + }) 525 497 526 498 export interface LocatorOptions { 527 499 /**
+31 -14
packages/browser-playwright/src/commands/pointer.ts
··· 1 - import type { SerializedUserEventPointerOptions } from 'vitest/browser' 1 + import type { UserEvent } from 'vitest/browser' 2 2 import type { UserEventCommand } from './utils' 3 3 import { parseKeyDef } from '@vitest/browser' 4 + import { click } from './click' 4 5 import { hover } from './hover' 5 6 6 - type PointerCommand = (options: readonly SerializedUserEventPointerOptions[]) => void 7 - 8 - export const pointer: UserEventCommand<PointerCommand> = async ( 7 + export const pointer: UserEventCommand<UserEvent['pointer']> = async ( 9 8 context, 10 9 options, 11 10 ) => { ··· 37 36 } 38 37 } 39 38 40 - if (option.target) { 41 - await hover(context, option.target) 42 - } 43 - else { 44 - await context.page.mouse.move(option.coordinates.x, option.coordinates.y) 39 + // `click` has its own moving logic, no need to move twice 40 + if (option.action !== 'click') { 41 + if (option.target) { 42 + await hover(context, option.target) 43 + } 44 + else { 45 + await context.page.mouse.move(option.coordinates.x, option.coordinates.y) 46 + } 45 47 } 46 48 47 49 if (option.action) { 48 - const options = { 50 + const mouseOptions = { 49 51 button: option.button, 50 52 } 51 53 52 54 switch (option.action) { 53 55 case 'down': { 54 - await context.page.mouse.down(options) 56 + await context.page.mouse.down(mouseOptions) 55 57 break 56 58 } 57 59 case 'up': { 58 - await context.page.mouse.up(options) 60 + await context.page.mouse.up(mouseOptions) 59 61 break 60 62 } 61 63 case 'click': { 62 - await context.page.mouse.down(options) 63 - await context.page.mouse.up(options) 64 + const clickOptions = { 65 + ...mouseOptions, 66 + clickCount: option.times ?? 1, 67 + position: option.offset, 68 + } satisfies Parameters<typeof click>[2] 69 + 70 + if (option.target) { 71 + await click(context, option.target, clickOptions) 72 + } 73 + else { 74 + await context.page.mouse.click( 75 + option.coordinates.x, 76 + option.coordinates.y, 77 + clickOptions, 78 + ) 79 + } 80 + 64 81 break 65 82 } 66 83 }
+41 -27
test/browser/fixtures/user-event/pointer.test.ts
··· 1 1 import { test, vi } from 'vitest' 2 - import { userEvent, page, server } from 'vitest/browser' 2 + import { userEvent, page } from 'vitest/browser' 3 3 4 4 type PointerAction = (event: PointerEvent) => void 5 5 ··· 35 35 expect(click).toHaveBeenCalledBefore(leave) 36 36 }) 37 37 38 + test('click at coordinates triggers hover events', async ({ expect }) => { 39 + document.body.innerHTML = ` 40 + <button style="position: absolute; top: 10px; left: 10px; width: 100px; height: 40px;">Button</button> 41 + ` 42 + 43 + const enter = vi.fn<PointerAction>() 44 + const leave = vi.fn<PointerAction>() 45 + const click = vi.fn<PointerAction>() 46 + 47 + const buttonElement = document.body.querySelector('button') 48 + 49 + buttonElement.addEventListener('mouseenter', enter) 50 + buttonElement.addEventListener('mouseleave', leave) 51 + buttonElement.addEventListener('click', click) 52 + 53 + await userEvent.pointer([ 54 + { coordinates: { x: 11, y: 11 }, action: 'click' }, 55 + { target: document.body }, 56 + ]) 57 + 58 + expect(enter).toHaveBeenCalledOnce() 59 + expect(click).toHaveBeenCalledOnce() 60 + expect(click).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ 61 + clientX: expect.closeTo(11), 62 + clientY: expect.closeTo(11), 63 + })) 64 + 65 + expect(enter).toHaveBeenCalledBefore(click) 66 + expect(click).toHaveBeenCalledBefore(leave) 67 + }) 68 + 38 69 test('moves between coordinates', async ({ expect }) => { 39 70 document.body.innerHTML = ` 40 71 <div id="a" style="position:absolute; top:0; left:0; width:100px; height:100px;"></div> ··· 64 95 expect(leaveA).toHaveBeenCalledBefore(enterB) 65 96 }) 66 97 67 - test('clicks at coordinates', async ({ expect }) => { 68 - document.body.innerHTML = ` 69 - <button style="position: absolute; top: 10px; left: 10px; width: 100px; height: 40px;">Button</button> 70 - ` 71 - 72 - const click = vi.fn<PointerAction>() 73 - 74 - const buttonElement = document.body.querySelector('button') 75 - 76 - buttonElement.addEventListener('click', click) 77 - 78 - await userEvent.pointer([ 79 - { coordinates: { x: 11, y: 11 }, action: 'click' }, 80 - ]) 81 - 82 - expect(click).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ 83 - clientX: expect.closeTo(11), 84 - clientY: expect.closeTo(11), 85 - })) 86 - }) 87 - 88 - test('down only fires mousedown', async ({ expect }) => { 98 + test('down only fires mousedown event', async ({ expect }) => { 89 99 document.body.innerHTML = `<button>Button</button>` 90 100 91 101 const down = vi.fn<PointerAction>() ··· 109 119 expect(click).not.toHaveBeenCalled() 110 120 }) 111 121 112 - test.fails('double clicks', async ({ expect }) => { 122 + test('multiple clicks trigger double click', async ({ expect }) => { 113 123 document.body.innerHTML = `<button>Button</button>` 114 124 115 125 const click = vi.fn<PointerAction>() ··· 123 133 const target = page.getByRole('button') 124 134 125 135 await userEvent.pointer([ 126 - { target, action: 'click' }, 127 - { target, action: 'click' }, 136 + { target, action: 'click', times: 3 }, 128 137 ]) 129 138 130 - expect(click).toHaveBeenCalledTimes(2) 139 + expect(click).toHaveBeenCalledTimes(3) 140 + 141 + expect(click).toHaveBeenNthCalledWith(1, expect.objectContaining({ detail: 1 })) 142 + expect(click).toHaveBeenNthCalledWith(2, expect.objectContaining({ detail: 2 })) 143 + expect(click).toHaveBeenNthCalledWith(3, expect.objectContaining({ detail: 3 })) 144 + 131 145 expect(doubleClick).toHaveBeenCalledOnce() 132 146 }) 133 147