Skip to content

Commit e536ba3

Browse files
authored
Merge pull request #80 from adhocteam/js-98-multiselect-array-value
Multiselect's value is an array of strings
2 parents 24832d7 + 65b2db7 commit e536ba3

File tree

10 files changed

+172
-41
lines changed

10 files changed

+172
-41
lines changed

frontend/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@
4848
},
4949
"eslintConfig": {
5050
"root": true,
51+
"plugins": [
52+
"testing-library"
53+
],
5154
"extends": [
5255
"airbnb",
53-
"plugin:jest/recommended"
56+
"plugin:jest/recommended",
57+
"plugin:testing-library/react"
5458
],
5559
"ignorePatterns": [
5660
"build/*",
@@ -117,6 +121,7 @@
117121
"eslint-plugin-jsx-a11y": "^6.3.1",
118122
"eslint-plugin-react": "^7.20.5",
119123
"eslint-plugin-react-hooks": "^4.0.8",
124+
"eslint-plugin-testing-library": "^3.10.1",
120125
"fetch-mock": "^9.10.7",
121126
"history": "^5.0.0",
122127
"jest-fetch-mock": "^3.0.3",

frontend/src/.eslintrc

Whitespace-only changes.

frontend/src/__tests__/App.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import '@testing-library/jest-dom';
33
import join from 'url-join';
44
import {
5-
screen, render, waitFor, fireEvent,
5+
screen, render, fireEvent,
66
} from '@testing-library/react';
77
import fetchMock from 'fetch-mock';
88
import App from '../App';
@@ -21,13 +21,13 @@ describe('App', () => {
2121
});
2222

2323
it('displays the logout button', async () => {
24-
expect(await waitFor(() => screen.getByText('Logout'))).toBeVisible();
24+
expect(await screen.findByText('Logout')).toBeVisible();
2525
});
2626

2727
it('can log the user out when "logout" is pressed', async () => {
28-
const logout = await waitFor(() => screen.getByText('Logout'));
28+
const logout = await screen.findByText('Logout');
2929
fireEvent.click(logout);
30-
expect(await waitFor(() => screen.getByText('HSES Login'))).toBeVisible();
30+
expect(await screen.findByText('HSES Login')).toBeVisible();
3131
});
3232
});
3333

@@ -38,7 +38,7 @@ describe('App', () => {
3838
});
3939

4040
it('displays the login button', async () => {
41-
expect(await waitFor(() => screen.getByText('HSES Login'))).toBeVisible();
41+
expect(await screen.findByText('HSES Login')).toBeVisible();
4242
});
4343
});
4444
});

frontend/src/components/MultiSelect.js

