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() +})