Skip to content

Commit

Permalink
refactor: dispatch all UI events per one internal API (#838)
Browse files Browse the repository at this point in the history
* refactor: centralize calls to createEvent

* refactor: centralize dispatching ui events

* refactor: apply event props in dispatcher

* refactor: inline paste implementation
  • Loading branch information
ph-fritsche authored Feb 3, 2022
1 parent a5ca2e4 commit 4720ac2
Show file tree
Hide file tree
Showing 37 changed files with 480 additions and 364 deletions.
3 changes: 1 addition & 2 deletions src/clipboard/copy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {fireEvent} from '@testing-library/dom'
import {Config, Instance} from '../setup'
import {copySelection, writeDataTransferToClipboard} from '../utils'

Expand All @@ -12,7 +11,7 @@ export async function copy(this: Instance) {
return
}

fireEvent.copy(target, {
this.dispatchUIEvent(target, 'copy', {
clipboardData,
})

Expand Down
5 changes: 2 additions & 3 deletions src/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {fireEvent} from '@testing-library/dom'
import {Config, Instance} from '../setup'
import {
copySelection,
Expand All @@ -17,12 +16,12 @@ export async function cut(this: Instance) {
return
}

fireEvent.cut(target, {
this.dispatchUIEvent(target, 'cut', {
clipboardData,
})

if (isEditable(target)) {
prepareInput('', target, 'deleteByCut')?.commit()
prepareInput(this[Config], '', target, 'deleteByCut')?.commit()
}

if (this[Config].writeToClipboard) {
Expand Down
17 changes: 6 additions & 11 deletions src/clipboard/paste.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {fireEvent} from '@testing-library/dom'
import {Config, Instance} from '../setup'
import {
createDataTransfer,
Expand All @@ -15,7 +14,7 @@ export async function paste(
const doc = this[Config].document
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body

const data: DataTransfer =
const dataTransfer: DataTransfer =
(typeof clipboardData === 'string'
? getClipboardDataFromString(clipboardData)
: clipboardData) ??
Expand All @@ -25,21 +24,17 @@ export async function paste(
)
}))

return pasteImpl(target, data)
}

function pasteImpl(target: Element, clipboardData: DataTransfer) {
fireEvent.paste(target, {
clipboardData,
this.dispatchUIEvent(target, 'paste', {
clipboardData: dataTransfer,
})

if (isEditable(target)) {
const data = clipboardData
const textData = dataTransfer
.getData('text')
.substr(0, getSpaceUntilMaxLength(target))

if (data) {
prepareInput(data, target, 'insertFromPaste')?.commit()
if (textData) {
prepareInput(this[Config], textData, target, 'insertFromPaste')?.commit()
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/document/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {fireEvent} from '@testing-library/dom'
import {dispatchUIEvent} from '../event'
import {Config} from '../setup'
import {prepareSelectionInterceptor} from './selection'
import {
getInitialValue,
Expand Down Expand Up @@ -45,7 +46,7 @@ export function prepareDocument(document: Document) {
const el = e.target as HTMLInputElement
const initialValue = getInitialValue(el)
if (typeof initialValue === 'string' && el.value !== initialValue) {
fireEvent.change(el)
dispatchUIEvent({} as Config, el, 'change')
}
},
{
Expand Down
98 changes: 98 additions & 0 deletions src/event/createEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {createEvent as createEventBase} from '@testing-library/dom'
import {eventMap} from '@testing-library/dom/dist/event-map.js'
import {isMouseEvent} from './eventTypes'
import {EventType, PointerCoords} from './types'

export type EventTypeInit<K extends EventType> = SpecificEventInit<
FixedDocumentEventMap[K]
>

interface FixedDocumentEventMap extends DocumentEventMap {
input: InputEvent
}

type SpecificEventInit<E extends Event> = E extends InputEvent
? InputEventInit
: E extends ClipboardEvent
? ClipboardEventInit
: E extends KeyboardEvent
? KeyboardEventInit
: E extends PointerEvent
? PointerEventInit
: E extends MouseEvent
? MouseEventInit
: E extends UIEvent
? UIEventInit
: EventInit

export function createEvent<K extends EventType>(
type: K,
target: Element,
init?: EventTypeInit<K>,
) {
const eventKey = Object.keys(eventMap).find(
k => k.toLowerCase() === type,
) as keyof typeof createEventBase

const event = createEventBase[eventKey](target, init) as DocumentEventMap[K]

// Can not use instanceof, as MouseEvent might be polyfilled.
if (isMouseEvent(type) && init) {
// see https://github.com/testing-library/react-testing-library/issues/268
assignPositionInit(event as MouseEvent, init)
assignPointerInit(event as PointerEvent, init)
}

return event
}

function assignProps(
obj: MouseEvent | PointerEvent,
props: MouseEventInit & PointerEventInit & PointerCoords,
) {
for (const [key, value] of Object.entries(props)) {
Object.defineProperty(obj, key, {get: () => value})
}
}

function assignPositionInit(
obj: MouseEvent | PointerEvent,
{
x,
y,
clientX,
clientY,
offsetX,
offsetY,
pageX,
pageY,
screenX,
screenY,
}: PointerCoords & MouseEventInit,
) {
assignProps(obj, {
/* istanbul ignore start */
x: x ?? clientX ?? 0,
y: y ?? clientY ?? 0,
clientX: x ?? clientX ?? 0,
clientY: y ?? clientY ?? 0,
offsetX: offsetX ?? 0,
offsetY: offsetY ?? 0,
pageX: pageX ?? 0,
pageY: pageY ?? 0,
screenX: screenX ?? 0,
screenY: screenY ?? 0,
/* istanbul ignore end */
})
}

function assignPointerInit(
obj: MouseEvent | PointerEvent,
{isPrimary, pointerId, pointerType}: PointerEventInit,
) {
assignProps(obj, {
isPrimary,
pointerId,
pointerType,
})
}
9 changes: 9 additions & 0 deletions src/event/dom-events.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module '@testing-library/dom/dist/event-map.js' {
import {EventType} from '@testing-library/dom'
export const eventMap: {
[k in EventType]: {
EventType: string
defaultInit: EventInit
}
}
}
20 changes: 20 additions & 0 deletions src/event/eventTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {eventMap} from '@testing-library/dom/dist/event-map.js'

const eventKeys = Object.fromEntries(
Object.keys(eventMap).map(k => [k.toLowerCase(), k]),
) as {
[k in keyof DocumentEventMap]: keyof typeof eventMap
}

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

const mouseEvents = ['MouseEvent', 'PointerEvent']
export function isMouseEvent(type: keyof DocumentEventMap) {
return mouseEvents.includes(getEventClass(type))
}

export function isKeyboardEvent(type: keyof DocumentEventMap) {
return getEventClass(type) === 'KeyboardEvent'
}
30 changes: 30 additions & 0 deletions src/event/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Config} from '../setup'
import {getUIEventModifiers} from '../utils'
import {createEvent, EventTypeInit} from './createEvent'
import {isKeyboardEvent, isMouseEvent} from './eventTypes'
import {EventType, PointerCoords} from './types'
import {wrapEvent} from './wrapEvent'

export type {EventType, PointerCoords}

export function dispatchUIEvent<K extends EventType>(
config: Config,
target: Element,
type: K,
init?: EventTypeInit<K>,
) {
if (isMouseEvent(type) || isKeyboardEvent(type)) {
init = {
...init,
...getUIEventModifiers(config.keyboardState),
} as EventTypeInit<K>
}

const event = createEvent(type, target, init)

return wrapEvent(() => target.dispatchEvent(event), target)
}

export function bindDispatchUIEvent(config: Config) {
return dispatchUIEvent.bind(undefined, config)
}
14 changes: 14 additions & 0 deletions src/event/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type EventType = keyof DocumentEventMap

export interface PointerCoords {
x?: number
y?: number
clientX?: number
clientY?: number
offsetX?: number
offsetY?: number
pageX?: number
pageY?: number
screenX?: number
screenY?: number
}
5 changes: 5 additions & 0 deletions src/event/wrapEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {getConfig} from '@testing-library/dom'

export function wrapEvent<R>(cb: () => R, _element: Element) {
return getConfig().eventWrapper(cb) as unknown as R
}
4 changes: 1 addition & 3 deletions src/keyboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export {releaseAllKeys}
export type {keyboardKey, keyboardState}

export async function keyboard(this: Instance, text: string): Promise<void> {
const {keyboardMap} = this[Config]

const actions: KeyboardAction[] = parseKeyDef(keyboardMap, text)
const actions: KeyboardAction[] = parseKeyDef(this[Config].keyboardMap, text)

return keyboardAction(this[Config], actions)
}
Expand Down
18 changes: 11 additions & 7 deletions src/keyboard/keyboardAction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {fireEvent} from '@testing-library/dom'
import {dispatchUIEvent} from '../event'
import {Config} from '../setup'
import {getActiveElement, getKeyEventProps, wait} from '../utils'
import {behaviorPlugin, keyboardKey} from './types'
Expand Down Expand Up @@ -85,9 +85,11 @@ async function keydown(

applyPlugins(plugins.preKeydownBehavior, keyDef, element, config)

const unpreventedDefault = fireEvent.keyDown(
const unpreventedDefault = dispatchUIEvent(
config,
element,
getKeyEventProps(keyDef, config.keyboardState),
'keydown',
getKeyEventProps(keyDef),
)

config.keyboardState.pressed.push({keyDef, unpreventedDefault})
Expand All @@ -107,8 +109,8 @@ async function keypress(
) {
const element = getCurrentElement()

const unpreventedDefault = fireEvent.keyPress(element, {
...getKeyEventProps(keyDef, config.keyboardState),
const unpreventedDefault = dispatchUIEvent(config, element, 'keypress', {
...getKeyEventProps(keyDef),
charCode: keyDef.key === 'Enter' ? 13 : String(keyDef.key).charCodeAt(0),
})

Expand All @@ -127,9 +129,11 @@ async function keyup(

applyPlugins(plugins.preKeyupBehavior, keyDef, element, config)

const unpreventedDefault = fireEvent.keyUp(
const unpreventedDefault = dispatchUIEvent(
config,
element,
getKeyEventProps(keyDef, config.keyboardState),
'keyup',
getKeyEventProps(keyDef),
)

if (unprevented && unpreventedDefault) {
Expand Down
Loading

0 comments on commit 4720ac2

Please sign in to comment.