diff --git a/test/browser/fixtures/user-event/pointer.test.ts b/test/browser/fixtures/user-event/pointer.test.ts
index b93836508..c4af4f0a7 100644
--- a/test/browser/fixtures/user-event/pointer.test.ts
+++ b/test/browser/fixtures/user-event/pointer.test.ts
@@ -1,9 +1,9 @@
import { test, vi } from 'vitest'
-import { userEvent, page } from 'vitest/browser'
+import { userEvent, page, server } from 'vitest/browser'
type PointerAction = (event: PointerEvent) => void
-test('hovers and clicks a button', async ({ expect }) => {
+test('click triggers hover events', async ({ expect }) => {
document.body.innerHTML = `
@@ -34,3 +34,260 @@ test('hovers and clicks a button', async ({ expect }) => {
expect(enter).toHaveBeenCalledBefore(click)
expect(click).toHaveBeenCalledBefore(leave)
})
+
+test('moves between coordinates', async ({ expect }) => {
+ document.body.innerHTML = `
+
+
+ `
+
+ const enterA = vi.fn
()
+ const leaveA = vi.fn()
+ const enterB = vi.fn()
+
+ const a = document.body.querySelector('#a')
+ const b = document.body.querySelector('#b')
+
+ a.addEventListener('mouseenter', enterA)
+ a.addEventListener('mouseleave', leaveA)
+ b.addEventListener('mouseenter', enterB)
+
+ await userEvent.pointer([
+ { coordinates: { x: 50, y: 50 } },
+ { coordinates: { x: 50, y: 250 } },
+ ])
+
+ expect(enterA).toHaveBeenCalledOnce()
+ expect(leaveA).toHaveBeenCalledOnce()
+ expect(enterB).toHaveBeenCalledOnce()
+ expect(enterA).toHaveBeenCalledBefore(leaveA)
+ 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 }) => {
+ document.body.innerHTML = ``
+
+ const down = vi.fn()
+ const up = vi.fn()
+ const click = vi.fn()
+
+ const buttonElement = document.body.querySelector('button')
+
+ buttonElement.addEventListener('mousedown', down)
+ buttonElement.addEventListener('mouseup', up)
+ buttonElement.addEventListener('click', click)
+
+ const target = page.getByRole('button')
+
+ await userEvent.pointer([
+ { target, action: 'down' },
+ ])
+
+ expect(down).toHaveBeenCalledOnce()
+ expect(up).not.toHaveBeenCalled()
+ expect(click).not.toHaveBeenCalled()
+})
+
+test('middle click', async ({ expect }) => {
+ document.body.innerHTML = ``
+
+ const down = vi.fn()
+ const up = vi.fn()
+ const click = vi.fn()
+
+ const buttonElement = document.body.querySelector('button')
+
+ buttonElement.addEventListener('mousedown', down)
+ buttonElement.addEventListener('mouseup', up)
+ buttonElement.addEventListener('click', click)
+
+ const target = page.getByRole('button')
+
+ await userEvent.pointer([
+ { target, button: 'middle', action: 'down' },
+ ])
+
+ expect(down).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
+ button: 1,
+ }))
+ expect(up).not.toHaveBeenCalled()
+ expect(click).not.toHaveBeenCalled()
+})
+
+test('drags and drops', async ({ expect }) => {
+ document.body.innerHTML = `
+ Drag me
+ Drop here
+ `
+
+ const source = document.body.querySelector('#source')
+ const dropTarget = document.body.querySelector('#target')
+
+ type DragAction = (event: DragEvent) => void
+
+ const dragStart = vi.fn()
+ const dragEnter = vi.fn()
+ const drop = vi.fn()
+ const dragEnd = vi.fn()
+
+ source.addEventListener('dragstart', dragStart)
+ dropTarget.addEventListener('dragenter', dragEnter)
+ dropTarget.addEventListener('dragover', (event) => event.preventDefault())
+ dropTarget.addEventListener('drop', drop)
+ source.addEventListener('dragend', dragEnd)
+
+ await userEvent.pointer([
+ { target: source, action: 'down' },
+ { target: dropTarget },
+ { target: dropTarget, action: 'up' },
+ ])
+
+ expect(dragStart).toHaveBeenCalledOnce()
+ expect(dragEnter).toHaveBeenCalledOnce()
+ expect(drop).toHaveBeenCalledOnce()
+ expect(dragEnd).toHaveBeenCalledOnce()
+ expect(dragStart).toHaveBeenCalledBefore(dragEnter)
+ expect(dragEnter).toHaveBeenCalledBefore(drop)
+ expect(drop).toHaveBeenCalledBefore(dragEnd)
+})
+
+test('temporary modifiers apply to one action', async ({ expect }) => {
+ document.body.innerHTML = ``
+
+ const click = vi.fn()
+
+ const buttonElement = document.body.querySelector('button')
+
+ buttonElement.addEventListener('click', click)
+
+ const target = page.getByRole('button')
+
+ await userEvent.pointer([
+ { target, action: 'click', keys: '{ShiftLeft}' },
+ { target, action: 'click' },
+ ])
+
+ expect(click).toHaveBeenCalledTimes(2)
+ expect(click).toHaveBeenNthCalledWith(1, expect.objectContaining({ shiftKey: true }))
+ expect(click).toHaveBeenNthCalledWith(2, expect.objectContaining({ shiftKey: false }))
+})
+
+test('persistent modifiers survive multiple actions', async ({ expect }) => {
+ document.body.innerHTML = `
+
+
+
+
+ `
+
+ const clickA = vi.fn()
+ const clickB = vi.fn()
+ const clickC = vi.fn()
+ const clickD = vi.fn()
+
+ const a = document.body.querySelector('#a')
+ const b = document.body.querySelector('#b')
+ const c = document.body.querySelector('#c')
+ const d = document.body.querySelector('#d')
+
+ a.addEventListener('click', clickA)
+ b.addEventListener('click', clickB)
+ c.addEventListener('click', clickC)
+ d.addEventListener('click', clickD)
+
+ await userEvent.pointer([
+ { target: a, action: 'click', keys: '{ShiftLeft>}{AltLeft>}' },
+ { target: b, action: 'click' },
+ { target: c, action: 'click', keys: '{/ShiftLeft}{/AltLeft}' },
+ { target: d, action: 'click' },
+ ])
+
+ expect(clickA).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ shiftKey: true, altKey: true }))
+ expect(clickB).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ shiftKey: true, altKey: true }))
+ expect(clickC).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ shiftKey: true, altKey: true }))
+ expect(clickD).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ shiftKey: false, altKey: false }))
+})
+
+test('modifiers work with 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',
+ keys: '{AltLeft}',
+ },
+ ])
+
+ expect(click).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
+ altKey: true,
+ clientX: expect.closeTo(11),
+ clientY: expect.closeTo(11),
+ }))
+})
+
+// on webkit, right click opens the context menu even in headless
+// keep as the last test to prevent failing tests
+test('right click', async ({ expect }) => {
+ document.body.innerHTML = ``
+
+ const down = vi.fn()
+ const up = vi.fn()
+ const click = vi.fn()
+
+ const buttonElement = document.body.querySelector('button')
+
+ buttonElement.addEventListener('mousedown', down)
+ buttonElement.addEventListener('mouseup', up)
+ buttonElement.addEventListener('click', click)
+
+ const target = page.getByRole('button')
+
+ await userEvent.pointer([
+ { target, button: 'right', action: 'click' },
+ ])
+
+ expect(down).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
+ button: 2,
+ }))
+
+ // mouseup is not fired on webkit when right clicking
+ if (server.config.browser.name === 'webkit') {
+ expect(up).not.toHaveBeenCalled()
+ } else {
+ expect(up).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
+ button: 2,
+ }))
+ }
+
+ expect(click).not.toHaveBeenCalled()
+})