Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/event/createEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
interface InterfaceMap {
ClipboardEvent: {type: ClipboardEvent, init: ClipboardEventInit}
InputEvent: {type: InputEvent, init: InputEventInit}
UIEvent: {type: UIEvent, init: UIEventInit}
MouseEvent: {type: MouseEvent, init: MouseEventInit}
PointerEvent: {type: PointerEvent, init: PointerEventInit}
WheelEvent: {type: WheelEvent, init: WheelEventInit}
KeyboardEvent: {type: KeyboardEvent, init: KeyboardEventInit}
FocusEvent: {type: FocusEvent, init: FocusEventInit}
}
Expand All @@ -28,13 +30,20 @@ const eventInitializer: {
Event: [],
FocusEvent: [initUIEvent, initFocusEvent],
InputEvent: [initUIEvent, initInputEvent],
UIEvent: [initUIEvent],
MouseEvent: [initUIEvent, initUIEventModifiers, initMouseEvent],
PointerEvent: [
initUIEvent,
initUIEventModifiers,
initMouseEvent,
initPointerEvent,
],
WheelEvent: [
initUIEvent,
initUIEventModifiers,
initMouseEvent,
initWheelEvent,
],
KeyboardEvent: [initUIEvent, initUIEventModifiers, initKeyboardEvent],
}

