emails-section-add-new-email.test.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. import {
  2. render,
  3. screen,
  4. fireEvent,
  5. waitForElementToBeRemoved,
  6. } from '@testing-library/react'
  7. import userEvent from '@testing-library/user-event'
  8. import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
  9. import { expect } from 'chai'
  10. import fetchMock from 'fetch-mock'
  11. import { UserEmailData } from '../../../../../../types/user-email'
  12. import { Affiliation } from '../../../../../../types/affiliation'
  13. import withMarkup from '../../../../helpers/with-markup'
  14. const userEmailData: UserEmailData & { affiliation: Affiliation } = {
  15. affiliation: {
  16. cachedConfirmedAt: null,
  17. cachedEntitlement: null,
  18. cachedLastDayToReconfirm: null,
  19. cachedPastReconfirmDate: false,
  20. cachedReconfirmedAt: null,
  21. department: 'Art History',
  22. institution: {
  23. commonsAccount: false,
  24. confirmed: true,
  25. id: 1,
  26. isUniversity: true,
  27. maxConfirmationMonths: null,
  28. name: 'Overleaf',
  29. ssoEnabled: false,
  30. ssoBeta: false,
  31. },
  32. inReconfirmNotificationPeriod: false,
  33. inferred: false,
  34. licence: 'pro_plus',
  35. pastReconfirmDate: false,
  36. portal: { slug: '', templates_count: 1 },
  37. role: 'Reader',
  38. },
  39. email: 'baz@overleaf.com',
  40. default: false,
  41. }
  42. const institutionDomainData = [
  43. {
  44. university: {
  45. id: 1234,
  46. ssoEnabled: true,
  47. name: 'Auto Complete University',
  48. },
  49. hostname: 'autocomplete.edu',
  50. confirmed: true,
  51. },
  52. ] as const
  53. function resetFetchMock() {
  54. fetchMock.reset()
  55. fetchMock.get('express:/institutions/domains', [])
  56. }
  57. describe('<EmailsSection />', function () {
  58. beforeEach(function () {
  59. window.metaAttributesCache.set('ol-ExposedSettings', {
  60. hasAffiliationsFeature: true,
  61. hasSamlFeature: true,
  62. samlInitPath: 'saml/init',
  63. })
  64. fetchMock.reset()
  65. })
  66. afterEach(function () {
  67. window.metaAttributesCache = new Map()
  68. resetFetchMock()
  69. })
  70. it('renders "add another email" button', async function () {
  71. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  72. render(<EmailsSection />)
  73. await screen.findByRole('button', { name: /add another email/i })
  74. })
  75. it('renders input', async function () {
  76. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  77. render(<EmailsSection />)
  78. await fetchMock.flush(true)
  79. const button = await screen.findByRole<HTMLButtonElement>('button', {
  80. name: /add another email/i,
  81. })
  82. fireEvent.click(button)
  83. await screen.findByLabelText(/email/i)
  84. })
  85. it('renders "Start adding your address" until a valid email is typed', async function () {
  86. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  87. fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
  88. fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
  89. render(<EmailsSection />)
  90. await fetchMock.flush(true)
  91. const button = await screen.findByRole<HTMLButtonElement>('button', {
  92. name: /add another email/i,
  93. })
  94. fireEvent.click(button)
  95. const input = screen.getByLabelText(/email/i)
  96. // initially the text is displayed and the "add email" button disabled
  97. screen.getByText('Start by adding your email address.')
  98. expect(
  99. screen.getByRole<HTMLButtonElement>('button', {
  100. name: /add new email/i,
  101. }).disabled
  102. ).to.be.true
  103. // no changes while writing the email address
  104. fireEvent.change(input, {
  105. target: { value: 'partial@email' },
  106. })
  107. screen.getByText('Start by adding your email address.')
  108. expect(
  109. screen.getByRole<HTMLButtonElement>('button', {
  110. name: /add new email/i,
  111. }).disabled
  112. ).to.be.true
  113. // the text is removed when the complete email address is typed, and the "add button" is reenabled
  114. fireEvent.change(input, {
  115. target: { value: 'valid@email.com' },
  116. })
  117. expect(screen.queryByText('Start by adding your email address.')).to.be.null
  118. expect(
  119. screen.getByRole<HTMLButtonElement>('button', {
  120. name: /add new email/i,
  121. }).disabled
  122. ).to.be.false
  123. })
  124. it('renders "add new email" button', async function () {
  125. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  126. render(<EmailsSection />)
  127. const button = await screen.findByRole<HTMLButtonElement>('button', {
  128. name: /add another email/i,
  129. })
  130. fireEvent.click(button)
  131. screen.getByRole('button', { name: /add new email/i })
  132. })
  133. it('prevent users from adding new emails when the limit is reached', async function () {
  134. const emails = []
  135. for (let i = 0; i < 10; i++) {
  136. emails.push({ email: `bar${i}@overleaf.com` })
  137. }
  138. fetchMock.get('/user/emails?ensureAffiliation=true', emails)
  139. render(<EmailsSection />)
  140. const findByTextWithMarkup = withMarkup(screen.findByText)
  141. await findByTextWithMarkup(
  142. 'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
  143. )
  144. expect(screen.queryByRole('button', { name: /add another email/i })).to.not
  145. .exist
  146. })
  147. it('adds new email address', async function () {
  148. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  149. render(<EmailsSection />)
  150. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  151. 'button',
  152. { name: /add another email/i }
  153. )
  154. await fetchMock.flush(true)
  155. resetFetchMock()
  156. fetchMock
  157. .get('/user/emails?ensureAffiliation=true', [userEmailData])
  158. .post('/user/emails', 200)
  159. fireEvent.click(addAnotherEmailBtn)
  160. const input = screen.getByLabelText(/email/i)
  161. fireEvent.change(input, {
  162. target: { value: userEmailData.email },
  163. })
  164. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  165. name: /add new email/i,
  166. })
  167. expect(submitBtn.disabled).to.be.false
  168. fireEvent.click(submitBtn)
  169. expect(submitBtn.disabled).to.be.true
  170. await waitForElementToBeRemoved(() =>
  171. screen.getByRole('button', {
  172. name: /add new email/i,
  173. })
  174. )
  175. screen.getByText(userEmailData.email)
  176. })
  177. it('fails to add add new email address', async function () {
  178. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  179. render(<EmailsSection />)
  180. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  181. 'button',
  182. { name: /add another email/i }
  183. )
  184. await fetchMock.flush(true)
  185. resetFetchMock()
  186. fetchMock
  187. .get('/user/emails?ensureAffiliation=true', [])
  188. .post('/user/emails', 400)
  189. fireEvent.click(addAnotherEmailBtn)
  190. const input = screen.getByLabelText(/email/i)
  191. fireEvent.change(input, {
  192. target: { value: userEmailData.email },
  193. })
  194. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  195. name: /add new email/i,
  196. })
  197. expect(submitBtn.disabled).to.be.false
  198. fireEvent.click(submitBtn)
  199. expect(submitBtn.disabled).to.be.true
  200. await screen.findByText(
  201. /Invalid Request. Please correct the data and try again./i
  202. )
  203. expect(submitBtn).to.not.be.null
  204. expect(submitBtn.disabled).to.be.false
  205. })
  206. it('can link email address to an existing SSO institution', async function () {
  207. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  208. render(<EmailsSection />)
  209. const button = await screen.findByRole<HTMLButtonElement>('button', {
  210. name: /add another email/i,
  211. })
  212. await fetchMock.flush(true)
  213. fetchMock.reset()
  214. fetchMock.get('express:/institutions/domains', institutionDomainData)
  215. await userEvent.click(button)
  216. const input = screen.getByLabelText(/email/i)
  217. fireEvent.change(input, {
  218. target: { value: 'user@autocomplete.edu' },
  219. })
  220. await screen.findByRole('button', { name: 'Link Accounts and Add Email' })
  221. })
  222. it('adds new email address with existing institution and custom departments', async function () {
  223. const country = 'Germany'
  224. const customDepartment = 'Custom department'
  225. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  226. render(<EmailsSection />)
  227. const button = await screen.findByRole<HTMLButtonElement>('button', {
  228. name: /add another email/i,
  229. })
  230. await fetchMock.flush(true)
  231. resetFetchMock()
  232. await userEvent.click(button)
  233. await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
  234. await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
  235. const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
  236. name: /university/i,
  237. })
  238. expect(universityInput.disabled).to.be.true
  239. fetchMock.get('/institutions/list?country_code=de', [
  240. {
  241. id: userEmailData.affiliation.institution.id,
  242. name: userEmailData.affiliation.institution.name,
  243. country_code: 'de',
  244. departments: [customDepartment],
  245. },
  246. ])
  247. // Select the country from dropdown
  248. await userEvent.type(
  249. screen.getByRole('textbox', {
  250. name: /country/i,
  251. }),
  252. country
  253. )
  254. await userEvent.click(screen.getByText(country))
  255. expect(universityInput.disabled).to.be.false
  256. await fetchMock.flush(true)
  257. resetFetchMock()
  258. // Select the university from dropdown
  259. await userEvent.click(universityInput)
  260. await userEvent.click(
  261. await screen.findByText(userEmailData.affiliation.institution.name)
  262. )
  263. const roleInput = screen.getByRole('textbox', { name: /role/i })
  264. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  265. const departmentInput = screen.getByRole('textbox', { name: /department/i })
  266. await userEvent.click(departmentInput)
  267. await userEvent.click(screen.getByText(customDepartment))
  268. const userEmailDataCopy = {
  269. ...userEmailData,
  270. affiliation: {
  271. ...userEmailData.affiliation,
  272. department: customDepartment,
  273. },
  274. }
  275. fetchMock
  276. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  277. .post(/\/user\/emails/, 200)
  278. await userEvent.click(
  279. screen.getByRole('button', {
  280. name: /add new email/i,
  281. })
  282. )
  283. const [[, request]] = fetchMock.calls(/\/user\/emails/)
  284. expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
  285. email: userEmailData.email,
  286. university: {
  287. id: userEmailData.affiliation?.institution.id,
  288. },
  289. role: userEmailData.affiliation?.role,
  290. department: customDepartment,
  291. })
  292. screen.getByText(userEmailData.email)
  293. screen.getByText(userEmailData.affiliation.institution.name)
  294. screen.getByText(userEmailData.affiliation.role!, { exact: false })
  295. screen.getByText(customDepartment, { exact: false })
  296. })
  297. it('autocompletes institution name', async function () {
  298. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  299. render(<EmailsSection />)
  300. const button = await screen.findByRole<HTMLButtonElement>('button', {
  301. name: /add another email/i,
  302. })
  303. await fetchMock.flush(true)
  304. resetFetchMock()
  305. fetchMock.get('/institutions/list?country_code=de', [
  306. {
  307. id: 1,
  308. name: 'University of Bonn',
  309. },
  310. {
  311. id: 2,
  312. name: 'Bochum institute of Science',
  313. },
  314. ])
  315. // open "add new email" section and click "let us know" to open the Country/University form
  316. await userEvent.click(button)
  317. await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
  318. await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
  319. // select a country
  320. const countryInput = screen.getByRole<HTMLInputElement>('textbox', {
  321. name: /country/i,
  322. })
  323. await userEvent.click(countryInput)
  324. await userEvent.type(countryInput, 'Germ')
  325. await userEvent.click(await screen.findByText('Germany'))
  326. // match several universities on initial typing
  327. const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
  328. name: /university/i,
  329. })
  330. await userEvent.click(universityInput)
  331. await userEvent.type(universityInput, 'bo')
  332. await screen.findByText('University of Bonn')
  333. await screen.findByText('Bochum institute of Science')
  334. // match a single university when typing to refine the search
  335. await userEvent.type(universityInput, 'nn')
  336. await screen.findByText('University of Bonn')
  337. expect(screen.queryByText('Bochum institute of Science')).to.be.null
  338. })
  339. it('adds new email address without existing institution', async function () {
  340. const country = 'Germany'
  341. const countryCode = 'de'
  342. const newUniversity = 'Abcdef'
  343. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  344. render(<EmailsSection />)
  345. const button = await screen.findByRole<HTMLButtonElement>('button', {
  346. name: /add another email/i,
  347. })
  348. await fetchMock.flush(true)
  349. resetFetchMock()
  350. await userEvent.click(button)
  351. await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
  352. await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
  353. const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
  354. name: /university/i,
  355. })
  356. expect(universityInput.disabled).to.be.true
  357. fetchMock.get(/\/institutions\/list/, [
  358. {
  359. id: userEmailData.affiliation.institution.id,
  360. name: userEmailData.affiliation.institution.name,
  361. country_code: 'de',
  362. departments: [],
  363. },
  364. ])
  365. // Select the country from dropdown
  366. await userEvent.type(
  367. screen.getByRole('textbox', {
  368. name: /country/i,
  369. }),
  370. country
  371. )
  372. await userEvent.click(screen.getByText(country))
  373. expect(universityInput.disabled).to.be.false
  374. await fetchMock.flush(true)
  375. resetFetchMock()
  376. // Enter the university manually
  377. await userEvent.type(universityInput, newUniversity)
  378. const roleInput = screen.getByRole('textbox', { name: /role/i })
  379. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  380. const departmentInput = screen.getByRole('textbox', { name: /department/i })
  381. await userEvent.type(departmentInput, userEmailData.affiliation.department!)
  382. const userEmailDataCopy = {
  383. ...userEmailData,
  384. affiliation: {
  385. ...userEmailData.affiliation,
  386. institution: {
  387. ...userEmailData.affiliation.institution,
  388. name: newUniversity,
  389. },
  390. },
  391. }
  392. fetchMock
  393. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  394. .post(/\/user\/emails/, 200)
  395. await userEvent.click(
  396. screen.getByRole('button', {
  397. name: /add new email/i,
  398. })
  399. )
  400. const [[, request]] = fetchMock.calls(/\/user\/emails/)
  401. expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
  402. email: userEmailData.email,
  403. university: {
  404. name: newUniversity,
  405. country_code: countryCode,
  406. },
  407. role: userEmailData.affiliation?.role,
  408. department: userEmailData.affiliation?.department,
  409. })
  410. screen.getByText(userEmailData.email)
  411. screen.getByText(newUniversity)
  412. screen.getByText(userEmailData.affiliation.role!, { exact: false })
  413. screen.getByText(userEmailData.affiliation.department!, { exact: false })
  414. })
  415. it('shows country, university, role and department fields based on whether `change` was clicked or not', async function () {
  416. const institutionDomainDataCopy = [
  417. {
  418. ...institutionDomainData[0],
  419. university: {
  420. ...institutionDomainData[0].university,
  421. ssoEnabled: false,
  422. },
  423. },
  424. ]
  425. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  426. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  427. render(<EmailsSection />)
  428. const button = await screen.findByRole<HTMLButtonElement>('button', {
  429. name: /add another email/i,
  430. })
  431. await fetchMock.flush(true)
  432. fetchMock.reset()
  433. fetchMock.get(
  434. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  435. institutionDomainDataCopy
  436. )
  437. await userEvent.click(button)
  438. await userEvent.type(
  439. screen.getByLabelText(/email/i),
  440. `user@${hostnameFirstChar}`
  441. )
  442. await userEvent.keyboard('{Tab}')
  443. await fetchMock.flush(true)
  444. fetchMock.reset()
  445. expect(
  446. screen.queryByRole('textbox', {
  447. name: /country/i,
  448. })
  449. ).to.be.null
  450. expect(
  451. screen.queryByRole('textbox', {
  452. name: /university/i,
  453. })
  454. ).to.be.null
  455. screen.getByRole('textbox', {
  456. name: /role/i,
  457. })
  458. screen.getByRole('textbox', {
  459. name: /department/i,
  460. })
  461. await userEvent.click(screen.getByRole('button', { name: /change/i }))
  462. screen.getByRole('textbox', {
  463. name: /country/i,
  464. })
  465. screen.getByRole('textbox', {
  466. name: /university/i,
  467. })
  468. expect(
  469. screen.queryByRole('textbox', {
  470. name: /role/i,
  471. })
  472. ).to.be.null
  473. expect(
  474. screen.queryByRole('textbox', {
  475. name: /department/i,
  476. })
  477. ).to.be.null
  478. })
  479. it('displays institution name with change button when autocompleted and adds new record', async function () {
  480. const institutionDomainDataCopy = [
  481. {
  482. ...institutionDomainData[0],
  483. university: {
  484. ...institutionDomainData[0].university,
  485. ssoEnabled: false,
  486. },
  487. },
  488. ]
  489. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  490. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  491. render(<EmailsSection />)
  492. const button = await screen.findByRole<HTMLButtonElement>('button', {
  493. name: /add another email/i,
  494. })
  495. await fetchMock.flush(true)
  496. fetchMock.reset()
  497. fetchMock.get(
  498. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  499. institutionDomainDataCopy
  500. )
  501. await userEvent.click(button)
  502. await userEvent.type(
  503. screen.getByLabelText(/email/i),
  504. `user@${hostnameFirstChar}`
  505. )
  506. await userEvent.keyboard('{Tab}')
  507. await fetchMock.flush(true)
  508. fetchMock.reset()
  509. screen.getByText(institutionDomainDataCopy[0].university.name)
  510. const userEmailDataCopy = {
  511. ...userEmailData,
  512. affiliation: {
  513. ...userEmailData.affiliation,
  514. institution: {
  515. ...userEmailData.affiliation.institution,
  516. name: institutionDomainDataCopy[0].university.name,
  517. },
  518. },
  519. }
  520. fetchMock
  521. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  522. .post('/user/emails', 200)
  523. await userEvent.type(
  524. screen.getByRole('textbox', { name: /role/i }),
  525. userEmailData.affiliation.role!
  526. )
  527. await userEvent.type(
  528. screen.getByRole('textbox', { name: /department/i }),
  529. userEmailData.affiliation.department!
  530. )
  531. await userEvent.click(
  532. screen.getByRole('button', {
  533. name: /add new email/i,
  534. })
  535. )
  536. await fetchMock.flush(true)
  537. fetchMock.reset()
  538. screen.getByText(userEmailDataCopy.affiliation.institution.name, {
  539. exact: false,
  540. })
  541. screen.getByText(userEmailDataCopy.affiliation.role!, { exact: false })
  542. screen.getByText(userEmailDataCopy.affiliation.department!, {
  543. exact: false,
  544. })
  545. })
  546. })