add-email-input.test.tsx 9.6 KB

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