select.spec.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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.findByText('Choose an item')
  62. })
  63. it('renders default item', function () {
  64. render({ defaultItem: testData[2] })
  65. cy.findByText('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.findByText('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.findByText('test label')
  78. cy.findByText('(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.findByText('test label')
  87. cy.findByText('(Optional)')
  88. })
  89. it('renders a spinner while loading when there is a label', function () {
  90. render({
  91. defaultText: 'Choose an item',
  92. label: 'test label',
  93. loading: true,
  94. })
  95. cy.findByTestId('spinner')
  96. })
  97. it('does not render a spinner while loading if there is no label', function () {
  98. render({
  99. defaultText: 'Choose an item',
  100. loading: true,
  101. })
  102. cy.findByTestId('spinner').should('not.exist')
  103. })
  104. })
  105. describe('items rendering', function () {
  106. it('renders all items', function () {
  107. render({ defaultText: 'Choose an item' })
  108. cy.findByText('Choose an item').click()
  109. cy.findByText('Demo item 1')
  110. cy.findByText('Demo item 2')
  111. cy.findByText('Demo item 3')
  112. })
  113. it('renders subtitles', function () {
  114. render({
  115. defaultText: 'Choose an item',
  116. itemToSubtitle: x => String(x?.sub),
  117. })
  118. cy.findByText('Choose an item').click()
  119. cy.findByText('Subtitle 1')
  120. cy.findByText('Subtitle 2')
  121. cy.findByText('Subtitle 3')
  122. })
  123. })
  124. describe('item selection', function () {
  125. it('cannot select an item when disabled', function () {
  126. render({ defaultText: 'Choose an item', disabled: true })
  127. cy.findByText('Choose an item').click()
  128. cy.findByText('Demo item 1').should('not.exist')
  129. cy.findByText('Demo item 2').should('not.exist')
  130. cy.findByText('Demo item 3').should('not.exist')
  131. cy.findByText('Choose an item')
  132. })
  133. it('renders only the selected item after selection', function () {
  134. render({ defaultText: 'Choose an item' })
  135. cy.findByText('Choose an item').click()
  136. cy.findByText('Demo item 1')
  137. cy.findByText('Demo item 2')
  138. cy.findByText('Demo item 3').click()
  139. cy.findByText('Choose an item').should('not.exist')
  140. cy.findByText('Demo item 1').should('not.exist')
  141. cy.findByText('Demo item 2').should('not.exist')
  142. cy.findByText('Demo item 3')
  143. })
  144. it('invokes callback after selection', function () {
  145. const selectionHandler = cy.stub().as('selectionHandler')
  146. render({
  147. defaultText: 'Choose an item',
  148. onSelectedItemChanged: selectionHandler,
  149. })
  150. cy.findByText('Choose an item').click()
  151. cy.findByText('Demo item 2').click()
  152. cy.get('@selectionHandler').should(
  153. 'have.been.calledOnceWith',
  154. testData[1]
  155. )
  156. })
  157. })
  158. describe('when the form is submitted', function () {
  159. it('populates FormData with the default selected item', function () {
  160. const submitHandler = cy.stub().as('submitHandler')
  161. render({ defaultItem: testData[1], onSubmit: submitHandler })
  162. cy.findByText('submit').click()
  163. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  164. select_control: 'Demo item 2',
  165. })
  166. })
  167. it('populates FormData with the selected item', function () {
  168. const submitHandler = cy.stub().as('submitHandler')
  169. render({ defaultItem: testData[1], onSubmit: submitHandler })
  170. cy.findByText('Demo item 2').click() // open dropdown
  171. cy.findByText('Demo item 3').click() // choose a different item
  172. cy.findByText('submit').click()
  173. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  174. select_control: 'Demo item 3',
  175. })
  176. })
  177. it('does not populate FormData when no item is selected', function () {
  178. const submitHandler = cy.stub().as('submitHandler')
  179. render({ defaultText: 'Choose an item', onSubmit: submitHandler })
  180. cy.findByText('submit').click()
  181. cy.get('@submitHandler').should('have.been.calledOnceWith', {})
  182. })
  183. })
  184. describe('with react-bootstrap forms', function () {
  185. type FormWithSelectProps = {
  186. onSubmit: (formData: object) => void
  187. }
  188. const FormWithSelect = ({ onSubmit }: FormWithSelectProps) => {
  189. const selectComponent = useCallback(
  190. () => (
  191. <Select
  192. name="select_control"
  193. items={testData}
  194. defaultItem={testData[0]}
  195. itemToString={x => String(x?.value)}
  196. itemToKey={x => String(x.key)}
  197. />
  198. ),
  199. []
  200. )
  201. function handleSubmit(event: FormEvent<Form>) {
  202. event.preventDefault()
  203. const formData = new FormData(event.target as HTMLFormElement)
  204. // a plain object is more convenient to work later with assertions
  205. onSubmit(Object.fromEntries(formData.entries()))
  206. }
  207. return (
  208. <Form onSubmit={handleSubmit}>
  209. <FormControl componentClass={selectComponent} />
  210. <Button type="submit">submit</Button>
  211. </Form>
  212. )
  213. }
  214. it('populates FormData with the selected item when the form is submitted', function () {
  215. const submitHandler = cy.stub().as('submitHandler')
  216. cy.mount(
  217. <div
  218. style={{
  219. display: 'flex',
  220. alignItems: 'center',
  221. justifyContent: 'center',
  222. height: '100vh',
  223. }}
  224. >
  225. <FormWithSelect onSubmit={submitHandler} />
  226. </div>
  227. )
  228. cy.findByText('Demo item 1').click() // open dropdown
  229. cy.findByText('Demo item 3').click() // choose a different item
  230. cy.findByText('submit').click()
  231. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  232. select_control: 'Demo item 3',
  233. })
  234. })
  235. })
  236. describe('keyboard navigation', function () {
  237. it('can select an item using the keyboard', function () {
  238. render({ defaultText: 'Choose an item' })
  239. cy.findByText('Choose an item').type('{Enter}{downArrow}{Enter}')
  240. cy.findByText('Demo item 1').should('exist')
  241. cy.findByText('Demo item 2').should('not.exist')
  242. })
  243. })
  244. describe('selectedIcon', function () {
  245. it('renders a selected icon if the prop is set', function () {
  246. render({
  247. defaultText: 'Choose an item',
  248. selectedIcon: true,
  249. })
  250. cy.findByText('Choose an item').click()
  251. cy.findByText('Demo item 1').click()
  252. cy.findByText('Demo item 1').click()
  253. cy.get('.fa-check').should('exist')
  254. })
  255. it('renders no selected icon if the prop is not set', function () {
  256. render({
  257. defaultText: 'Choose an item',
  258. selectedIcon: false,
  259. })
  260. cy.findByText('Choose an item').click()
  261. cy.findByText('Demo item 1').click()
  262. cy.findByText('Demo item 1').click()
  263. cy.get('.fa-check').should('not.exist')
  264. })
  265. })
  266. describe('itemToDisabled', function () {
  267. it('prevents selecting a disabled item', function () {
  268. render({
  269. defaultText: 'Choose an item',
  270. itemToDisabled: x => x?.key === 2,
  271. })
  272. cy.findByText('Choose an item').click()
  273. cy.findByText('Demo item 2').click()
  274. // still showing other list items
  275. cy.findByText('Demo item 3').should('exist')
  276. cy.findByText('Demo item 1').click()
  277. // clicking an enabled item dismisses the list
  278. cy.findByText('Demo item 3').should('not.exist')
  279. })
  280. })
  281. describe('selected', function () {
  282. it('shows the item provided in the selected prop', function () {
  283. render({
  284. defaultText: 'Choose an item',
  285. selected: testData[1],
  286. })
  287. cy.findByText('Demo item 2').should('exist')
  288. })
  289. })
  290. })