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 all 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
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;
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, 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure in this type, took it from there

};

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;