Expand Down Expand Up @@ -77,6 +86,7 @@ function getEventConstructors(window: Window & typeof globalThis) {
const DragEvent = window.DragEvent ?? class DragEvent extends MouseEvent {}
const PointerEvent =
window.PointerEvent ?? class PointerEvent extends MouseEvent {}
const WheelEvent = window.WheelEvent ?? class WheelEvent extends MouseEvent {}
const TouchEvent = window.TouchEvent ?? class TouchEvent extends UIEvent {}

return {
Expand All @@ -94,6 +104,7 @@ function getEventConstructors(window: Window & typeof globalThis) {
MouseEvent,
DragEvent,
PointerEvent,
WheelEvent,
TouchEvent,
}
}
Expand Down Expand Up @@ -222,9 +233,9 @@ function initMouseEvent(
pageX,
pageY,
}: MouseEventInit &
Partial<
Pick<MouseEvent, 'x' | 'y' | 'offsetX' | 'offsetY' | 'pageX' | 'pageY'>
>,
Partial<
Pick<MouseEvent, 'x' | 'y' | 'offsetX' | 'offsetY' | 'pageX' | 'pageY'>
>,
) {
assignProps(event, {
screenX: sanitizeNumber(screenX),
Expand All @@ -243,6 +254,18 @@ function initMouseEvent(
})
}

function initWheelEvent(
event: WheelEvent,
{deltaMode, deltaX, deltaY, deltaZ}: WheelEventInit,
) {
assignProps(event, {
deltaMode: sanitizeNumber(deltaMode),
deltaX: sanitizeNumber(deltaX),
deltaY: sanitizeNumber(deltaY),
deltaZ: sanitizeNumber(deltaZ),
})
}

function initPointerEvent(
event: PointerEvent,
{
Expand Down
10 changes: 9 additions & 1 deletion src/event/eventMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,25 @@ export const eventMap = {
EventType: 'PointerEvent',
defaultInit: {bubbles: false, cancelable: false},
},
scroll: {
EventType: 'UIEvent',
defaultInit: {bubbles: false, cancelable: false},
},
submit: {
EventType: 'Event',
defaultInit: {bubbles: true, cancelable: true},
},
wheel: {
EventType: 'WheelEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
} as const

function getEventClass(type: EventType) {
return eventMap[type].EventType
}

const mouseEvents = ['MouseEvent', 'PointerEvent']
const mouseEvents = ['MouseEvent', 'PointerEvent', 'WheelEvent']
export function isMouseEvent(type: EventType) {
return mouseEvents.includes(getEventClass(type))
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export {userEvent} from './setup'
export type {UserEvent} from './setup/setup'
export type {keyboardKey} from './system/keyboard'
export type {pointerKey} from './system/pointer'
export type {ScrollOptions} from './utility'
export {PointerEventsCheckLevel, type Options} from './options'
12 changes: 11 additions & 1 deletion src/setup/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import {click, dblClick, tripleClick, hover, unhover, tab} from '../convenience'
import {keyboard} from '../keyboard'
import {copy, cut, paste} from '../clipboard'
import {pointer} from '../pointer'
import {clear, deselectOptions, selectOptions, type, upload} from '../utility'
import {
clear,
deselectOptions,
scroll,
selectOptions,
type,
upload,
wheel,
} from '../utility'

export const userEventApi = {
click,
Expand All @@ -18,7 +26,9 @@ export const userEventApi = {
pointer,
clear,
deselectOptions,
scroll,
selectOptions,
type,
upload,
wheel,
}
17 changes: 17 additions & 0 deletions src/setup/directApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {type Options} from '../options'
import {type PointerInput} from '../pointer'
import {type ScrollOptions} from '../utility'
import {type System} from '../system'
import {setupDirect, type UserEventApi} from './setup'

Expand Down Expand Up @@ -62,6 +63,14 @@ export function paste(
return setupDirect(options).api.paste(clipboardData)
}

export function scroll(
element: Element,
scrollOptions: ScrollOptions = {},
options: DirectOptions = {},
) {
return setupDirect(options, element).api.scroll(element, scrollOptions)
}

export function selectOptions(
select: Element,
values: HTMLElement | HTMLElement[] | string[] | string,
Expand Down Expand Up @@ -89,6 +98,14 @@ export function unhover(element: Element, options: DirectOptions = {}) {
return api.unhover(element)
}

export function wheel(
element: Element,
init: WheelEventInit = {},
options: DirectOptions = {},
) {
return setupDirect(options, element).api.wheel(element, init)
}

export function upload(
element: HTMLElement,
fileOrFiles: File | File[],
Expand Down
1 change: 1 addition & 0 deletions src/utility/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './clear'
export * from './scroll'
export * from './selectOptions'
export * from './type'
export * from './upload'
37 changes: 37 additions & 0 deletions src/utility/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {type Instance} from '../setup'

export interface ScrollOptions {
left?: number
top?: number
}

/**
* Set an element's scroll position and dispatch a scroll event.
*
* This does not calculate layout, scroll limits, or intermediate scroll positions.
*/
export async function scroll(
this: Instance,
element: Element,
{left, top}: ScrollOptions = {},
) {
if (left !== undefined) {
element.scrollLeft = left
}
if (top !== undefined) {
element.scrollTop = top
}

this.dispatchUIEvent(element, 'scroll')
}

/**
* Dispatch a wheel event on an element.
*/
export async function wheel(
this: Instance,
element: Element,
init: WheelEventInit = {},
) {
this.dispatchUIEvent(element, 'wheel', init)
}
8 changes: 8 additions & 0 deletions tests/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const apiDeclarations: ApiDeclarations = {
pointer: {
args: ['foo'],
},
scroll: {
args: [null, {}],
elementArg: 0,
},
selectOptions: {
args: [null, ['foo']],
elementArg: 0,
Expand All @@ -64,6 +68,10 @@ const apiDeclarations: ApiDeclarations = {
elementArg: 0,
elementHtml: `<input type="file"/>`,
},
wheel: {
args: [null, {}],
elementArg: 0,
},
}

type ApiDeclarationsEntry<
Expand Down
69 changes: 69 additions & 0 deletions tests/utility/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import userEvent from '#src'
import {setup} from '#testHelpers'

describe('scroll', () => {
test('sets the supplied scroll positions before dispatching a non-bubbling scroll event', async () => {
const {element, getEvents, user} = setup('<div></div>')
const onParentScroll = mocks.fn()
let scrollPosition: {left: number, top: number} | undefined
element.parentElement?.addEventListener('scroll', onParentScroll)
element.addEventListener('scroll', () => {
scrollPosition = {left: element.scrollLeft, top: element.scrollTop}
})
element.scrollLeft = 10

await user.scroll(element, {top: 20})

expect(element.scrollLeft).toBe(10)
expect(element.scrollTop).toBe(20)
expect(scrollPosition).toEqual({left: 10, top: 20})
expect(getEvents('scroll')).toHaveLength(1)
expect(getEvents('scroll')[0]).toMatchObject({
bubbles: false,
cancelable: false,
})
expect(onParentScroll).not.toHaveBeenCalled()
})

test('is available from the direct API', async () => {
const element = document.createElement('div')
const onScroll = mocks.fn()
element.addEventListener('scroll', onScroll)

await userEvent.scroll(element, {left: 20, top: 30})

expect(element.scrollLeft).toBe(20)
expect(element.scrollTop).toBe(30)
expect(onScroll).toHaveBeenCalledTimes(1)
})
})

describe('wheel', () => {
test('dispatches a wheel event with the supplied deltas', async () => {
const {element, getEvents, user} = setup('<div></div>')

await user.wheel(element, {
deltaMode: WheelEvent.DOM_DELTA_LINE,
deltaX: 10,
deltaY: -20,
deltaZ: 30,
})

const [event] = getEvents('wheel')
expect(event).toMatchObject({
deltaMode: WheelEvent.DOM_DELTA_LINE,
deltaX: 10,
deltaY: -20,
deltaZ: 30,
})
})

test('includes pressed modifier keys', async () => {
const {element, getEvents, user} = setup('<div></div>')
await user.keyboard('[ShiftLeft>]')

await user.wheel(element)

expect(getEvents('wheel')[0].shiftKey).toBe(true)
})
})