+33-14
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,44 @@ const styles = {
4444
function MultiSelect({
4545
label, name, options, disabled, control, required,
4646
}) {
47+
const findLabel = (value) => {
48+
const opt = options.find((o) => o.value === value);
49+
if (!opt) {
50+
return value;
51+
}
52+
return opt.label;
53+
};
54+
4755
return (
4856
<Label>
4957
{label}
5058
<Controller
51-
render={({ onChange, value }) => (
52-
<Select
53-
id={name}
54-
value={value}
55-
onChange={onChange}
56-
styles={styles}
57-
components={{ DropdownIndicator }}
58-
options={options}
59-
isDisabled={disabled}
60-
placeholder=""
61-
isMulti
62-
/>
63-
)}
59+
render={({ onChange, value }) => {
60+
let values = value;
61+
if (value) {
62+
values = value.map((v) => ({
63+
value: v, label: findLabel(v),
64+
}));
65+
}
66+
return (
67+
<Select
68+
id={name}
69+
value={values}
70+
onChange={(e) => {
71+
const newValue = e ? e.map((v) => v.value) : null;
72+
onChange(newValue);
73+
}}
74+
styles={styles}
75+
components={{ DropdownIndicator }}
76+
options={options}
77+
isDisabled={disabled}
78+
placeholder=""
79+
isMulti
80+
/>
81+
);
82+
}}
6483
control={control}
65-
defaultValue=""
84+
defaultValue={[]}
6685
rules={{
6786
required,
6887
}}

frontend/src/components/Navigator/__tests__/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('Navigator', () => {
4747
renderNavigator();
4848
const firstInput = screen.getByTestId('first');
4949
userEvent.click(firstInput);
50-
const second = await waitFor(() => screen.getByText('second page'));
50+
const second = await screen.findByText('second page');
5151
userEvent.click(second);
5252
const first = screen.getByText('first page');
5353
await waitFor(() => expect(within(first.nextSibling).getByText('In progress')).toBeVisible());

frontend/src/components/__tests__/DatePicker.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('DatePicker', () => {
5858
render(<RenderDatePicker />);
5959
const openCalendar = screen.getByRole('button');
6060
fireEvent.click(openCalendar);
61-
const button = await waitFor(() => screen.getByLabelText('Move backward to switch to the previous month.'));
61+
const button = await screen.findByLabelText('Move backward to switch to the previous month.');
6262
await waitFor(() => expect(button).toBeVisible());
6363
});
6464
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import '@testing-library/jest-dom';
2+
import React from 'react';
3+
import { render, screen } from '@testing-library/react';
4+
import selectEvent from 'react-select-event';
5+
import { act } from 'react-dom/test-utils';
6+
import { useForm } from 'react-hook-form';
7+
import userEvent from '@testing-library/user-event';
8+
9+
import MultiSelect from '../MultiSelect';
10+
11+
const options = [
12+
{ label: 'one', value: 'one' },
13+
{ label: 'two', value: 'two' },
14+
];
15+
16+
describe('MultiSelect', () => {
17+
// eslint-disable-next-line react/prop-types
18+
const TestMultiSelect = ({ onSubmit, defaultValues }) => {
19+
const { control, handleSubmit } = useForm({
20+
defaultValues,
21+
mode: 'all',
22+
});
23+
24+
const submit = (data) => {
25+
onSubmit(data);
26+
};
27+
28+
return (
29+
<form onSubmit={handleSubmit(submit)}>
30+
<MultiSelect
31+
control={control}
32+
label="label"
33+
name="name"
34+
options={options}
35+
required={false}
36+
/>
37+
<button data-testid="submit" type="submit">submit</button>
38+
</form>
39+
);
40+
};
41+
42+
it('selected value is an array of strings', async () => {
43+
const onSubmit = jest.fn();
44+
render(<TestMultiSelect onSubmit={onSubmit} />);
45+
await selectEvent.select(screen.getByLabelText('label'), ['one']);
46+
await act(async () => {
47+
userEvent.click(screen.getByTestId('submit'));
48+
});
49+
expect(onSubmit).toHaveBeenCalledWith({ name: ['one'] });
50+
});
51+
52+
it('null values do not cause an error', async () => {
53+
const onSubmit = jest.fn();
54+
render(<TestMultiSelect onSubmit={onSubmit} />);
55+
const select = screen.getByLabelText('label');
56+
await selectEvent.select(select, ['one']);
57+
await selectEvent.clearAll(select);
58+
await act(async () => {
59+
userEvent.click(screen.getByTestId('submit'));
60+
});
61+
expect(onSubmit).toHaveBeenCalledWith({ name: null });
62+
});
63+
});

frontend/src/pages/ActivityReport/__tests__/index.js

+13-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const formData = () => ({
1414
'activity-type': ['training'],
1515
duration: '1',
1616
'end-date': moment(),
17-
grantees: 'Grantee Name 1',
17+
grantees: ['Grantee Name 1'],
1818
'number-of-participants': '1',
1919
'participant-category': 'grantee',
2020
participants: ['CEO / CFO / Executive'],
@@ -31,33 +31,33 @@ describe('ActivityReport', () => {
3131
describe('changes the participant selection to', () => {
3232
it('Grantee', async () => {
3333
render(<ActivityReport />);
34-
const information = await waitFor(() => screen.getByRole('group', { name: 'Who was the activity for?' }));
34+
const information = await screen.findByRole('group', { name: 'Who was the activity for?' });
3535
const grantee = within(information).getByLabelText('Grantee');
3636
fireEvent.click(grantee);
37-
const granteeSelectbox = await waitFor(() => screen.getByRole('textbox', { name: 'Grantee name(s)' }));
37+
const granteeSelectbox = await screen.findByRole('textbox', { name: 'Grantee name(s)' });
3838
reactSelectEvent.openMenu(granteeSelectbox);
39-
expect(await waitFor(() => screen.getByText(withText('Grantee Name 1')))).toBeVisible();
39+
expect(await screen.findByText(withText('Grantee Name 1'))).toBeVisible();
4040
});
4141

4242
it('Non-grantee', async () => {
4343
render(<ActivityReport />);
44-
const information = await waitFor(() => screen.getByRole('group', { name: 'Who was the activity for?' }));
44+
const information = await screen.findByRole('group', { name: 'Who was the activity for?' });
4545
const nonGrantee = within(information).getByLabelText('Non-Grantee');
4646
fireEvent.click(nonGrantee);
47-
const granteeSelectbox = await waitFor(() => screen.getByRole('textbox', { name: 'Grantee name(s)' }));
47+
const granteeSelectbox = await screen.findByRole('textbox', { name: 'Grantee name(s)' });
4848
reactSelectEvent.openMenu(granteeSelectbox);
49-
expect(await waitFor(() => screen.getByText(withText('QRIS System')))).toBeVisible();
49+
expect(await screen.findByText(withText('QRIS System'))).toBeVisible();
5050
});
5151
});
5252

5353
it('when non-grantee is selected', async () => {
5454
render(<ActivityReport />);
5555
const enabled = screen.getByRole('textbox', { name: 'Grantee name(s)' });
5656
expect(enabled).toBeDisabled();
57-
const information = await waitFor(() => screen.getByRole('group', { name: 'Who was the activity for?' }));
57+
const information = await screen.findByRole('group', { name: 'Who was the activity for?' });
5858
const grantee = within(information).getByLabelText('Grantee');
5959
fireEvent.click(grantee);
60-
const disabled = await waitFor(() => screen.getByRole('textbox', { name: 'Grantee name(s)' }));
60+
const disabled = await screen.findByRole('textbox', { name: 'Grantee name(s)' });
6161
expect(disabled).not.toBeDisabled();
6262
});
6363
});
@@ -68,8 +68,8 @@ describe('ActivityReport', () => {
6868
delete data['activity-method'];
6969

7070
render(<ActivityReport initialData={data} />);
71-
expect(await waitFor(() => screen.getByText('Continue'))).toBeDisabled();
72-
const box = await waitFor(() => screen.getByLabelText('Virtual'));
71+
expect(await screen.findByText('Continue')).toBeDisabled();
72+
const box = await screen.findByLabelText('Virtual');
7373
fireEvent.click(box);
7474
await waitFor(() => expect(screen.getByText('Continue')).not.toBeDisabled());
7575
});
@@ -81,8 +81,8 @@ describe('ActivityReport', () => {
8181
delete data['activity-type'];
8282

8383
render(<ActivityReport initialData={data} />);
84-
expect(await waitFor(() => screen.getByText('Continue'))).toBeDisabled();
85-
const box = await waitFor(() => screen.getByLabelText('Training'));
84+
expect(await screen.findByText('Continue')).toBeDisabled();
85+
const box = await screen.findByLabelText('Training');
8686
fireEvent.click(box);
8787
await waitFor(() => expect(screen.getByText('Continue')).not.toBeDisabled());
8888
});

frontend/src/pages/Admin/__tests__/index.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
22
import React from 'react';
33
import { Router } from 'react-router';
44
import {
5-
render, screen, waitFor, within,
5+
render, screen, within,
66
} from '@testing-library/react';
77
import userEvent from '@testing-library/user-event';
88
import { createMemoryHistory } from 'history';
@@ -17,7 +17,7 @@ describe('UserInfo', () => {
1717
});
1818

1919
it('user list is filterable', async () => {
20-
const filter = await waitFor(() => screen.getByLabelText('Filter Users'));
20+
const filter = await screen.findByLabelText('Filter Users');
2121
userEvent.type(filter, 'Harry');
2222
const sideNav = screen.getByTestId('sidenav');
2323
const links = within(sideNav).getAllByRole('link');
@@ -26,27 +26,27 @@ describe('UserInfo', () => {
2626
});
2727

2828
it('new user button properly sets url', async () => {
29-
const newUser = await waitFor(() => screen.getByText('Create New User'));
29+
const newUser = await screen.findByText('Create New User');
3030
userEvent.click(newUser);
3131
expect(history.location.pathname).toBe('/admin/new');
3232
});
3333

3434
it('allows a user to be selected', async () => {
35-
const button = await waitFor(() => screen.getByText('Harry Potter'));
35+
const button = await screen.findByText('Harry Potter');
3636
userEvent.click(button);
3737
expect(history.location.pathname).toBe('/admin/3');
3838
});
3939
});
4040

4141
it('displays a new user', async () => {
4242
render(<Router history={history}><Admin match={{ path: '', url: '', params: { userId: 'new' } }} /></Router>);
43-
const userInfo = await waitFor(() => screen.getByRole('group', { name: 'User Info' }));
43+
const userInfo = await screen.findByRole('group', { name: 'User Info' });
4444
expect(userInfo).toBeVisible();
4545
});
4646

4747
it('displays an existing user', async () => {
4848
render(<Router history={history}><Admin match={{ path: '', url: '', params: { userId: '3' } }} /></Router>);
49-
const userInfo = await waitFor(() => screen.getByRole('group', { name: 'User Info' }));
49+
const userInfo = await screen.findByRole('group', { name: 'User Info' });
5050
expect(userInfo).toBeVisible();
5151
});
5252
});

frontend/yarn.lock

+44
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,17 @@
18261826
eslint-scope "^5.0.0"
18271827
eslint-utils "^2.0.0"
18281828

1829+
"@typescript-eslint/experimental-utils@^3.10.1":
1830+
version "3.10.1"
1831+
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
1832+
integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==
1833+
dependencies:
1834+
"@types/json-schema" "^7.0.3"
1835+
"@typescript-eslint/types" "3.10.1"
1836+
"@typescript-eslint/typescript-estree" "3.10.1"
1837+
eslint-scope "^5.0.0"
1838+
eslint-utils "^2.0.0"
1839+
18291840
"@typescript-eslint/parser@^2.10.0":
18301841
version "2.34.0"
18311842
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8"
@@ -1836,6 +1847,11 @@
18361847
"@typescript-eslint/typescript-estree" "2.34.0"
18371848
eslint-visitor-keys "^1.1.0"
18381849

1850+
"@typescript-eslint/[email protected]":
1851+
version "3.10.1"
1852+
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
1853+
integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==
1854+
18391855
"@typescript-eslint/[email protected]":
18401856
version "2.34.0"
18411857
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5"
@@ -1849,6 +1865,27 @@
18491865
semver "^7.3.2"
18501866
tsutils "^3.17.1"
18511867

1868+
"@typescript-eslint/[email protected]":
1869+
version "3.10.1"
1870+
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
1871+
integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==
1872+
dependencies:
1873+
"@typescript-eslint/types" "3.10.1"
1874+
"@typescript-eslint/visitor-keys" "3.10.1"
1875+
debug "^4.1.1"
1876+
glob "^7.1.6"
1877+
is-glob "^4.0.1"
1878+
lodash "^4.17.15"
1879+
semver "^7.3.2"
1880+
tsutils "^3.17.1"
1881+
1882+
"@typescript-eslint/[email protected]":
1883+
version "3.10.1"
1884+
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
1885+
integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==
1886+
dependencies:
1887+
eslint-visitor-keys "^1.1.0"
1888+
18521889
"@webassemblyjs/[email protected]":
18531890
version "1.8.5"
18541891
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
@@ -4674,6 +4711,13 @@ eslint-plugin-react@^7.20.5:
46744711
resolve "^1.18.1"
46754712
string.prototype.matchall "^4.0.2"
46764713

4714+
eslint-plugin-testing-library@^3.10.1:
4715+
version "3.10.1"
4716+
resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.1.tgz#4dd02306d601c3238fdabf1d1dbc5f2a8e85d531"
4717+
integrity sha512-nQIFe2muIFv2oR2zIuXE4vTbcFNx8hZKRzgHZqJg8rfopIWwoTwtlbCCNELT/jXzVe1uZF68ALGYoDXjLczKiQ==
4718+
dependencies:
4719+
"@typescript-eslint/experimental-utils" "^3.10.1"
4720+
46774721
eslint-scope@^4.0.3:
46784722
version "4.0.3"
46794723
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"

0 commit comments

Comments
 (0)