select.spec.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import '../../helpers/bootstrap-3'
  2. import { useCallback, FormEvent } from 'react'
  3. import { Button, Form, FormControl } from 'react-bootstrap'
  4. import {
  5. Select,
  6. SelectProps,
  7. } from '../../../../frontend/js/shared/components/select'
  8. const testData = [1, 2, 3].map(index => ({
  9. key: index,
  10. value: `Demo item ${index}`,
  11. sub: `Subtitle ${index}`,
  12. }))
  13. type RenderProps = Partial<SelectProps<(typeof testData)[number]>> & {
  14. onSubmit?: (formData: object) => void
  15. }
  16. function render(props: RenderProps) {
  17. const submitHandler = (event: FormEvent<HTMLFormElement>) => {
  18. event.preventDefault()
  19. if (props.onSubmit) {
  20. const formData = new FormData(event.target as HTMLFormElement)
  21. // a plain object is more convenient to work later with assertions
  22. props.onSubmit(Object.fromEntries(formData.entries()))
  23. }
  24. }
  25. cy.mount(
  26. <div
  27. style={{
  28. display: 'flex',
  29. alignItems: 'center',
  30. justifyContent: 'center',
  31. height: '100vh',
  32. }}
  33. >
  34. <form onSubmit={submitHandler}>
  35. <Select
  36. items={testData}
  37. itemToString={x => String(x?.value)}
  38. label={props.label}
  39. name="select_control"
  40. defaultText={props.defaultText}
  41. defaultItem={props.defaultItem}
  42. itemToSubtitle={props.itemToSubtitle}
  43. itemToKey={x => String(x.key)}
  44. onSelectedItemChanged={props.onSelectedItemChanged}
  45. selected={props.selected}
  46. disabled={props.disabled}
  47. itemToDisabled={props.itemToDisabled}
  48. optionalLabel={props.optionalLabel}
  49. loading={props.loading}
  50. selectedIcon={props.selectedIcon}
  51. />
  52. <button type="submit">submit</button>
  53. </form>
  54. </div>
  55. )
  56. }
  57. describe('<Select />', function () {
  58. describe('initial rendering', function () {
  59. it('renders default text', function () {
  60. render({ defaultText: 'Choose an item' })
  61. cy.findByTestId('spinner').should('not.exist')
  62. cy.findByText('Choose an item')
  63. })
  64. it('renders default item', function () {
  65. render({ defaultItem: testData[2] })
  66. cy.findByText('Demo item 3')
  67. })
  68. it('default item takes precedence over default text', function () {
  69. render({ defaultText: 'Choose an item', defaultItem: testData[2] })
  70. cy.findByText('Demo item 3')
  71. })
  72. it('renders label', function () {
  73. render({
  74. defaultText: 'Choose an item',
  75. label: 'test label',
  76. optionalLabel: false,
  77. })
  78. cy.findByText('test label')
  79. cy.findByText('(Optional)').should('not.exist')
  80. })
  81. it('renders optional label', function () {
  82. render({
  83. defaultText: 'Choose an item',
  84. label: 'test label',
  85. optionalLabel: true,
  86. })
  87. cy.findByText('test label')
  88. cy.findByText('(Optional)')
  89. })
  90. it('renders a spinner while loading when there is a label', function () {
  91. render({
  92. defaultText: 'Choose an item',
  93. label: 'test label',
  94. loading: true,
  95. })
  96. cy.findByTestId('spinner')
  97. })
  98. it('does not render a spinner while loading if there is no label', function () {
  99. render({
  100. defaultText: 'Choose an item',
  101. loading: true,
  102. })
  103. cy.findByTestId('spinner').should('not.exist')
  104. })
  105. })
  106. describe('items rendering', function () {
  107. it('renders all items', function () {
  108. render({ defaultText: 'Choose an item' })
  109. cy.findByText('Choose an item').click()
  110. cy.findByText('Demo item 1')
  111. cy.findByText('Demo item 2')
  112. cy.findByText('Demo item 3')
  113. })
  114. it('renders subtitles', function () {
  115. render({
  116. defaultText: 'Choose an item',
  117. itemToSubtitle: x => String(x?.sub),
  118. })
  119. cy.findByText('Choose an item').click()
  120. cy.findByText('Subtitle 1')
  121. cy.findByText('Subtitle 2')
  122. cy.findByText('Subtitle 3')
  123. })
  124. })
  125. describe('item selection', function () {
  126. it('cannot select an item when disabled', function () {
  127. render({ defaultText: 'Choose an item', disabled: true })
  128. cy.findByText('Choose an item').click()
  129. cy.findByText('Demo item 1').should('not.exist')
  130. cy.findByText('Demo item 2').should('not.exist')
  131. cy.findByText('Demo item 3').should('not.exist')
  132. cy.findByText('Choose an item')
  133. })
  134. it('renders only the selected item after selection', function () {
  135. render({ defaultText: 'Choose an item' })
  136. cy.findByText('Choose an item').click()
  137. cy.findByText('Demo item 1')
  138. cy.findByText('Demo item 2')
  139. cy.findByText('Demo item 3').click()
  140. cy.findByText('Choose an item').should('not.exist')
  141. cy.findByText('Demo item 1').should('not.exist')
  142. cy.findByText('Demo item 2').should('not.exist')
  143. cy.findByText('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.findByText('Choose an item').click()
  152. cy.findByText('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.findByText('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.findByText('Demo item 1').click() // open dropdown
  230. cy.findByText('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.findByText('Choose an item').type('{Enter}{downArrow}{Enter}')
  241. cy.findByText('Demo item 1').should('exist')
  242. cy.findByText('Demo item 2').should('not.exist')
  243. })
  244. })
  245. describe('selectedIcon', function () {
  246. it('renders a selected icon if the prop is set', function () {
  247. render({
  248. defaultText: 'Choose an item',
  249. selectedIcon: true,
  250. })
  251. cy.findByText('Choose an item').click()
  252. cy.findByText('Demo item 1').click()
  253. cy.findByText('Demo item 1').click()
  254. cy.get('.fa-check').should('exist')
  255. })
  256. it('renders no selected icon if the prop is not set', function () {
  257. render({
  258. defaultText: 'Choose an item',
  259. selectedIcon: false,
  260. })
  261. cy.findByText('Choose an item').click()
  262. cy.findByText('Demo item 1').click()
  263. cy.findByText('Demo item 1').click()
  264. cy.get('.fa-check').should('not.exist')
  265. })
  266. })
  267. describe('itemToDisabled', function () {
  268. it('prevents selecting a disabled item', function () {
  269. render({
  270. defaultText: 'Choose an item',
  271. itemToDisabled: x => x?.key === 2,
  272. })
  273. cy.findByText('Choose an item').click()
  274. cy.findByText('Demo item 2').click()
  275. // still showing other list items
  276. cy.findByText('Demo item 3').should('exist')
  277. cy.findByText('Demo item 1').click()
  278. // clicking an enabled item dismisses the list
  279. cy.findByText('Demo item 3').should('not.exist')
  280. })
  281. })
  282. describe('selected', function () {
  283. it('shows the item provided in the selected prop', function () {
  284. render({
  285. defaultText: 'Choose an item',
  286. selected: testData[1],
  287. })
  288. cy.findByText('Demo item 2').should('exist')
  289. })
  290. it('should show default text when selected is null', function () {
  291. render({
  292. selected: null,
  293. defaultText: 'Choose an item',
  294. })
  295. cy.findByText('Choose an item').should('exist')
  296. })
  297. })
  298. })