Skip to content

Commit 41432e5

Browse files
committed
fix(upload): improve correctness
1 parent a8ad3df commit 41432e5

File tree

4 files changed

+120
-95
lines changed

4 files changed

+120
-95
lines changed

src/events.js

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,72 +18,78 @@ function fireEvent(element, event) {
1818
})
1919
}
2020

21-
const createEvent = {}
21+
function createEvent(
22+
eventName,
23+
node,
24+
init,
25+
{EventType = 'Event', defaultInit = {}} = {},
26+
) {
27+
if (!node) {
28+
throw new Error(
29+
`Unable to fire a "${eventName}" event - please provide a DOM element.`,
30+
)
31+
}
32+
const eventInit = {...defaultInit, ...init}
33+
const {target: {value, files, ...targetProperties} = {}} = eventInit
34+
if (value !== undefined) {
35+
setNativeValue(node, value)
36+
}
37+
if (files !== undefined) {
38+
// input.files is a read-only property so this is not allowed:
39+
// input.files = [file]
40+
// so we have to use this workaround to set the property
41+
Object.defineProperty(node, 'files', {
42+
configurable: true,
43+
enumerable: true,
44+
writable: true,
45+
value: files,
46+
})
47+
}
48+
Object.assign(node, targetProperties)
49+
const window = getWindowFromNode(node)
50+
const EventConstructor = window[EventType] || window.Event
51+
let event
52+
/* istanbul ignore else */
53+
if (typeof EventConstructor === 'function') {
54+
event = new EventConstructor(eventName, eventInit)
55+
} else {
56+
// IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
57+
event = window.document.createEvent(EventType)
58+
const {bubbles, cancelable, detail, ...otherInit} = eventInit
59+
event.initEvent(eventName, bubbles, cancelable, detail)
60+
Object.keys(otherInit).forEach(eventKey => {
61+
event[eventKey] = otherInit[eventKey]
62+
})
63+
}
64+
65+
// DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568
66+
const dataTransferProperties = ['dataTransfer', 'clipboardData']
67+
dataTransferProperties.forEach(dataTransferKey => {
68+
const dataTransferValue = eventInit[dataTransferKey]
69+
70+
if (typeof dataTransferValue === 'object') {
71+
/* istanbul ignore if */
72+
if (typeof window.DataTransfer === 'function') {
73+
Object.defineProperty(event, dataTransferKey, {
74+
value: Object.assign(new window.DataTransfer(), dataTransferValue),
75+
})
76+
} else {
77+
Object.defineProperty(event, dataTransferKey, {
78+
value: dataTransferValue,
79+
})
80+
}
81+
}
82+
})
83+
84+
return event
85+
}
2286

2387
Object.keys(eventMap).forEach(key => {
2488
const {EventType, defaultInit} = eventMap[key]
2589
const eventName = key.toLowerCase()
2690

27-
createEvent[key] = (node, init) => {
28-
if (!node) {
29-
throw new Error(
30-
`Unable to fire a "${key}" event - please provide a DOM element.`,
31-
)
32-
}
33-
const eventInit = {...defaultInit, ...init}
34-
const {target: {value, files, ...targetProperties} = {}} = eventInit
35-
if (value !== undefined) {
36-
setNativeValue(node, value)
37-
}
38-
if (files !== undefined) {
39-
// input.files is a read-only property so this is not allowed:
40-
// input.files = [file]
41-
// so we have to use this workaround to set the property
42-
Object.defineProperty(node, 'files', {
43-
configurable: true,
44-
enumerable: true,
45-
writable: true,
46-
value: files,
47-
})
48-
}
49-
Object.assign(node, targetProperties)
50-
const window = getWindowFromNode(node)
51-
const EventConstructor = window[EventType] || window.Event
52-
let event
53-
/* istanbul ignore else */
54-
if (typeof EventConstructor === 'function') {
55-
event = new EventConstructor(eventName, eventInit)
56-
} else {
57-
// IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
58-
event = window.document.createEvent(EventType)
59-
const {bubbles, cancelable, detail, ...otherInit} = eventInit
60-
event.initEvent(eventName, bubbles, cancelable, detail)
61-
Object.keys(otherInit).forEach(eventKey => {
62-
event[eventKey] = otherInit[eventKey]
63-
})
64-
}
65-
66-
// DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568
67-
['dataTransfer', 'clipboardData'].forEach(dataTransferKey => {
68-
const dataTransferValue = eventInit[dataTransferKey];
69-
70-
if (typeof dataTransferValue === 'object') {
71-
/* istanbul ignore if */
72-
if (typeof window.DataTransfer === 'function') {
73-
Object.defineProperty(event, dataTransferKey, {
74-
value: Object.assign(new window.DataTransfer(), dataTransferValue)
75-
})
76-
} else {
77-
Object.defineProperty(event, dataTransferKey, {
78-
value: dataTransferValue
79-
})
80-
}
81-
}
82-
})
83-
84-
return event
85-
}
86-
91+
createEvent[key] = (node, init) =>
92+
createEvent(eventName, node, init, {EventType, defaultInit})
8793
fireEvent[key] = (node, init) => fireEvent(node, createEvent[key](node, init))
8894
})
8995

