select.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { useCallback, FormEvent } from 'react'
  2. import { Button, Form, FormControl } from 'react-bootstrap'
  3. import {
  4. Select,
  5. SelectProps,
  6. } from '../../../../frontend/js/shared/components/select'
  7. const testData = [1, 2, 3].map(index => ({
  8. key: index,
  9. value: `Demo item ${index}`,
  10. sub: `Subtitle ${index}`,
  11. }))
  12. type RenderProps = Partial<SelectProps<(typeof testData)[number]>> & {
  13. onSubmit?: (formData: object) => void
  14. }
  15. function render(props: RenderProps) {
  16. const submitHandler = (event: FormEvent<HTMLFormElement>) => {
  17. event.preventDefault()
  18. if (props.onSubmit) {
  19. const formData = new FormData(event.target as HTMLFormElement)
  20. // a plain object is more convenient to work later with assertions
  21. props.onSubmit(Object.fromEntries(formData.entries()))
  22. }
  23. }
  24. cy.mount(
  25. <div
  26. style={{
  27. display: 'flex',
  28. alignItems: 'center',
  29. justifyContent: 'center',
  30. height: '100vh',
  31. }}
  32. >
  33. <form onSubmit={submitHandler}>
  34. <Select
  35. items={testData}
  36. itemToString={x => String(x?.value)}
  37. label={props.label}
  38. name="select_control"
  39. defaultText={props.defaultText}
  40. defaultItem={props.defaultItem}
  41. itemToSubtitle={props.itemToSubtitle}
  42. itemToKey={x => String(x.key)}
  43. onSelectedItemChanged={props.onSelectedItemChanged}
  44. selected={props.selected}
  45. disabled={props.disabled}
  46. itemToDisabled={props.itemToDisabled}
  47. optionalLabel={props.optionalLabel}
  48. loading={props.loading}
  49. selectedIcon={props.selectedIcon}
  50. />
  51. <button type="submit">submit</button>
  52. </form>
  53. </div>
  54. )
  55. }
  56. describe('<Select />', function () {
  57. describe('initial rendering', function () {
  58. it('renders default text', function () {
  59. render({ defaultText: 'Choose an item' })
  60. cy.findByTestId('spinner').should('not.exist')
  61. cy.findByRole('textbox', { name: 'Choose an item' })
  62. })
  63. it('renders default item', function () {
  64. render({ defaultItem: testData[2] })
  65. cy.findByRole('textbox', { name: 'Demo item 3' })
  66. })
  67. it('default item takes precedence over default text', function () {
  68. render({ defaultText: 'Choose an item', defaultItem: testData[2] })
  69. cy.findByRole('textbox', { name: 'Demo item 3' })
  70. })
  71. it('renders label', function () {
  72. render({
  73. defaultText: 'Choose an item',
  74. label: 'test label',
  75. optionalLabel: false,
  76. })
  77. cy.findByRole('textbox', { name: 'test label' })
  78. cy.findByRole('textbox', { name: '(Optional)' }).should('not.exist')
  79. })
  80. it('renders optional label', function () {
  81. render({
  82. defaultText: 'Choose an item',
  83. label: 'test label',
  84. optionalLabel: true,
  85. })
  86. cy.findByRole('textbox', { name: 'test label (Optional)' })
  87. })
  88. it('renders a spinner while loading when there is a label', function () {
  89. render({
  90. defaultText: 'Choose an item',
  91. label: 'test label',
  92. loading: true,
  93. })
  94. cy.findByTestId('spinner')
  95. })
  96. it('does not render a spinner while loading if there is no label', function () {
  97. render({
  98. defaultText: 'Choose an item',
  99. loading: true,
  100. })
  101. cy.findByTestId('spinner').should('not.exist')
  102. })
  103. })
  104. describe('items rendering', function () {
  105. it('renders all items', function () {
  106. render({ defaultText: 'Choose an item' })
  107. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  108. cy.findByRole('option', { name: 'Demo item 1' })
  109. cy.findByRole('option', { name: 'Demo item 2' })
  110. cy.findByRole('option', { name: 'Demo item 3' })
  111. })
  112. it('renders subtitles', function () {
  113. render({
  114. defaultText: 'Choose an item',
  115. itemToSubtitle: x => String(x?.sub),
  116. })
  117. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  118. cy.findByRole('option', { name: 'Demo item 1 Subtitle 1' })
  119. cy.findByRole('option', { name: 'Demo item 2 Subtitle 2' })
  120. cy.findByRole('option', { name: 'Demo item 3 Subtitle 3' })
  121. })
  122. })
  123. describe('item selection', function () {
  124. it('cannot select an item when disabled', function () {
  125. render({ defaultText: 'Choose an item', disabled: true })
  126. cy.findByRole('textbox', { name: 'Choose an item' }).click({
  127. force: true,
  128. })
  129. cy.findByRole('option', { name: 'Demo item 1' }).should('not.exist')
  130. cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
  131. cy.findByRole('option', { name: 'Demo item 3' }).should('not.exist')
  132. cy.findByRole('textbox', { name: 'Choose an item' })
  133. })
  134. it('renders only the selected item after selection', function () {
  135. render({ defaultText: 'Choose an item' })
  136. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  137. cy.findByRole('option', { name: 'Demo item 1' })
  138. cy.findByRole('option', { name: 'Demo item 2' })
  139. cy.findByRole('option', { name: 'Demo item 3' }).click()
  140. cy.findByRole('textbox', { name: 'Choose an item' }).should('not.exist')
  141. cy.findByRole('option', { name: 'Demo item 1' }).should('not.exist')
  142. cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
  143. cy.findByRole('textbox', { name: 'Demo item 3' })
  144. })
  145. it('invokes callback after selection', function () {
  146. const selectionHandler = cy.stub().as('selectionHandler')
  147. render({
  148. defaultText: 'Choose an item',
  149. onSelectedItemChanged: selectionHandler,
  150. })
  151. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  152. cy.findByRole('option', { name: 'Demo item 2' }).click()
  153. cy.get('@selectionHandler').should(
  154. 'have.been.calledOnceWith',
  155. testData[1]
  156. )
  157. })
  158. })
  159. describe('when the form is submitted', function () {
  160. it('populates FormData with the default selected item', function () {
  161. const submitHandler = cy.stub().as('submitHandler')
  162. render({ defaultItem: testData[1], onSubmit: submitHandler })
  163. cy.findByText('submit').click()
  164. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  165. select_control: 'Demo item 2',
  166. })
  167. })
  168. it('populates FormData with the selected item', function () {
  169. const submitHandler = cy.stub().as('submitHandler')
  170. render({ defaultItem: testData[1], onSubmit: submitHandler })
  171. cy.findByRole('textbox', { name: 'Demo item 2' }).click() // open dropdown
  172. cy.findByText('Demo item 3').click() // choose a different item
  173. cy.findByText('submit').click()
  174. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  175. select_control: 'Demo item 3',
  176. })
  177. })
  178. it('does not populate FormData when no item is selected', function () {
  179. const submitHandler = cy.stub().as('submitHandler')
  180. render({ defaultText: 'Choose an item', onSubmit: submitHandler })
  181. cy.findByText('submit').click()
  182. cy.get('@submitHandler').should('have.been.calledOnceWith', {})
  183. })
  184. })
  185. describe('with react-bootstrap forms', function () {
  186. type FormWithSelectProps = {
  187. onSubmit: (formData: object) => void
  188. }
  189. const FormWithSelect = ({ onSubmit }: FormWithSelectProps) => {
  190. const selectComponent = useCallback(
  191. () => (
  192. <Select
  193. name="select_control"
  194. items={testData}
  195. defaultItem={testData[0]}
  196. itemToString={x => String(x?.value)}
  197. itemToKey={x => String(x.key)}
  198. />
  199. ),
  200. []
  201. )
  202. function handleSubmit(event: FormEvent<Form>) {
  203. event.preventDefault()
  204. const formData = new FormData(event.target as HTMLFormElement)
  205. // a plain object is more convenient to work later with assertions
  206. onSubmit(Object.fromEntries(formData.entries()))
  207. }
  208. return (
  209. <Form onSubmit={handleSubmit}>
  210. <FormControl componentClass={selectComponent} />
  211. <Button type="submit">submit</Button>
  212. </Form>
  213. )
  214. }
  215. it('populates FormData with the selected item when the form is submitted', function () {
  216. const submitHandler = cy.stub().as('submitHandler')
  217. cy.mount(
  218. <div
  219. style={{
  220. display: 'flex',
  221. alignItems: 'center',
  222. justifyContent: 'center',
  223. height: '100vh',
  224. }}
  225. >
  226. <FormWithSelect onSubmit={submitHandler} />
  227. </div>
  228. )
  229. cy.findByRole('textbox', { name: 'Demo item 1' }).click() // open dropdown
  230. cy.findByRole('option', { name: 'Demo item 3' }).click() // choose a different item
  231. cy.findByText('submit').click()
  232. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  233. select_control: 'Demo item 3',
  234. })
  235. })
  236. })
  237. describe('keyboard navigation', function () {
  238. it('can select an item using the keyboard', function () {
  239. render({ defaultText: 'Choose an item' })
  240. cy.findByRole('textbox', { name: 'Choose an item' }).type(
  241. '{Enter}{downArrow}{Enter}',
  242. { force: true }
  243. )
  244. cy.findByRole('textbox', { name: 'Demo item 1' }).should('exist')
  245. cy.findByRole('option', { name: 'Demo item 2' }).should('not.exist')
  246. })
  247. })
  248. describe('selectedIcon', function () {
  249. it('renders a selected icon if the prop is set', function () {
  250. render({
  251. defaultText: 'Choose an item',
  252. selectedIcon: true,
  253. })
  254. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  255. cy.findByRole('option', { name: 'Demo item 1' }).click()
  256. cy.findByRole('textbox', { name: 'Demo item 1' }).click()
  257. cy.findByText('check').should('exist')
  258. })
  259. it('renders no selected icon if the prop is not set', function () {
  260. render({
  261. defaultText: 'Choose an item',
  262. selectedIcon: false,
  263. })
  264. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  265. cy.findByRole('option', { name: 'Demo item 1' }).click()
  266. cy.findByRole('textbox', { name: 'Demo item 1' }).click()
  267. cy.findByText('check').should('exist')
  268. })
  269. })
  270. describe('itemToDisabled', function () {
  271. it('prevents selecting a disabled item', function () {
  272. render({
  273. defaultText: 'Choose an item',
  274. itemToDisabled: x => x?.key === 2,
  275. })
  276. cy.findByRole('textbox', { name: 'Choose an item' }).click()
  277. cy.findByRole('option', { name: 'Demo item 2' }).click({ force: true })
  278. // still showing other list items
  279. cy.findByRole('option', { name: 'Demo item 3' }).should('exist')
  280. cy.findByRole('option', { name: 'Demo item 1' }).click()
  281. // clicking an enabled item dismisses the list
  282. cy.findByRole('option', { name: 'Demo item 3' }).should('not.exist')
  283. })
  284. })
  285. describe('selected', function () {
  286. it('shows the item provided in the selected prop', function () {
  287. render({
  288. defaultText: 'Choose an item',
  289. selected: testData[1],
  290. })
  291. cy.findByRole('textbox', { name: 'Demo item 2' }).should('exist')
  292. })
  293. it('should show default text when selected is null', function () {
  294. render({
  295. selected: null,
  296. defaultText: 'Choose an item',
  297. })
  298. cy.findByRole('textbox', { name: 'Choose an item' }).should('exist')
  299. })
  300. })
  301. })