diff --git a/packages/browser-playwright/src/commands/pointer.ts b/packages/browser-playwright/src/commands/pointer.ts index d5465674c..518d1af61 100644 --- a/packages/browser-playwright/src/commands/pointer.ts +++ b/packages/browser-playwright/src/commands/pointer.ts @@ -1,11 +1,10 @@ -import type { SerializedUserEventPointerOptions } from 'vitest/browser' +import type { UserEvent } from 'vitest/browser' import type { UserEventCommand } from './utils' import { parseKeyDef } from '@vitest/browser' +import { click } from './click' import { hover } from './hover' -type PointerCommand = (options: readonly SerializedUserEventPointerOptions[]) => void - -export const pointer: UserEventCommand = async ( +export const pointer: UserEventCommand = async ( context, options, ) => { @@ -37,30 +36,48 @@ export const pointer: UserEventCommand = async ( } } - if (option.target) { - await hover(context, option.target) - } - else { - await context.page.mouse.move(option.coordinates.x, option.coordinates.y) + // `click` has its own moving logic, no need to move twice + if (option.action !== 'click') { + if (option.target) { + await hover(context, option.target) + } + else { + await context.page.mouse.move(option.coordinates.x, option.coordinates.y) + } } if (option.action) { - const options = { + const mouseOptions = { button: option.button, } switch (option.action) { case 'down': { - await context.page.mouse.down(options) + await context.page.mouse.down(mouseOptions) break } case 'up': { - await context.page.mouse.up(options) + await context.page.mouse.up(mouseOptions) break } case 'click': { - await context.page.mouse.down(options) - await context.page.mouse.up(options) + const clickOptions = { + ...mouseOptions, + clickCount: option.times ?? 1, + position: option.offset, + } satisfies Parameters[2] + + if (option.target) { + await click(context, option.target, clickOptions) + } + else { + await context.page.mouse.click( + option.coordinates.x, + option.coordinates.y, + clickOptions, + ) + } + break } } diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 68c482309..4b4201e1b 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -446,82 +446,54 @@ export interface UserEventWheelDirectionOptions extends UserEventWheelBaseOption export type UserEventWheelOptions = UserEventWheelDeltaOptions | UserEventWheelDirectionOptions /** - * Base options shared by all pointer event configurations. + * Options for triggering pointer events. + * + * Specify pointer position using either `coordinates` for precise pixel values, or `target` for element-based interaction. These are mutually exclusive. * * @since 5.0.0 */ -export interface UserEventPointerBaseOptions { +export type UserEventPointerOptions = { /** - * The keys to press while interacting with the pointer. - * - * @default undefined - */ + * The keys to press while interacting with the pointer. + * + * @default undefined + */ keys?: string /** - * The button to use for the pointer event. - * - * @default 'left' - */ + * The button to use for the pointer event. + * + * @default 'left' + */ button?: 'left' | 'right' | 'middle' +} & ({ + /** + * The action to perform with the pointer event. + * + * @default undefined + */ + action?: 'down' | 'up' + times?: undefined +} | { + action: 'click' + times?: number +}) & ({ /** - * The action to perform with the pointer event. - * - * @default undefined - */ - action?: 'down' | 'up' | 'click' - -} - -/** - * Options for triggering pointer events using coordinates. - * - * @since 5.0.0 - */ -export interface UserEventPointerCoordinatesOptions extends UserEventPointerBaseOptions { - /** - * The coordinates to interact with. - */ + * The coordinates to interact with. + */ coordinates: { x: number; y: number } + offset?: undefined target?: undefined -} - -/** - * Options for triggering pointer events using a target element. - * - * @since 5.0.0 - */ -export interface UserEventPointerTargetOptions extends UserEventPointerBaseOptions { +} | { + coordinates?: undefined /** - * The target element to interact with. + * 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. */ + offset?: { x: number; y: number } + /** + * The target element to interact with. + */ target: Element | Locator - coordinates?: undefined -} - -/** - * Options for triggering pointer events. - * - * Specify pointer position using either `coordinates` for precise pixel values, or `target` for element-based interaction. These are mutually exclusive. - * - * @since 5.0.0 - */ -export type UserEventPointerOptions = UserEventPointerCoordinatesOptions | UserEventPointerTargetOptions - -/** - * Serialized options for triggering pointer events using a target element. - * - * @internal - */ -export interface SerializedUserEventPointerTargetOptions extends UserEventPointerTargetOptions { - target: SerializedLocator -} - -/** - * Serialized options for triggering pointer events. - * - * @internal - */ -export type SerializedUserEventPointerOptions = UserEventPointerCoordinatesOptions | SerializedUserEventPointerTargetOptions +}) export interface LocatorOptions { /** diff --git a/test/browser/fixtures/user-event/pointer.test.ts b/test/browser/fixtures/user-event/pointer.test.ts index ff86d5c19..c892ede33 100644 --- a/test/browser/fixtures/user-event/pointer.test.ts +++ b/test/browser/fixtures/user-event/pointer.test.ts @@ -1,5 +1,5 @@ import { test, vi } from 'vitest' -import { userEvent, page, server } from 'vitest/browser' +import { userEvent, page } from 'vitest/browser' type PointerAction = (event: PointerEvent) => void @@ -35,6 +35,37 @@ test('click triggers hover events', async ({ expect }) => { expect(click).toHaveBeenCalledBefore(leave) }) +test('click at coordinates triggers hover events', async ({ expect }) => { + document.body.innerHTML = ` + + ` + + const enter = vi.fn() + const leave = vi.fn() + const click = vi.fn() + + const buttonElement = document.body.querySelector('button') + + buttonElement.addEventListener('mouseenter', enter) + buttonElement.addEventListener('mouseleave', leave) + buttonElement.addEventListener('click', click) + + await userEvent.pointer([ + { coordinates: { x: 11, y: 11 }, action: 'click' }, + { target: document.body }, + ]) + + expect(enter).toHaveBeenCalledOnce() + expect(click).toHaveBeenCalledOnce() + expect(click).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + clientX: expect.closeTo(11), + clientY: expect.closeTo(11), + })) + + expect(enter).toHaveBeenCalledBefore(click) + expect(click).toHaveBeenCalledBefore(leave) +}) + test('moves between coordinates', async ({ expect }) => { document.body.innerHTML = `
@@ -64,28 +95,7 @@ test('moves between coordinates', async ({ expect }) => { expect(leaveA).toHaveBeenCalledBefore(enterB) }) -test('clicks at coordinates', async ({ expect }) => { - document.body.innerHTML = ` - - ` - - const click = vi.fn() - - const buttonElement = document.body.querySelector('button') - - buttonElement.addEventListener('click', click) - - await userEvent.pointer([ - { coordinates: { x: 11, y: 11 }, action: 'click' }, - ]) - - expect(click).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - clientX: expect.closeTo(11), - clientY: expect.closeTo(11), - })) -}) - -test('down only fires mousedown', async ({ expect }) => { +test('down only fires mousedown event', async ({ expect }) => { document.body.innerHTML = `` const down = vi.fn() @@ -109,7 +119,7 @@ test('down only fires mousedown', async ({ expect }) => { expect(click).not.toHaveBeenCalled() }) -test.fails('double clicks', async ({ expect }) => { +test('multiple clicks trigger double click', async ({ expect }) => { document.body.innerHTML = `` const click = vi.fn() @@ -123,11 +133,15 @@ test.fails('double clicks', async ({ expect }) => { const target = page.getByRole('button') await userEvent.pointer([ - { target, action: 'click' }, - { target, action: 'click' }, + { target, action: 'click', times: 3 }, ]) - expect(click).toHaveBeenCalledTimes(2) + expect(click).toHaveBeenCalledTimes(3) + + expect(click).toHaveBeenNthCalledWith(1, expect.objectContaining({ detail: 1 })) + expect(click).toHaveBeenNthCalledWith(2, expect.objectContaining({ detail: 2 })) + expect(click).toHaveBeenNthCalledWith(3, expect.objectContaining({ detail: 3 })) + expect(doubleClick).toHaveBeenCalledOnce() })