Skip to content

Commit f66091e

Browse files
authored
Merge pull request #117 from CodeYourFuture/feature/655-add-employer
Select or add employer to the form
2 parents 983ca0b + 02109cc commit f66091e

File tree

8 files changed

+299
-28
lines changed

8 files changed

+299
-28
lines changed

e2e/integration/journey.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,26 @@ it('requires employee selection', () => {
123123
})
124124
})
125125

126+
it('can create a new employer', () => {
127+
cy.findByRole('combobox', { name: /hear about code your future/i }).select(
128+
'Employer'
129+
)
130+
cy.findByRole('combobox', { name: /who is your employer/i }).type(
131+
'Weyland Yutani{enter}'
132+
)
133+
cy.findByText('Weyland Yutani').should('exist')
134+
cy.findByText(/Make sure you typed it correctly\./).should('exist')
135+
cy.visit('/')
136+
cy.findByRole('combobox', { name: /hear about code your future/i }).select(
137+
'Employer'
138+
)
139+
cy.findByRole('combobox', { name: /who is your employer/i }).type(
140+
'yutani{enter}'
141+
)
142+
cy.findByText('Weyland Yutani').should('exist')
143+
cy.findByText(/Make sure you typed it correctly\./).should('exist')
144+
})
145+
126146
const setExperience = (topic, level) => {
127147
cy.findByRole('checkbox', { name: new RegExp(topic, 'i') })
128148
.check()

src/Components/forms/data.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@
120120
{ "name": "The Developer Society", "_id": "The Developer Society" },
121121
{ "name": "University of Arts", "_id": "University of Arts" },
122122
{ "name": "Venditan", "_id": "Venditan" },
123-
{ "name": "Yoti", "_id": "Yoti" },
124-
{ "name": "Other", "_id": "Other" }
123+
{ "name": "Yoti", "_id": "Yoti" }
125124
],
126125

