Skip to content

Commit f77c996

Browse files
feat(upload): Add upload API (testing-library#279)
1 parent e2272ab commit f77c996

File tree

5 files changed

+239
-17
lines changed

5 files changed

+239
-17
lines changed

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ test("click", () => {
9090
You can also ctrlClick / shiftClick etc with
9191

9292
```js
93-
userEvent.click(elem, { ctrlKey: true, shiftKey: true })
93+
userEvent.click(elem, { ctrlKey: true, shiftKey: true });
9494
```
9595

96-
See the [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
96+
See the
97+
[`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
9798
constructor documentation for more options.
9899

99100
### `dblClick(element)`
@@ -140,6 +141,45 @@ one character at the time. `false` is the default value.
140141
are typed. By default it's 0. You can use this option if your component has a
141142
different behavior for fast or slow users.
142143

144+
### `upload(element, file, [{ clickInit, changeInit }])`
145+
146+
Uploads file to an `<input>`. For uploading multiple files use `<input>` with
147+
`multiple` attribute and the second `upload` argument must be array then. Also
148+
it's possible to initialize click or change event with using third argument.
149+
150+
```jsx
151+
import React from "react";
152+
import { render, screen } from "@testing-library/react";
153+
import userEvent from "@testing-library/user-event";
154+
155+
test("upload file", () => {
156+
const file = new File(["hello"], "hello.png", { type: "image/png" });
157+
158+
render(<input type="file" data-testid="upload" />);
159+
160+
userEvent.upload(screen.getByTestId("upload"), file);
161+
162+
expect(input.files[0]).toStrictEqual(file);
163+
expect(input.files.item(0)).toStrictEqual(file);
164+
expect(input.files).toHaveLength(1);
165+
});
166+
167+
test("upload multiple files", () => {
168+
const files = [
169+
new File(["hello"], "hello.png", { type: "image/png" }),
170+
new File(["there"], "there.png", { type: "image/png" }),
171+
];
172+
173+
render(<input type="file" multiple data-testid="upload" />);
174+
175+
userEvent.upload(screen.getByTestId("upload"), files);
176+
177+
expect(input.files).toHaveLength(2);
178+
expect(input.files[0]).toStrictEqual(files[0]);
179+
expect(input.files[1]).toStrictEqual(files[1]);
180+
});
181+
```
182+
143183
### `clear(element)`
144184

145185
Selects the text inside an `<input>` or `<textarea>` and deletes it.
@@ -299,6 +339,7 @@ Thanks goes to these wonderful people
299339

300340
<!-- markdownlint-enable -->
301341
<!-- prettier-ignore-end -->
342+
302343
<!-- ALL-CONTRIBUTORS-LIST:END -->
303344

304345
This project follows the

__tests__/react/clear.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { cleanup, render, wait, fireEvent } from "@testing-library/react";
2+
import { cleanup, render } from "@testing-library/react";
33
import "@testing-library/jest-dom/extend-expect";
44
import userEvent from "../../src";
55

__tests__/react/upload.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React from "react";
2+
import { cleanup, render, fireEvent } from "@testing-library/react";
3+
import "@testing-library/jest-dom/extend-expect";
4+
import userEvent from "../../src";
5+
6+
afterEach(cleanup);
7+
8+
describe("userEvent.upload", () => {
9+
it("should fire the correct events for input", () => {
10+
const file = new File(["hello"], "hello.png", { type: "image/png" });
11+
const events = [];
12+
const eventsHandler = jest.fn((evt) => events.push(evt.type));
13+
const eventHandlers = {
14+
onMouseOver: eventsHandler,
15+
onMouseMove: eventsHandler,
16+
onMouseDown: eventsHandler,
17+
onFocus: eventsHandler,
18+
onMouseUp: eventsHandler,
19+
onClick: eventsHandler,
20+
};
21+
22+
const { getByTestId } = render(
23+
<input type="file" data-testid="element" {...eventHandlers} />
24+
);
25+
26+
userEvent.upload(getByTestId("element"), file);
27+
28+
expect(events).toEqual([
29+
"mouseover",
30+
"mousemove",
31+
"mousedown",
32+
"focus",
33+
"mouseup",
34+
"click",
35+
]);
36+
});
37+
38+
it("should fire the correct events with label", () => {
39+
const file = new File(["hello"], "hello.png", { type: "image/png" });
40+
41+
const inputEvents = [];
42+
const labelEvents = [];
43+
const eventsHandler = (events) => jest.fn((evt) => events.push(evt.type));
44+
45+
const getEventHandlers = (events) => ({
46+
onMouseOver: eventsHandler(events),
47+
onMouseMove: eventsHandler(events),
48+
onMouseDown: eventsHandler(events),
49+
onFocus: eventsHandler(events),
50+
onMouseUp: eventsHandler(events),
51+
onClick: eventsHandler(events),
52+
});
53+
54+
const { getByTestId } = render(
55+
<>
56+
<label
57+
htmlFor="element"
58+
data-testid="label"
59+
{...getEventHandlers(labelEvents)}
60+
>
61+
Element
62+
</label>
63+
<input type="file" id="element" {...getEventHandlers(inputEvents)} />
64+
</>
65+
);
66+
67+
userEvent.upload(getByTestId("label"), file);
68+
69+
expect(inputEvents).toEqual(["focus", "click"]);
70+
expect(labelEvents).toEqual([
71+
"mouseover",
72+
"mousemove",
73+
"mousedown",
74+
"mouseup",
75+
"click",
76+
]);
77+
});
78+
79+
it("should upload the file", () => {
80+
const file = new File(["hello"], "hello.png", { type: "image/png" });
81+
const { getByTestId } = render(<input type="file" data-testid="element" />);
82+
const input = getByTestId("element");
83+
84+
userEvent.upload(input, file);
85+
86+
expect(input.files[0]).toStrictEqual(file);
87+
expect(input.files.item(0)).toStrictEqual(file);
88+
expect(input.files).toHaveLength(1);
89+
90+
fireEvent.change(input, {
91+
target: { files: { item: () => {}, length: 0 } },
92+
});
93+
94+
expect(input.files[0]).toBeUndefined();
95+
expect(input.files.item[0]).toBeUndefined();
96+
expect(input.files).toHaveLength(0);
97+
});
98+
99+
it("should upload multiple files", () => {
100+
const files = [
101+
new File(["hello"], "hello.png", { type: "image/png" }),
102+
new File(["there"], "there.png", { type: "image/png" }),
103+
];
104+
const { getByTestId } = render(
105+
<input type="file" multiple data-testid="element" />
106+
);
107+
const input = getByTestId("element");
108+
109+
userEvent.upload(input, files);
110+
111+
expect(input.files[0]).toStrictEqual(files[0]);
112+
expect(input.files.item(0)).toStrictEqual(files[0]);
113+
expect(input.files[1]).toStrictEqual(files[1]);
114+
expect(input.files.item(1)).toStrictEqual(files[1]);
115+
expect(input.files).toHaveLength(2);
116+
117+
fireEvent.change(input, {
118+
target: { files: { item: () => {}, length: 0 } },
119+
});
120+
121+
expect(input.files[0]).toBeUndefined();
122+
expect(input.files.item[0]).toBeUndefined();
123+
expect(input.files).toHaveLength(0);
124+
});
125+
126+
it("should not upload when is disabled", () => {
127+
const file = new File(["hello"], "hello.png", { type: "image/png" });
128+
const { getByTestId } = render(
129+
<input type="file" data-testid="element" disabled />
130+
);
131+
132+
const input = getByTestId("element");
133+
134+
userEvent.upload(input, file);
135+
136+
expect(input.files[0]).toBeUndefined();
137+
expect(input.files.item[0]).toBeUndefined();
138+
expect(input.files).toHaveLength(0);
139+
});
140+
});

src/index.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,35 @@ const userEvent = {
294294
element.addEventListener("blur", fireChangeEvent);
295295
},
296296

297+
upload(element, fileOrFiles, { clickInit, changeInit } = {}) {
298+
if (element.disabled) return;
299+
const focusedElement = element.ownerDocument.activeElement;
300+
301+
let files;
302+
303+
if (element.tagName === "LABEL") {
304+
clickLabel(element);
305+
const inputElement = element.htmlFor
306+
? document.getElementById(element.htmlFor)
307+
: querySelector("input");
308+
files = inputElement.multiple ? fileOrFiles : [fileOrFiles];
309+
} else {
310+
files = element.multiple ? fileOrFiles : [fileOrFiles];
311+
clickElement(element, focusedElement, clickInit);
312+
}
313+
314+
fireEvent.change(element, {
315+
target: {
316+
files: {
317+
length: files.length,
318+
item: (index) => files[index],
319+
...files,
320+
},
321+
},
322+
...changeInit,
323+
});
324+
},
325+
297326
tab({ shift = false, focusTrap = document } = {}) {
298327
const focusableElements = focusTrap.querySelectorAll(
299328
"input, button, select, textarea, a[href], [tabindex]"

typings/index.d.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
// Definitions by: Wu Haotian <https://github.com/whtsky>
22
export interface IUserOptions {
3-
allAtOnce?: boolean;
4-
delay?: number;
3+
allAtOnce?: boolean;
4+
delay?: number;
55
}
66

77
export interface ITabUserOptions {
8-
shift?: boolean;
9-
focusTrap?: Document | Element;
8+
shift?: boolean;
9+
focusTrap?: Document | Element;
1010
}
1111

1212
export type TargetElement = Element | Window;
1313

14+
export type FilesArgument = File | File[];
15+
16+
export type UploadInitArgument = {
17+
clickInit?: MouseEventInit;
18+
changeInit?: Event;
19+
};
20+
1421
declare const userEvent: {
15-
clear: (element: TargetElement) => void;
16-
click: (element: TargetElement, init?: MouseEventInit) => void;
17-
dblClick: (element: TargetElement) => void;
18-
selectOptions: (element: TargetElement, values: string | string[]) => void;
19-
type: (
20-
element: TargetElement,
21-
text: string,
22-
userOpts?: IUserOptions
23-
) => Promise<void>;
24-
tab: (userOpts?: ITabUserOptions) => void;
22+
clear: (element: TargetElement) => void;
23+
click: (element: TargetElement, init?: MouseEventInit) => void;
24+
dblClick: (element: TargetElement) => void;
25+
selectOptions: (element: TargetElement, values: string | string[]) => void;
26+
upload: (
27+
element: TargetElement,
28+
files: FilesArgument,
29+
init?: UploadInitArgument
30+
) => void;
31+
type: (
32+
element: TargetElement,
33+
text: string,
34+
userOpts?: IUserOptions
35+
) => Promise<void>;
36+
tab: (userOpts?: ITabUserOptions) => void;
2537
};
2638

2739
export default userEvent;

0 commit comments

Comments
 (0)