add-email-input.test.tsx 10 KB

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