add-email-input.test.tsx 11 KB

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