select.spec.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. disabled={props.disabled}
  45. optionalLabel={props.optionalLabel}
  46. loading={props.loading}
  47. />
  48. <button type="submit">submit</button>
  49. </form>
  50. </div>
  51. )
  52. }
  53. describe('<Select />', function () {
  54. describe('initial rendering', function () {
  55. it('renders default text', function () {
  56. render({ defaultText: 'Choose an item' })
  57. cy.findByTestId('spinner').should('not.exist')
  58. cy.findByText('Choose an item')
  59. })
  60. it('renders default item', function () {
  61. render({ defaultItem: testData[2] })
  62. cy.findByText('Demo item 3')
  63. })
  64. it('default item takes precedence over default text', function () {
  65. render({ defaultText: 'Choose an item', defaultItem: testData[2] })
  66. cy.findByText('Demo item 3')
  67. })
  68. it('renders label', function () {
  69. render({
  70. defaultText: 'Choose an item',
  71. label: 'test label',
  72. optionalLabel: false,
  73. })
  74. cy.findByText('test label')
  75. cy.findByText('(Optional)').should('not.exist')
  76. })
  77. it('renders optional label', function () {
  78. render({
  79. defaultText: 'Choose an item',
  80. label: 'test label',
  81. optionalLabel: true,
  82. })
  83. cy.findByText('test label')
  84. cy.findByText('(Optional)')
  85. })
  86. it('renders a spinner while loading when there is a label', function () {
  87. render({
  88. defaultText: 'Choose an item',
  89. label: 'test label',
  90. loading: true,
  91. })
  92. cy.findByTestId('spinner')
  93. })
  94. it('does not render a spinner while loading if there is no label', function () {
  95. render({
  96. defaultText: 'Choose an item',
  97. loading: true,
  98. })
  99. cy.findByTestId('spinner').should('not.exist')
  100. })
  101. })
  102. describe('items rendering', function () {
  103. it('renders all items', function () {
  104. render({ defaultText: 'Choose an item' })
  105. cy.findByText('Choose an item').click()
  106. cy.findByText('Demo item 1')
  107. cy.findByText('Demo item 2')
  108. cy.findByText('Demo item 3')
  109. })
  110. it('renders subtitles', function () {
  111. render({
  112. defaultText: 'Choose an item',
  113. itemToSubtitle: x => String(x?.sub),
  114. })
  115. cy.findByText('Choose an item').click()
  116. cy.findByText('Subtitle 1')
  117. cy.findByText('Subtitle 2')
  118. cy.findByText('Subtitle 3')
  119. })
  120. })
  121. describe('item selection', function () {
  122. it('cannot select an item when disabled', function () {
  123. render({ defaultText: 'Choose an item', disabled: true })
  124. cy.findByText('Choose an item').click()
  125. cy.findByText('Demo item 1').should('not.exist')
  126. cy.findByText('Demo item 2').should('not.exist')
  127. cy.findByText('Demo item 3').should('not.exist')
  128. cy.findByText('Choose an item')
  129. })
  130. it('renders only the selected item after selection', function () {
  131. render({ defaultText: 'Choose an item' })
  132. cy.findByText('Choose an item').click()
  133. cy.findByText('Demo item 1')
  134. cy.findByText('Demo item 2')
  135. cy.findByText('Demo item 3').click()
  136. cy.findByText('Choose an item').should('not.exist')
  137. cy.findByText('Demo item 1').should('not.exist')
  138. cy.findByText('Demo item 2').should('not.exist')
  139. cy.findByText('Demo item 3')
  140. })
  141. it('invokes callback after selection', function () {
  142. const selectionHandler = cy.stub().as('selectionHandler')
  143. render({
  144. defaultText: 'Choose an item',
  145. onSelectedItemChanged: selectionHandler,
  146. })
  147. cy.findByText('Choose an item').click()
  148. cy.findByText('Demo item 2').click()
  149. cy.get('@selectionHandler').should(
  150. 'have.been.calledOnceWith',
  151. testData[1]
  152. )
  153. })
  154. })
  155. describe('when the form is submitted', function () {
  156. it('populates FormData with the default selected item', function () {
  157. const submitHandler = cy.stub().as('submitHandler')
  158. render({ defaultItem: testData[1], onSubmit: submitHandler })
  159. cy.findByText('submit').click()
  160. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  161. select_control: 'Demo item 2',
  162. })
  163. })
  164. it('populates FormData with the selected item', function () {
  165. const submitHandler = cy.stub().as('submitHandler')
  166. render({ defaultItem: testData[1], onSubmit: submitHandler })
  167. cy.findByText('Demo item 2').click() // open dropdown
  168. cy.findByText('Demo item 3').click() // choose a different item
  169. cy.findByText('submit').click()
  170. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  171. select_control: 'Demo item 3',
  172. })
  173. })
  174. it('does not populate FormData when no item is selected', function () {
  175. const submitHandler = cy.stub().as('submitHandler')
  176. render({ defaultText: 'Choose an item', onSubmit: submitHandler })
  177. cy.findByText('submit').click()
  178. cy.get('@submitHandler').should('have.been.calledOnceWith', {})
  179. })
  180. })
  181. describe('with react-bootstrap forms', function () {
  182. type FormWithSelectProps = {
  183. onSubmit: (formData: object) => void
  184. }
  185. const FormWithSelect = ({ onSubmit }: FormWithSelectProps) => {
  186. const selectComponent = useCallback(
  187. () => (
  188. <Select
  189. name="select_control"
  190. items={testData}
  191. defaultItem={testData[0]}
  192. itemToString={x => String(x?.value)}
  193. itemToKey={x => String(x.key)}
  194. />
  195. ),
  196. []
  197. )
  198. function handleSubmit(event: FormEvent<Form>) {
  199. event.preventDefault()
  200. const formData = new FormData(event.target as HTMLFormElement)
  201. // a plain object is more convenient to work later with assertions
  202. onSubmit(Object.fromEntries(formData.entries()))
  203. }
  204. return (
  205. <Form onSubmit={handleSubmit}>
  206. <FormControl componentClass={selectComponent} />
  207. <Button type="submit">submit</Button>
  208. </Form>
  209. )
  210. }
  211. it('populates FormData with the selected item when the form is submitted', function () {
  212. const submitHandler = cy.stub().as('submitHandler')
  213. cy.mount(
  214. <div
  215. style={{
  216. display: 'flex',
  217. alignItems: 'center',
  218. justifyContent: 'center',
  219. height: '100vh',
  220. }}
  221. >
  222. <FormWithSelect onSubmit={submitHandler} />
  223. </div>
  224. )
  225. cy.findByText('Demo item 1').click() // open dropdown
  226. cy.findByText('Demo item 3').click() // choose a different item
  227. cy.findByText('submit').click()
  228. cy.get('@submitHandler').should('have.been.calledOnceWith', {
  229. select_control: 'Demo item 3',
  230. })
  231. })
  232. })
  233. })