add-email-input.test.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {
  2. fireEvent,
  3. render,
  4. screen,
  5. waitForElementToBeRemoved,
  6. } from '@testing-library/react'
  7. import { expect } from 'chai'
  8. import sinon from 'sinon'
  9. import fetchMock from 'fetch-mock'
  10. import {
  11. AddEmailInput,
  12. clearDomainCache,
  13. } from '../../../../../../frontend/js/features/settings/components/emails/add-email-input'
  14. const testInstitutionData = [
  15. { university: { id: 124 }, hostname: 'domain.edu' },
  16. ]
  17. describe('<AddEmailInput/>', function () {
  18. const defaultProps = {
  19. onChange: (value: string) => {},
  20. }
  21. beforeEach(function () {
  22. clearDomainCache()
  23. fetchMock.reset()
  24. })
  25. describe('on initial render', function () {
  26. it('should render an input with a placeholder', function () {
  27. render(<AddEmailInput {...defaultProps} />)
  28. screen.getByPlaceholderText('e.g. johndoe@mit.edu')
  29. })
  30. it('should not dispatch any `change` event', function () {
  31. const onChangeStub = sinon.stub()
  32. render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
  33. expect(onChangeStub.called).to.equal(false)
  34. })
  35. })
  36. describe('when typing text that does not contain any potential domain match', function () {
  37. let onChangeStub
  38. beforeEach(function () {
  39. fetchMock.get('express:/institutions/domains', 200)
  40. onChangeStub = sinon.stub()
  41. render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
  42. fireEvent.change(screen.getByRole('textbox'), {
  43. target: { value: 'user' },
  44. })
  45. })
  46. it('should render the text being typed', function () {
  47. const input = screen.getByRole('textbox') as HTMLInputElement
  48. expect(input.value).to.equal('user')
  49. })
  50. it('should dispatch a `change` event on every stroke', function () {
  51. expect(onChangeStub.calledWith('user')).to.equal(true)
  52. fireEvent.change(screen.getByRole('textbox'), {
  53. target: { value: 's' },
  54. })
  55. expect(onChangeStub.calledWith('s')).to.equal(true)
  56. })
  57. it('should not make any request for institution domains', function () {
  58. expect(fetchMock.called()).to.be.false
  59. })
  60. })
  61. describe('when typing text that contains a potential domain match', function () {
  62. let onChangeStub
  63. beforeEach(function () {
  64. onChangeStub = sinon.stub()
  65. render(<AddEmailInput onChange={onChangeStub} />)
  66. })
  67. describe('when there are no matches', function () {
  68. beforeEach(function () {
  69. fetchMock.get('express:/institutions/domains', 200)
  70. fireEvent.change(screen.getByRole('textbox'), {
  71. target: { value: 'user@d' },
  72. })
  73. })
  74. it('should render the text being typed', function () {
  75. const input = screen.getByRole('textbox') as HTMLInputElement
  76. expect(input.value).to.equal('user@d')
  77. })
  78. })
  79. describe('when there is a domain match', function () {
  80. beforeEach(function () {
  81. fetchMock.get('express:/institutions/domains', testInstitutionData)
  82. fireEvent.change(screen.getByRole('textbox'), {
  83. target: { value: 'user@d' },
  84. })
  85. })
  86. it('should render the text being typed along with the suggestion', async function () {
  87. const input = screen.getByRole('textbox') as HTMLInputElement
  88. expect(input.value).to.equal('user@d')
  89. await screen.findByText('user@domain.edu')
  90. })
  91. it('should dispatch a `change` event with the typed text', function () {
  92. expect(onChangeStub.calledWith('user@d')).to.equal(true)
  93. })
  94. it('should dispatch a `change` event with institution data when the typed email contains the institution domain', async function () {
  95. fireEvent.change(screen.getByRole('textbox'), {
  96. target: { value: 'user@domain.edu' },
  97. })
  98. await fetchMock.flush(true)
  99. expect(
  100. onChangeStub.calledWith(
  101. 'user@domain.edu',
  102. sinon.match(testInstitutionData[0])
  103. )
  104. ).to.equal(true)
  105. })
  106. it('should clear the suggestion when the potential domain match is completely deleted', function () {
  107. fireEvent.change(screen.getByRole('textbox'), {
  108. target: { value: 'user@' },
  109. })
  110. expect(onChangeStub.calledWith('user@')).to.equal(true)
  111. expect(screen.queryByText('user@domain.edu')).to.be.null
  112. })
  113. describe('when there is a suggestion and "Tab" key is pressed', function () {
  114. beforeEach(async function () {
  115. await screen.findByText('user@domain.edu') // wait until autocompletion available
  116. fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' })
  117. })
  118. it('it should autocomplete the input', async function () {
  119. const input = screen.getByRole('textbox') as HTMLInputElement
  120. expect(input.value).to.equal('user@domain.edu')
  121. })
  122. it('should dispatch a `change` event with the domain matched', async function () {
  123. expect(
  124. onChangeStub.calledWith(
  125. 'user@domain.edu',
  126. sinon.match(testInstitutionData[0])
  127. )
  128. ).to.equal(true)
  129. })
  130. })
  131. describe('when there is a suggestion and "Enter" key is pressed', function () {
  132. beforeEach(async function () {
  133. await screen.findByText('user@domain.edu') // wait until autocompletion available
  134. fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
  135. })
  136. it('it should autocomplete the input', async function () {
  137. const input = screen.getByRole('textbox') as HTMLInputElement
  138. expect(input.value).to.equal('user@domain.edu')
  139. })
  140. it('should dispatch a `change` event with the domain matched', async function () {
  141. expect(
  142. onChangeStub.calledWith(
  143. 'user@domain.edu',
  144. sinon.match(testInstitutionData[0])
  145. )
  146. ).to.equal(true)
  147. })
  148. })
  149. it('should cache the result and skip subsequent requests', async function () {
  150. fetchMock.reset()
  151. // clear input
  152. fireEvent.change(screen.getByRole('textbox'), {
  153. target: { value: '' },
  154. })
  155. // type a hint to trigger the domain search
  156. fireEvent.change(screen.getByRole('textbox'), {
  157. target: { value: 'user@d' },
  158. })
  159. expect(fetchMock.called()).to.be.false
  160. expect(onChangeStub.calledWith('user@d')).to.equal(true)
  161. await screen.findByText('user@domain.edu')
  162. })
  163. })
  164. describe('while waiting for a response', function () {
  165. beforeEach(async function () {
  166. // type an initial suggestion
  167. fetchMock.get('express:/institutions/domains', testInstitutionData)
  168. fireEvent.change(screen.getByRole('textbox'), {
  169. target: { value: 'user@d' },
  170. })
  171. await screen.findByText('user@domain.edu')
  172. // make sure the next suggestions are delayed
  173. clearDomainCache()
  174. fetchMock.reset()
  175. fetchMock.get('express:/institutions/domains', 200, { delay: 1000 })
  176. })
  177. it('should keep the suggestion if the hint matches the previously matched domain', async function () {
  178. fireEvent.change(screen.getByRole('textbox'), {
  179. target: { value: 'user@do' },
  180. })
  181. screen.getByText('user@domain.edu')
  182. })
  183. it('should remove the suggestion if the hint does not match the previously matched domain', async function () {
  184. fireEvent.change(screen.getByRole('textbox'), {
  185. target: { value: 'user@foo' },
  186. })
  187. expect(screen.queryByText('user@domain.edu')).to.be.null
  188. })
  189. })
  190. })
  191. describe('when the request to fetch institution domains fail', function () {
  192. let onChangeStub
  193. beforeEach(async function () {
  194. // initial request populates the suggestion
  195. fetchMock.get('express:/institutions/domains', testInstitutionData)
  196. onChangeStub = sinon.stub()
  197. render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
  198. fireEvent.change(screen.getByRole('textbox'), {
  199. target: { value: 'user@d' },
  200. })
  201. await screen.findByText('user@domain.edu')
  202. // subsequent requests fail
  203. fetchMock.reset()
  204. fetchMock.get('express:/institutions/domains', 500)
  205. })
  206. it('should clear suggestions', async function () {
  207. fireEvent.change(screen.getByRole('textbox'), {
  208. target: { value: 'user@dom' },
  209. })
  210. const input = screen.getByRole('textbox') as HTMLInputElement
  211. expect(input.value).to.equal('user@dom')
  212. await waitForElementToBeRemoved(() =>
  213. screen.queryByText('user@domain.edu')
  214. )
  215. expect(fetchMock.called()).to.be.true // ensures `domainCache` hasn't been hit
  216. })
  217. })
  218. })