Skip to content

Commit 4720ac2

Browse files
authored
refactor: dispatch all UI events per one internal API (#838)
* refactor: centralize calls to createEvent * refactor: centralize dispatching ui events * refactor: apply event props in dispatcher * refactor: inline paste implementation
1 parent a5ca2e4 commit 4720ac2

37 files changed

+480
-364
lines changed

src/clipboard/copy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {fireEvent} from '@testing-library/dom'
21
import {Config, Instance} from '../setup'
32
import {copySelection, writeDataTransferToClipboard} from '../utils'
43

@@ -12,7 +11,7 @@ export async function copy(this: Instance) {
1211
return
1312
}
1413

15-
fireEvent.copy(target, {
14+
this.dispatchUIEvent(target, 'copy', {
1615
clipboardData,
1716
})
1817

src/clipboard/cut.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {fireEvent} from '@testing-library/dom'
21
import {Config, Instance} from '../setup'
32
import {
43
copySelection,
@@ -17,12 +16,12 @@ export async function cut(this: Instance) {
1716
return
1817
}
1918

20-
fireEvent.cut(target, {
19+
this.dispatchUIEvent(target, 'cut', {
2120
clipboardData,
2221
})
2322

2423
if (isEditable(target)) {
25-
prepareInput('', target, 'deleteByCut')?.commit()
24+
prepareInput(this[Config], '', target, 'deleteByCut')?.commit()
2625
}
2726

2827
if (this[Config].writeToClipboard) {

src/clipboard/paste.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {fireEvent} from '@testing-library/dom'
21
import {Config, Instance} from '../setup'
32
import {
43
createDataTransfer,
@@ -15,7 +14,7 @@ export async function paste(
1514
const doc = this[Config].document
1615
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body
1716

18-
const data: DataTransfer =
17+
const dataTransfer: DataTransfer =
1918
(typeof clipboardData === 'string'
2019
? getClipboardDataFromString(clipboardData)
2120
: clipboardData) ??
@@ -25,21 +24,17 @@ export async function paste(
2524
)
2625
}))
2726

28-
return pasteImpl(target, data)
29-
}
30-
31-
function pasteImpl(target: Element, clipboardData: DataTransfer) {
32-
fireEvent.paste(target, {
33-
clipboardData,
27+
this.dispatchUIEvent(target, 'paste', {
28+
clipboardData: dataTransfer,
3429
})
3530

3631
if (isEditable(target)) {
37-
const data = clipboardData
32+
const textData = dataTransfer
3833
.getData('text')
3934
.substr(0, getSpaceUntilMaxLength(target))
4035

41-
if (data) {
42-
prepareInput(data, target, 'insertFromPaste')?.commit()
36+
if (textData) {
37+
prepareInput(this[Config], textData, target, 'insertFromPaste')?.commit()
4338
}
4439
}
4540
}

src/document/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {fireEvent} from '@testing-library/dom'
1+
import {dispatchUIEvent} from '../event'
2+
import {Config} from '../setup'
23
import {prepareSelectionInterceptor} from './selection'
34
import {
45
getInitialValue,
@@ -45,7 +46,7 @@ export function prepareDocument(document: Document) {
4546
const el = e.target as HTMLInputElement
4647
const initialValue = getInitialValue(el)
4748
if (typeof initialValue === 'string' && el.value !== initialValue) {
48-
fireEvent.change(el)
49+
dispatchUIEvent({} as Config, el, 'change')
4950
}
5051
},
5152
{

src/event/createEvent.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {createEvent as createEventBase} from '@testing-library/dom'
2+
import {eventMap} from '@testing-library/dom/dist/event-map.js'
3+
import {isMouseEvent} from './eventTypes'
4+
import {EventType, PointerCoords} from './types'
5+
6+
export type EventTypeInit<K extends EventType> = SpecificEventInit<
7+
FixedDocumentEventMap[K]
8+
>
9+
10+
interface FixedDocumentEventMap extends DocumentEventMap {
11+
input: InputEvent
12+
}
13+
14+
type SpecificEventInit<E extends Event> = E extends InputEvent
15+
? InputEventInit
16+
: E extends ClipboardEvent
17+
? ClipboardEventInit
18+
: E extends KeyboardEvent
19+
? KeyboardEventInit
20+
: E extends PointerEvent
21+
? PointerEventInit
22+
: E extends MouseEvent
23+
? MouseEventInit
24+
: E extends UIEvent
25+
? UIEventInit
26+
: EventInit
27+
28+
export function createEvent<K extends EventType>(
29+
type: K,
30+
target: Element,
31+
init?: EventTypeInit<K>,
32+
) {
33+
const eventKey = Object.keys(eventMap).find(
34+
k => k.toLowerCase() === type,
35+
) as keyof typeof createEventBase
36+
37+
const event = createEventBase[eventKey](target, init) as DocumentEventMap[K]
38+
39+
// Can not use instanceof, as MouseEvent might be polyfilled.
40+
if (isMouseEvent(type) && init) {
41+
// see https://github.com/testing-library/react-testing-library/issues/268
42+
assignPositionInit(event as MouseEvent, init)
43+
assignPointerInit(event as PointerEvent, init)
44+
}
45+
46+
return event
47+
}
48+
49+
function assignProps(
50+
obj: MouseEvent | PointerEvent,
51+
props: MouseEventInit & PointerEventInit & PointerCoords,
52+
) {
53+
for (const [key, value] of Object.entries(props)) {
54+
Object.defineProperty(obj, key, {get: () => value})
55+
}
56+
}
57+
58+
function assignPositionInit(
59+
obj: MouseEvent | PointerEvent,
60+
{
61+
x,
62+
y,
63+
clientX,
64+
clientY,
65+
offsetX,
66+
offsetY,
67+
pageX,
68+
pageY,
69+
screenX,
70+
screenY,
71+
}: PointerCoords & MouseEventInit,
72+
) {
73+
assignProps(obj, {
74+
/* istanbul ignore start */
75+
x: x ?? clientX ?? 0,
76+
y: y ?? clientY ?? 0,
77+
clientX: x ?? clientX ?? 0,
78+
clientY: y ?? clientY ?? 0,
79+
offsetX: offsetX ?? 0,
80+
offsetY: offsetY ?? 0,
81+
pageX: pageX ?? 0,
82+
pageY: pageY ?? 0,
83+
screenX: screenX ?? 0,
84+
screenY: screenY ?? 0,
85+
/* istanbul ignore end */
86+
})
87+
}
88+
89+
function assignPointerInit(
90+
obj: MouseEvent | PointerEvent,
91+
{isPrimary, pointerId, pointerType}: PointerEventInit,
92+
) {
93+
assignProps(obj, {
94+
isPrimary,
95+
pointerId,
96+
pointerType,
97+
})
98+
}

src/event/dom-events.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare module '@testing-library/dom/dist/event-map.js' {
2+
import {EventType} from '@testing-library/dom'
3+
export const eventMap: {
4+
[k in EventType]: {
5+
EventType: string
6+
defaultInit: EventInit
7+
}
8+
}
9+
}

src/event/eventTypes.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {eventMap} from '@testing-library/dom/dist/event-map.js'
2+
3+
const eventKeys = Object.fromEntries(
4+
Object.keys(eventMap).map(k => [k.toLowerCase(), k]),
5+
) as {
6+
[k in keyof DocumentEventMap]: keyof typeof eventMap
7+
}
8+
9+
function getEventClass(type: keyof DocumentEventMap) {
10+
return eventMap[eventKeys[type]].EventType
11+
}
12+
13+
const mouseEvents = ['MouseEvent', 'PointerEvent']
14+
export function isMouseEvent(type: keyof DocumentEventMap) {
15+
return mouseEvents.includes(getEventClass(type))
16+
}
17+
18+
export function isKeyboardEvent(type: keyof DocumentEventMap) {
19+
return getEventClass(type) === 'KeyboardEvent'
20+
}

src/event/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Config} from '../setup'
2+
import {getUIEventModifiers} from '../utils'
3+
import {createEvent, EventTypeInit} from './createEvent'
4+
import {isKeyboardEvent, isMouseEvent} from './eventTypes'
5+
import {EventType, PointerCoords} from './types'
6+
import {wrapEvent} from './wrapEvent'
7+
8+
export type {EventType, PointerCoords}
9+
10+
export function dispatchUIEvent<K extends EventType>(
11+
config: Config,
12+
target: Element,
13+
type: K,
14+
init?: EventTypeInit<K>,
15+
) {
16+
if (isMouseEvent(type) || isKeyboardEvent(type)) {
17+
init = {
18+
...init,
19+
...getUIEventModifiers(config.keyboardState),
20+
} as EventTypeInit<K>
21+
}
22+
23+
const event = createEvent(type, target, init)
24+
25+
return wrapEvent(() => target.dispatchEvent(event), target)
26+
}
27+
28+
export function bindDispatchUIEvent(config: Config) {
29+
return dispatchUIEvent.bind(undefined, config)
30+
}

src/event/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type EventType = keyof DocumentEventMap
2+
3+
export interface PointerCoords {
4+
x?: number
5+
y?: number
6+
clientX?: number
7+
clientY?: number
8+
offsetX?: number
9+
offsetY?: number
10+
pageX?: number
11+
pageY?: number
12+
screenX?: number
13+
screenY?: number
14+
}

src/event/wrapEvent.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {getConfig} from '@testing-library/dom'
2+
3+
export function wrapEvent<R>(cb: () => R, _element: Element) {
4+
return getConfig().eventWrapper(cb) as unknown as R
5+
}

0 commit comments

Comments
 (0)