Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(upload): Add upload API #279

Merged
merged 5 commits into from May 15, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 42 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,44 @@ 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)`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument might be single file or array of files. I'm not sure about that, may be second arg must be array only? But personally I think it's inconvenient to always remember about wrapping file to an array.


Uploads file to an `<input>`. For uploading multiple files use `<input>` with
`multiple` attribute and the second `upload` argument must be array then.

```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 +338,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);
});
});
28 changes: 28 additions & 0 deletions src/index.js
Expand Up @@ -294,6 +294,34 @@ const userEvent = {
element.addEventListener("blur", fireChangeEvent);
},

upload(element, fileOrFiles, init) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make sense to allow configurability of both events?

Suggested change
upload(element, fileOrFiles, init) {
upload(element, fileOrFiles, {clickInit, changeInit}) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, looks reasonable but here I'm only following clickElement function API. It would be breaking change to all functions which use clickElement internally then. Is it desirable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the clickElement function is fine as is. No need to change it. It just seems odd to me that we'd allow you to initialize the click event and not the change event (which is actually what people will be thinking about when using this method).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I've catched it, thanks for guiding 😄

if (element.disabled) return;
const focusedElement = element.ownerDocument.activeElement;

let files;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This files variable depends on input multiple attribute. If it's true fileOrFiles arg assuming as array otherwise as singlefile. At first it relied on Array.isArray check but then I thought about passing test cases with multiple files and input without attribute multiple.


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, init);
}

fireEvent.change(element, {
target: {
files: {
length: files.length,
item: (index) => files[index],
...{ ...files },
vadimshvetsov marked this conversation as resolved.
Show resolved Hide resolved
},
},
});
},

tab({ shift = false, focusTrap = document } = {}) {
const focusableElements = focusTrap.querySelectorAll(
"input, button, select, textarea, a[href], [tabindex]"
Expand Down
3 changes: 3 additions & 0 deletions typings/index.d.ts
Expand Up @@ -11,11 +11,14 @@ export interface ITabUserOptions {

export type TargetElement = Element | Window;

export type FilesArgument = File | File[];

declare const userEvent: {
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?: MouseEventInit) => void;
type: (
element: TargetElement,
text: string,
Expand Down