127126
"radioButtonList": [

src/Components/forms/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@
200200
padding: 10px;
201201
font-size: 20px;
202202
}
203+
.reminder {
204+
color: darkgreen;
205+
font-weight: bold;
206+
}
203207
@media screen and (max-width: 1000px) {
204208
.media {
205209
display: block;
Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,75 @@
1-
import React from 'react'
1+
import { useMemo } from 'react'
22
import { Label } from 'reactstrap'
3-
import Select from 'react-select'
3+
import Select from 'react-select/creatable'
4+
5+
import useSession from '../../../hooks/useSession'
6+
7+
const EmployerDropDown = ({
8+
arrayList: employers,
9+
isEmpty,
10+
onChange,
11+
value
12+
}) => {
13+
const [options, setOptions] = useSession('cyfEmployerList', () =>
14+
employers.map(({ _id, name }) => ({
15+
value: _id,
16+
label: name
17+
}))
18+
)
19+
20+
const isCustomEntry = useMemo(
21+
() => value !== '' && !employers.some(({ name }) => name === value),
22+
[employers, value]
23+
)
24+
25+
const selectedOption = useMemo(
26+
() => options.find(employer => employer.value === value),
27+
[options, value]
28+
)
29+
30+
const handleChange = value =>
31+
onChange({ target: { name: 'employer', type: 'text', value } })
432

5-
const EmployerDropDown = ({ onChange, isEmpty, arrayList }) => {
6-
const employersList = arrayList.map(({ _id, name }) => ({
7-
value: _id,
8-
label: name
9-
}))
1033
return (
1134
<div className="form-group">
1235
<Label htmlFor="employer">Who is your employer? *</Label>
1336
<Select
1437
className={isEmpty ? 'is-empty' : ''}
15-
noOptionsMessage={() => 'Employer not found? Please select "Other".'}
1638
inputId="employer"
39+
isClearable
1740
isSearchable
18-
options={employersList}
19-
onChange={e =>
20-
onChange({
21-
target: { name: 'employer', type: 'text', value: e.value }
22-
})
23-
}
2441
name="employer"
42+
onChange={event => handleChange(event?.value ?? '')}
43+
onCreateOption={newEmployer => {
44+
setOptions(oldOptions => insertedInto(oldOptions, newEmployer))
45+
handleChange(newEmployer)
46+
}}
47+
options={options}
2548
placeholder="Type your employer name here"
49+
value={selectedOption}
2650
/>
51+
{isCustomEntry && (
52+
<p className="reminder">
53+
This employer will be added to our list. Make sure you typed it
54+
correctly.
55+
</p>
56+
)}
2757
</div>
2858
)
2959
}
3060

61+
const insertedInto = (oldOptions, newEmployer) => {
62+
const options = [...oldOptions]
63+
const canonical = newEmployer.toLowerCase()
64+
const entry = { label: newEmployer, value: newEmployer }
65+
for (let index = 0; index < options.length; index++) {
66+
if (canonical < options[index].value.toLowerCase()) {
67+
options.splice(index, 0, entry)
68+
return options
69+
}
70+
}
71+
options.push(entry)
72+
return options
73+
}
74+
3175
export default EmployerDropDown

src/Components/forms/inputs/EmployerDropDown.test.js

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import React from 'react'
21
import { render, screen } from '@testing-library/react'
3-
import selectEvent from 'react-select-event'
4-
import '@testing-library/jest-dom'
52
import userEvent from '@testing-library/user-event'
3+
import selectEvent from 'react-select-event'
4+
65
import EmployerDropDown from './EmployerDropDown'
76

87
describe('EmployerDropDown', () => {
8+
beforeEach(() => {
9+
sessionStorage.clear()
10+
})
11+
912
it('shows the label value and place holder value', async () => {
1013
renderInForm({ employers: ['Arnold Clark'] })
1114
expect(screen.getByTestId('form')).toHaveTextContent(
@@ -47,12 +50,81 @@ describe('EmployerDropDown', () => {
4750
expect(container.getElementsByClassName('is-empty')).toHaveLength(1)
4851
})
4952

50-
it('asks user to select "Other" if no matches', async () => {
51-
const { user } = renderInForm({ employers: ['G-Research'] })
52-
await user.type(screen.getByRole('combobox', { name: /employer/i }), 'cap')
53-
expect(
54-
screen.getByText('Employer not found? Please select "Other".')
55-
).toBeInTheDocument()
53+
it('allows the user to enter their own employer', async () => {
54+
const onChange = jest.fn()
55+
const { user } = renderInForm({
56+
employers: ['ABC', 'BBC', 'CBC'],
57+
onChange
58+
})
59+
await user.type(
60+
screen.getByRole('combobox', { name: /employer/i }),
61+
'Google'
62+
)
63+
await user.click(screen.getByText('Create "Google"'))
64+
expect(onChange).toHaveBeenCalledWith({
65+
target: { name: 'employer', type: 'text', value: 'Google' }
66+
})
67+
})
68+
69+
it('adds the new employer to the list', async () => {
70+
const { user } = renderInForm({ employers: ['ABC', 'BBC', 'CBC'] })
71+
await user.type(
72+
screen.getByRole('combobox', { name: /employer/i }),
73+
'Google'
74+
)
75+
await user.click(screen.getByText('Create "Google"'))
76+
await selectEvent.select(
77+
screen.getByLabelText(/who is your employer/i),
78+
'BBC'
79+
)
80+
await selectEvent.select(
81+
screen.getByLabelText(/who is your employer/i),
82+
'Google'
83+
)
84+
})
85+
86+
it('keeps the list in alphabetical order', async () => {
87+
const { container, user } = renderInForm({
88+
employers: ['ABC', 'BBC', 'CBC']
89+
})
90+
for (const employer of ['Boggle', 'Google', 'Aardvark']) {
91+
await user.type(
92+
screen.getByRole('combobox', { name: /employer/i }),
93+
`${employer}{enter}`
94+
)
95+
}
96+
await user.type(
97+
screen.getByRole('combobox', { name: /employer/i }),
98+
'{delete}'
99+
)
100+
expect(renderedItems(container)).toEqual([
101+
'Aardvark',
102+
'ABC',
103+
'BBC',
104+
'Boggle',
105+
'CBC',
106+
'Google'
107+
])
108+
})
109+
110+
describe('reminder', () => {
111+
const reminder =
112+
'This employer will be added to our list. Make sure you typed it correctly.'
113+
114+
it('does not show message when no value is entered', () => {
115+
renderInForm({ employers: ['ABC', 'BBC', 'CBC'], value: '' })
116+
expect(screen.queryByText(reminder)).not.toBeInTheDocument()
117+
})
118+
119+
it('does not show message when employer is in list', () => {
120+
renderInForm({ employers: ['ABC', 'BBC', 'CBC'], value: 'BBC' })
121+
expect(screen.queryByText(reminder)).not.toBeInTheDocument()
122+
})
123+
124+
it('shows message when employer is custom entry', () => {
125+
renderInForm({ employers: ['ABC', 'BBC', 'CBC'], value: 'Google' })
126+
expect(screen.queryByText(reminder)).toBeInTheDocument()
127+
})
56128
})
57129

58130
it('shows the expected values in AC', async () => {
@@ -96,11 +168,31 @@ describe('EmployerDropDown', () => {
96168
expect(screen.getByText(employer)).toBeInTheDocument()
97169
)
98170
})
171+
172+
it('can be cleared', async () => {
173+
const onChange = jest.fn()
174+
renderInForm({ employers: ['ABC', 'BBC', 'CBC'], onChange, value: 'BBC' })
175+
await selectEvent.clearAll(screen.getByLabelText(/who is your employer/i))
176+
expect(onChange).toHaveBeenCalledWith({
177+
target: { name: 'employer', type: 'text', value: '' }
178+
})
179+
})
180+
181+
/**
182+
* @param {HTMLElement} container
183+
*/
184+
const renderedItems = container => {
185+
const divs = Array.from(container.getElementsByTagName('div'))
186+
return divs
187+
.filter(el => el.getAttribute('aria-disabled') === 'false')
188+
.map(el => el.textContent)
189+
}
99190
})
100191
const renderInForm = ({
101192
employers = [],
102193
isEmpty = false,
103-
onChange = () => {}
194+
onChange = () => {},
195+
value = ''
104196
}) => {
105197
const user = userEvent.setup()
106198
const wrapper = render(
@@ -110,6 +202,7 @@ const renderInForm = ({
110202
isEmpty={isEmpty}
111203
name="employer"
112204
onChange={onChange}
205+
value={value}
113206
/>
114207
</form>
115208
)

src/Components/forms/inputs/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export default class VolunteerForm extends Component {
2929
onChangeCheckList,
3030
guidePeople,
3131
techSkill,
32-
otherSkill
32+
otherSkill,
33+
employer
3334
} = this.props
3435

3536
return (
@@ -122,9 +123,10 @@ export default class VolunteerForm extends Component {
122123
/>
123124
{hearAboutCYFFromEmployer && (
124125
<EmployerDropDown
125-
onChange={onChange}
126126
arrayList={ListsData.employerList}
127127
isEmpty={errors.employer}
128+
onChange={onChange}
129+
value={employer}
128130
/>
129131
)}
130132
<span className="contact-interested">

src/hooks/useSession.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useCallback, useState } from 'react'
2+
3+
/**
4+
* Behaves like useState but with the value retained in sessionStorage.
5+
*
6+
* @template T
7+
* @param {string} key
8+
* @param {(T | (() => T))=} initialValue
9+
* @returns {[T | null, (newValue: T | ((oldValue: T) => T)) => void]}
10+
*/
11+
export default function useSession(key, initialValue) {
12+
const [data, setData] = useState(startValue(key, initialValue))
13+
const updateData = useCallback(
14+
newData => {
15+
setData(oldData => stored(key, callIfFunc(newData, oldData)))
16+
},
17+
[key]
18+
)
19+
return [data, updateData]
20+
}
21+
22+
const callIfFunc = (maybeFunc, ...args) =>
23+
typeof maybeFunc === 'function' ? maybeFunc(...args) : maybeFunc
24+
25+
const retrieved = key => {
26+
return JSON.parse(sessionStorage.getItem(key))
27+
}
28+
29+
const startValue = (key, initialValue) => {
30+
return retrieved(key) ?? stored(key, callIfFunc(initialValue))
31+
}
32+
33+
const stored = (key, value = null) => {
34+
sessionStorage.setItem(key, JSON.stringify(value))
35+
return value
36+
}

0 commit comments

Comments
 (0)