Skip to content

Commit

Permalink
feat(upload): Add upload API (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
vadimshvetsov committed May 15, 2020
1 parent e2272ab commit f77c996
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 17 deletions.
45 changes: 43 additions & 2 deletions README.md
Expand Up @@ -90,10 +90,11 @@ test("click", () => {
You can also ctrlClick / shiftClick etc with

```js
userEvent.click(elem, { ctrlKey: true, shiftKey: true })
userEvent.click(elem, { ctrlKey: true, shiftKey: true });
```

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

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

### `upload(element, file, [{ clickInit, changeInit }])`

Uploads file to an `<input>`. For uploading multiple files use `<input>` with
`multiple` attribute and the second `upload` argument must be array then. Also
it's possible to initialize click or change event with using third argument.

```jsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("upload file", () => {
const file = new File(["hello"], "hello.png", { type: "image/png" });

render(<input type="file" data-testid="upload" />);

userEvent.upload(screen.getByTestId("upload"), file);

expect(input.files[0]).toStrictEqual(file);
expect(input.files.item(0)).toStrictEqual(file);
expect(input.files).toHaveLength(1);
});

test("upload multiple files", () => {
const files = [
new File(["hello"], "hello.png", { type: "image/png" }),
new File(["there"], "there.png", { type: "image/png" }),
];

render(<input type="file" multiple data-testid="upload" />);

userEvent.upload(screen.getByTestId("upload"), files);

expect(input.files).toHaveLength(2);
expect(input.files[0]).toStrictEqual(files[0]);
expect(input.files[1]).toStrictEqual(files[1]);
});
```

### `clear(element)`

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

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the
Expand Down
2 changes: 1 addition & 1 deletion __tests__/react/clear.js
@@ -1,5 +1,5 @@
import React from "react";
import { cleanup, render, wait, fireEvent } from "@testing-library/react";
import { cleanup, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import userEvent from "../../src";

Expand Down
140 changes: 140 additions & 0 deletions __tests__/react/upload.js
@@ -0,0 +1,140 @@
import React from "react";
import { cleanup, render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import userEvent from "../../src";

afterEach(cleanup);

describe("userEvent.upload", () => {
it("should fire the correct events for input", () => {
const file = new File(["hello"], "hello.png", { type: "image/png" });
const events = [];
const eventsHandler = jest.fn((evt) => events.push(evt.type));
const eventHandlers = {
onMouseOver: eventsHandler,
onMouseMove: eventsHandler,
onMouseDown: eventsHandler,
onFocus: eventsHandler,
onMouseUp: eventsHandler,
onClick: eventsHandler,
};

const { getByTestId } = render(
<input type="file" data-testid="element" {...eventHandlers} />
);

userEvent.upload(getByTestId("element"), file);

expect(events).toEqual([
"mouseover",
"mousemove",
"mousedown",
"focus",
"mouseup",
"click",
]);
});

it("should fire the correct events with label", () => {
const file = new File(["hello"], "hello.png", { type: "image/png" });

const inputEvents = [];
const labelEvents = [];
const eventsHandler = (events) => jest.fn((evt) => events.push(evt.type));

const getEventHandlers = (events) => ({
onMouseOver: eventsHandler(events),
onMouseMove: eventsHandler(events),
onMouseDown: eventsHandler(events),
onFocus: eventsHandler(events),
onMouseUp: eventsHandler(events),
onClick: eventsHandler(events),
});

const { getByTestId } = render(
<>
<label
htmlFor="element"
data-testid="label"
{...getEventHandlers(labelEvents)}
>
Element
</label>
<input type="file" id="element" {...getEventHandlers(inputEvents)} />
</>
);

userEvent.upload(getByTestId("label"), file);

expect(inputEvents).toEqual(["focus", "click"]);
expect(labelEvents).toEqual([
"mouseover",
"mousemove",
"mousedown",
"mouseup",
"click",
]);
});

it("should upload the file", () => {
const file = new File(["hello"], "hello.png", { type: "image/png" });
const { getByTestId } = render(<input type="file" data-testid="element" />);
const input = getByTestId("element");

userEvent.upload(input, file);

expect(input.files[0]).toStrictEqual(file);
expect(input.files.item(0)).toStrictEqual(file);
expect(input.files).toHaveLength(1);

fireEvent.change(input, {
target: { files: { item: () => {}, length: 0 } },
});

expect(input.files[0]).toBeUndefined();
expect(input.files.item[0]).toBeUndefined();
expect(input.files).toHaveLength(0);
});

it("should upload multiple files", () => {
const files = [
new File(["hello"], "hello.png", { type: "image/png" }),
new File(["there"], "there.png", { type: "image/png" }),
];
const { getByTestId } = render(
<input type="file" multiple data-testid="element" />
);
const input = getByTestId("element");

userEvent.upload(input, files);

expect(input.files[0]).toStrictEqual(files[0]);
expect(input.files.item(0)).toStrictEqual(files[0]);
expect(input.files[1]).toStrictEqual(files[1]);
expect(input.files.item(1)).toStrictEqual(files[1]);
expect(input.files).toHaveLength(2);

fireEvent.change(input, {
target: { files: { item: () => {}, length: 0 } },
});

expect(input.files[0]).toBeUndefined();
expect(input.files.item[0]).toBeUndefined();
expect(input.files).toHaveLength(0);
});

it("should not upload when is disabled", () => {
const file = new File(["hello"], "hello.png", { type: "image/png" });
const { getByTestId } = render(
<input type="file" data-testid="element" disabled />
);

const input = getByTestId("element");

userEvent.upload(input, file);

expect(input.files[0]).toBeUndefined();
expect(input.files.item[0]).toBeUndefined();
expect(input.files).toHaveLength(0);
});
});
29 changes: 29 additions & 0 deletions src/index.js
Expand Up @@ -294,6 +294,35 @@ const userEvent = {
element.addEventListener("blur", fireChangeEvent);
},

upload(element, fileOrFiles, { clickInit, changeInit } = {}) {
if (element.disabled) return;
const focusedElement = element.ownerDocument.activeElement;

let files;

if (element.tagName === "LABEL") {
clickLabel(element);
const inputElement = element.htmlFor
? document.getElementById(element.htmlFor)
: querySelector("input");
files = inputElement.multiple ? fileOrFiles : [fileOrFiles];
} else {
files = element.multiple ? fileOrFiles : [fileOrFiles];
clickElement(element, focusedElement, clickInit);
}

fireEvent.change(element, {
target: {
files: {
length: files.length,
item: (index) => files[index],
...files,
},
},
...changeInit,
});
},

tab({ shift = false, focusTrap = document } = {}) {
const focusableElements = focusTrap.querySelectorAll(
"input, button, select, textarea, a[href], [tabindex]"
Expand Down
40 changes: 26 additions & 14 deletions typings/index.d.ts
@@ -1,27 +1,39 @@
// Definitions by: Wu Haotian <https://github.com/whtsky>
export interface IUserOptions {
allAtOnce?: boolean;
delay?: number;
allAtOnce?: boolean;
delay?: number;
}

export interface ITabUserOptions {
shift?: boolean;
focusTrap?: Document | Element;
shift?: boolean;
focusTrap?: Document | Element;
}

export type TargetElement = Element | Window;

export type FilesArgument = File | File[];

export type UploadInitArgument = {
clickInit?: MouseEventInit;
changeInit?: Event;
};

declare const userEvent: {
clear: (element: TargetElement) => void;
click: (element: TargetElement, init?: MouseEventInit) => void;
dblClick: (element: TargetElement) => void;
selectOptions: (element: TargetElement, values: string | string[]) => void;
type: (
element: TargetElement,
text: string,
userOpts?: IUserOptions
) => Promise<void>;
tab: (userOpts?: ITabUserOptions) => void;
clear: (element: TargetElement) => void;
click: (element: TargetElement, init?: MouseEventInit) => void;
dblClick: (element: TargetElement) => void;
selectOptions: (element: TargetElement, values: string | string[]) => void;
upload: (
element: TargetElement,
files: FilesArgument,
init?: UploadInitArgument
) => void;
type: (
element: TargetElement,
text: string,
userOpts?: IUserOptions
) => Promise<void>;
tab: (userOpts?: ITabUserOptions) => void;
};

export default userEvent;

0 comments on commit f77c996

Please sign in to comment.