Skip to content

Commit 92c46d4

Browse files
authored
Merge pull request testing-library#130 from testing-library/select-multiple
Select multiple
2 parents 0653587 + fcbe7a2 commit 92c46d4

File tree

4 files changed

+261
-3
lines changed

4 files changed

+261
-3
lines changed

.all-contributorsrc

+13-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@
5252
"contributions": [
5353
"doc"
5454
]
55+
},
56+
{
57+
"login": "michaellasky",
58+
"name": "Michael Lasky",
59+
"avatar_url": "https://avatars2.githubusercontent.com/u/6646599?v=4",
60+
"profile": "https://github.com/michaellasky",
61+
"contributions": [
62+
"code",
63+
"doc",
64+
"ideas"
65+
]
5566
}
56-
]
67+
],
68+
"commitConvention": "none"
5769
}

README.md

+36-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
[![Build Status](https://travis-ci.org/testing-library/user-event.svg?branch=master)](https://travis-ci.org/testing-library/user-event)
2121
[![Maintainability](https://api.codeclimate.com/v1/badges/75f1ff4397e994c6004e/maintainability)](https://codeclimate.com/github/testing-library/user-event/maintainability)
2222
[![Test Coverage](https://api.codeclimate.com/v1/badges/75f1ff4397e994c6004e/test_coverage)](https://codeclimate.com/github/testing-library/user-event/test_coverage)
23-
[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors)
23+
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors)
2424

2525
## The problem
2626

@@ -133,14 +133,48 @@ one character at the time. `false` is the default value.
133133
are typed. By default it's 0. You can use this option if your component has a
134134
different behavior for fast or slow users.
135135

136+
### `selectOptions(element, values)`
137+
138+
Selects the specified option(s) of a `<select>` or a `<select multiple>`
139+
element.
140+
141+
```jsx
142+
import React from "react";
143+
import { render } from "@testing-library/react";
144+
import userEvent from "@testing-library/user-event";
145+
146+
const { getByTestId } = render(
147+
<select multiple data-testid="select-multiple">
148+
<option data-testid="val1" value="1">
149+
1
150+
</option>
151+
<option data-testid="val2" value="2">
152+
2
153+
</option>
154+
<option data-testid="val3" value="3">
155+
3
156+
</option>
157+
</select>
158+
);
159+
160+
userEvent.selectOptions(getByTestId("select-multiple"), ["1", "3"]);
161+
162+
expect(getByTestId("val1").selected).toBe(true);
163+
expect(getByTestId("val2").selected).toBe(false);
164+
expect(getByTestId("val3").selected).toBe(true);
165+
```
166+
167+
The `values` parameter can be either an array of values or a singular scalar
168+
value.
169+
136170
## Contributors
137171

138172
Thanks goes to these wonderful people
139173
([emoji key](https://github.com/all-contributors/all-contributors#emoji-key)):
140174

141175
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
142176
<!-- prettier-ignore -->
143-
<table><tr><td align="center"><a href="https://twitter.com/Gpx"><img src="https://avatars0.githubusercontent.com/u/767959?v=4" width="100px;" alt="Giorgio Polvara"/><br /><sub><b>Giorgio Polvara</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3AGpx" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Documentation">📖</a> <a href="#ideas-Gpx" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-Gpx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#review-Gpx" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Tests">⚠️</a></td><td align="center"><a href="https://github.com/weyert"><img src="https://avatars3.githubusercontent.com/u/7049?v=4" width="100px;" alt="Weyert de Boer"/><br /><sub><b>Weyert de Boer</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Tests">⚠️</a></td><td align="center"><a href="https://github.com/twhitbeck"><img src="https://avatars2.githubusercontent.com/u/762471?v=4" width="100px;" alt="Tim Whitbeck"/><br /><sub><b>Tim Whitbeck</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Atwhitbeck" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=twhitbeck" title="Code">💻</a></td><td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4" width="100px;" alt="Michaël De Boey"/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=MichaelDeBoey" title="Documentation">📖</a></td></tr></table>
177+
<table><tr><td align="center"><a href="https://twitter.com/Gpx"><img src="https://avatars0.githubusercontent.com/u/767959?v=4" width="100px;" alt="Giorgio Polvara"/><br /><sub><b>Giorgio Polvara</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3AGpx" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Documentation">📖</a> <a href="#ideas-Gpx" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-Gpx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#review-Gpx" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Tests">⚠️</a></td><td align="center"><a href="https://github.com/weyert"><img src="https://avatars3.githubusercontent.com/u/7049?v=4" width="100px;" alt="Weyert de Boer"/><br /><sub><b>Weyert de Boer</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Tests">⚠️</a></td><td align="center"><a href="https://github.com/twhitbeck"><img src="https://avatars2.githubusercontent.com/u/762471?v=4" width="100px;" alt="Tim Whitbeck"/><br /><sub><b>Tim Whitbeck</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Atwhitbeck" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=twhitbeck" title="Code">💻</a></td><td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4" width="100px;" alt="Michaël De Boey"/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=MichaelDeBoey" title="Documentation">📖</a></td><td align="center"><a href="https://github.com/michaellasky"><img src="https://avatars2.githubusercontent.com/u/6646599?v=4" width="100px;" alt="Michael Lasky"/><br /><sub><b>Michael Lasky</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=michaellasky" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=michaellasky" title="Documentation">📖</a> <a href="#ideas-michaellasky" title="Ideas, Planning, & Feedback">🤔</a></td></tr></table>
144178

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

__tests__/selectoptions.js

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React from "react";
2+
import { render, cleanup, fireEvent } from "@testing-library/react";
3+
import "jest-dom/extend-expect";
4+
import userEvent from "../src";
5+
6+
afterEach(cleanup);
7+
8+
describe("userEvent.selectOptions", () => {
9+
it.each(["select", "select multiple"])(
10+
"should fire the correct events for <%s>",
11+
type => {
12+
const events = [];
13+
const eventsHandler = jest.fn(evt => events.push(evt.type));
14+
const multiple = type === "select multiple";
15+
const eventHandlers = {
16+
onMouseOver: eventsHandler,
17+
onMouseMove: eventsHandler,
18+
onMouseDown: eventsHandler,
19+
onFocus: eventsHandler,
20+
onMouseUp: eventsHandler,
21+
onClick: eventsHandler
22+
};
23+
24+
const { getByTestId } = render(
25+
<select {...{ ...eventHandlers, multiple }} data-testid="element">
26+
<option value="1">1</option>
27+
<option value="2">2</option>
28+
<option value="3">3</option>
29+
</select>
30+
);
31+
32+
userEvent.selectOptions(getByTestId("element"), "1");
33+
34+
expect(events).toEqual([
35+
"mouseover",
36+
"mousemove",
37+
"mousedown",
38+
"focus",
39+
"mouseup",
40+
"click",
41+
"mouseover", // The events repeat because we click on the child OPTION too
42+
"mousemove", // But these specifically are the events bubbling up to the <select>
43+
"mousedown",
44+
"focus",
45+
"mouseup",
46+
"click"
47+
]);
48+
}
49+
);
50+
51+
it("should fire the correct events on selected OPTION child with <select>", () => {
52+
function handleEvent(evt) {
53+
const optValue = parseInt(evt.target.value);
54+
events[optValue] = [...(events[optValue] || []), evt.type];
55+
}
56+
57+
const events = [];
58+
const eventsHandler = jest.fn(handleEvent);
59+
const eventHandlers = {
60+
onMouseOver: eventsHandler,
61+
onMouseMove: eventsHandler,
62+
onMouseDown: eventsHandler,
63+
onFocus: eventsHandler,
64+
onMouseUp: eventsHandler,
65+
onClick: eventsHandler
66+
};
67+
68+
const { getByTestId } = render(
69+
<select data-testid="element">
70+
<option {...eventHandlers} value="1">
71+
1
72+
</option>
73+
<option {...eventHandlers} value="2">
74+
2
75+
</option>
76+
<option {...eventHandlers} value="3">
77+
3
78+
</option>
79+
</select>
80+
);
81+
82+
userEvent.selectOptions(getByTestId("element"), ["2"]);
83+
84+
expect(events[1]).toBe(undefined);
85+
expect(events[3]).toBe(undefined);
86+
expect(events[2]).toEqual([
87+
"mouseover",
88+
"mousemove",
89+
"mousedown",
90+
"focus",
91+
"mouseup",
92+
"click"
93+
]);
94+
});
95+
96+
it("should fire the correct events on selected OPTION children with <select multiple>", () => {
97+
function handleEvent(evt) {
98+
const optValue = parseInt(evt.target.value);
99+
events[optValue] = [...(events[optValue] || []), evt.type];
100+
}
101+
102+
const events = [];
103+
const eventsHandler = jest.fn(handleEvent);
104+
const eventHandlers = {
105+
onMouseOver: eventsHandler,
106+
onMouseMove: eventsHandler,
107+
onMouseDown: eventsHandler,
108+
onFocus: eventsHandler,
109+
onMouseUp: eventsHandler,
110+
onClick: eventsHandler
111+
};
112+
113+
const { getByTestId } = render(
114+
<select multiple data-testid="element">
115+
<option {...eventHandlers} value="1">
116+
1
117+
</option>
118+
<option {...eventHandlers} value="2">
119+
2
120+
</option>
121+
<option {...eventHandlers} value="3">
122+
3
123+
</option>
124+
</select>
125+
);
126+
127+
userEvent.selectOptions(getByTestId("element"), ["1", "3"]);
128+
129+
expect(events[2]).toBe(undefined);
130+
expect(events[1]).toEqual([
131+
"mouseover",
132+
"mousemove",
133+
"mousedown",
134+
"focus",
135+
"mouseup",
136+
"click"
137+
]);
138+
139+
expect(events[3]).toEqual([
140+
"mouseover",
141+
"mousemove",
142+
"mousedown",
143+
"focus",
144+
"mouseup",
145+
"click"
146+
]);
147+
});
148+
149+
it("sets the selected prop on the selected OPTION", () => {
150+
const onSubmit = jest.fn();
151+
152+
const { getByTestId } = render(
153+
<form onSubmit={onSubmit}>
154+
<select multiple data-testid="element">
155+
<option data-testid="val1" value="1">
156+
1
157+
</option>
158+
<option data-testid="val2" value="2">
159+
2
160+
</option>
161+
<option data-testid="val3" value="3">
162+
3
163+
</option>
164+
</select>
165+
</form>
166+
);
167+
168+
userEvent.selectOptions(getByTestId("element"), ["1", "3"]);
169+
170+
expect(getByTestId("val1").selected).toBe(true);
171+
expect(getByTestId("val2").selected).toBe(false);
172+
expect(getByTestId("val3").selected).toBe(true);
173+
});
174+
});

src/index.js

+38
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ function dblClickCheckbox(checkbox) {
8282
fireEvent.change(checkbox);
8383
}
8484

85+
function selectOption(option) {
86+
fireEvent.mouseOver(option);
87+
fireEvent.mouseMove(option);
88+
fireEvent.mouseDown(option);
89+
fireEvent.focus(option);
90+
fireEvent.mouseUp(option);
91+
fireEvent.click(option);
92+
93+
option.selected = true;
94+
}
95+
8596
const userEvent = {
8697
click(element) {
8798
const focusedElement = document.activeElement;
@@ -130,6 +141,33 @@ const userEvent = {
130141
wasAnotherElementFocused && focusedElement.blur();
131142
},
132143

144+
selectOptions(element, values) {
145+
const focusedElement = document.activeElement;
146+
const wasAnotherElementFocused =
147+
focusedElement !== document.body && focusedElement !== element;
148+
if (wasAnotherElementFocused) {
149+
fireEvent.mouseMove(focusedElement);
150+
fireEvent.mouseLeave(focusedElement);
151+
}
152+
153+
clickElement(element);
154+
155+
const valArray = Array.isArray(values) ? values : [values];
156+
const selectedOptions = Array.from(element.children).filter(
157+
opt => opt.tagName === "OPTION" && valArray.includes(opt.value)
158+
);
159+
160+
if (selectedOptions.length > 0) {
161+
if (element.multiple) {
162+
selectedOptions.forEach(option => selectOption(option));
163+
} else {
164+
selectOption(selectedOptions[0]);
165+
}
166+
}
167+
168+
wasAnotherElementFocused && focusedElement.blur();
169+
},
170+
133171
async type(element, text, userOpts = {}) {
134172
const defaultOpts = {
135173
allAtOnce: false,

0 commit comments

Comments
 (0)