src/user-event/__mocks__/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function wrapWithTestData(fn) {
3333
}
3434

3535
const mockFireEvent = wrapWithTestData(actual.fireEvent)
36+
3637
for (const key of Object.keys(actual.fireEvent)) {
3738
if (typeof actual.fireEvent[key] === 'function') {
3839
mockFireEvent[key] = wrapWithTestData(actual.fireEvent[key], key)

src/user-event/__tests__/upload.js

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import {userEvent} from '../../'
2-
import {setup, addListeners} from './helpers/utils'
2+
import {setup} from './helpers/utils'
33

44
test('should fire the correct events for input', async () => {
55
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
66
const {element, getEventSnapshot} = setup('<input type="file" />')
77

88
await userEvent.upload(element, file)
99

10+
// NOTE: A known limitation is that it's impossible to set the
11+
// value of the input programmatically. The value in the browser
12+
// set by a user would be: `C:\\fakepath\\${file.name}`
1013
expect(getEventSnapshot()).toMatchInlineSnapshot(`
1114
Events fired on: input[value=""]
1215
@@ -23,48 +26,41 @@ test('should fire the correct events for input', async () => {
2326
input[value=""] - pointerup
2427
input[value=""] - mouseup: Left (0)
2528
input[value=""] - click: Left (0)
29+
input[value=""] - blur
30+
input[value=""] - focusout
31+
input[value=""] - focus
32+
input[value=""] - focusin
33+
input[value=""] - input
2634
input[value=""] - change
2735
`)
2836
})
2937

3038
test('should fire the correct events with label', async () => {
3139
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
3240

33-
const container = document.createElement('div')
34-
container.innerHTML = `
35-
<label for="element">Element</label>
36-
<input type="file" id="element" />
37-
`
38-
39-
const label = container.children[0]
40-
const input = container.children[1]
41-
const {getEventSnapshot: getLabelEventCalls} = addListeners(label)
42-
const {getEventSnapshot: getInputEventCalls} = addListeners(input)
41+
const {element, getEventSnapshot} = setup(`
42+
<form>
43+
<label for="element">Element</label>
44+
<input type="file" id="element" />
45+
</form>
46+
`)
4347

44-
await userEvent.upload(label, file)
48+
await userEvent.upload(element.querySelector('label'), file)
4549

46-
expect(getLabelEventCalls()).toMatchInlineSnapshot(`
47-
Events fired on: label[for="element"]
50+
expect(getEventSnapshot()).toMatchInlineSnapshot(`
51+
Events fired on: form
4852
4953
label[for="element"] - pointerover
50-
label[for="element"] - pointerenter
5154
label[for="element"] - mouseover: Left (0)
52-
label[for="element"] - mouseenter: Left (0)
5355
label[for="element"] - pointermove
5456
label[for="element"] - mousemove: Left (0)
5557
label[for="element"] - pointerdown
5658
label[for="element"] - mousedown: Left (0)
5759
label[for="element"] - pointerup
5860
label[for="element"] - mouseup: Left (0)
5961
label[for="element"] - click: Left (0)
60-
`)
61-
expect(getInputEventCalls()).toMatchInlineSnapshot(`
62-
Events fired on: input#element[value=""]
63-
6462
input#element[value=""] - click: Left (0)
65-
input#element[value=""] - focus
6663
input#element[value=""] - focusin
67-
input#element[value=""] - change
6864
`)
6965
})
7066

src/user-event/upload.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,53 @@
11
import {wrapAsync} from '../wrap-async'
2+
import {createEvent} from '../events'
23
import {fireEvent} from './utils'
34
import {click} from './click'
5+
import {blur} from './blur'
6+
import {focus} from './focus'
47

5-
async function upload(element, fileOrFiles, {clickInit, changeInit} = {}) {
8+
async function upload(element, fileOrFiles, init) {
69
if (element.disabled) return
710

811
let files
912
let input = element
1013

11-
await click(element, clickInit)
14+
await click(element, init)
1215
if (element.tagName === 'LABEL') {
1316
files = element.control.multiple ? fileOrFiles : [fileOrFiles]
1417
input = element.control
1518
} else {
1619
files = element.multiple ? fileOrFiles : [fileOrFiles]
1720
}
1821

19-
await fireEvent.change(input, {
20-
target: {
21-
files: {
22-
length: files.length,
23-
item: index => files[index],
24-
...files,
25-
},
26-
},
27-
...changeInit,
28-
})
22+
// blur fires when the file selector pops up
23+
await blur(element, init)
24+
// focus fires when they make their selection
25+
await focus(element, init)
26+
27+
// the event fired in the browser isn't actually an "input" or "change" event
28+
// but a new Event with a type set to "input" and "change"
29+
// Kinda odd...
30+
const inputFiles = {
31+
length: files.length,
32+
item: index => files[index],
33+
...files,
34+
}
35+
36+
await fireEvent(
37+
input,
38+
createEvent('input', input, {
39+
target: {files: inputFiles},
40+
...init,
41+
}),
42+
)
43+
44+
await fireEvent(
45+
input,
46+
createEvent('change', input, {
47+
target: {files: inputFiles},
48+
...init,
49+
}),
50+
)
2951
}
3052
upload = wrapAsync(upload)
3153

0 commit comments

Comments
 (0)