select.spec.tsx 13 KB

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