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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125
  1. import {
  2. render,
  3. screen,
  4. fireEvent,
  5. waitForElementToBeRemoved,
  6. within,
  7. } from '@testing-library/react'
  8. import userEvent from '@testing-library/user-event'
  9. import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
  10. import { expect } from 'chai'
  11. import fetchMock from 'fetch-mock'
  12. import { UserEmailData } from '../../../../../../types/user-email'
  13. import { Affiliation } from '../../../../../../types/affiliation'
  14. import withMarkup from '../../../../helpers/with-markup'
  15. import getMeta from '@/utils/meta'
  16. import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
  17. import { SplitTestProvider } from '@/shared/context/split-test-context'
  18. const userEmailData: UserEmailData & { affiliation: Affiliation } = {
  19. affiliation: {
  20. cachedConfirmedAt: null,
  21. cachedEntitlement: null,
  22. cachedLastDayToReconfirm: null,
  23. cachedPastReconfirmDate: false,
  24. cachedReconfirmedAt: null,
  25. department: 'Art History',
  26. institution: {
  27. commonsAccount: false,
  28. writefullCommonsAccount: false,
  29. confirmed: true,
  30. id: 1,
  31. isUniversity: true,
  32. maxConfirmationMonths: null,
  33. name: 'Overleaf',
  34. ssoEnabled: false,
  35. ssoBeta: false,
  36. },
  37. inReconfirmNotificationPeriod: false,
  38. inferred: false,
  39. licence: 'pro_plus',
  40. pastReconfirmDate: false,
  41. portal: { slug: '', templates_count: 1 },
  42. role: 'Reader',
  43. },
  44. email: 'baz@overleaf.com',
  45. default: false,
  46. }
  47. const institutionDomainData = [
  48. {
  49. university: {
  50. id: 1234,
  51. ssoEnabled: true,
  52. name: 'Auto Complete University',
  53. },
  54. hostname: 'autocomplete.edu',
  55. confirmed: true,
  56. },
  57. ] as const
  58. function renderEmailsSection() {
  59. return render(<EmailsSection />, {
  60. wrapper: ({ children }) => (
  61. <SplitTestProvider>{children}</SplitTestProvider>
  62. ),
  63. })
  64. }
  65. function resetFetchMock() {
  66. fetchMock.removeRoutes().clearHistory()
  67. fetchMock.get('express:/institutions/domains', [])
  68. }
  69. async function confirmCodeForEmail(email: string) {
  70. const inputCode = await screen.findByRole('textbox', {
  71. name: /Enter the 6-digit confirmation code sent to /,
  72. })
  73. screen.getByText(email, { exact: false })
  74. fireEvent.change(inputCode, { target: { value: '123456' } })
  75. const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
  76. name: 'Confirm',
  77. })
  78. fireEvent.click(submitCodeBtn)
  79. await waitForElementToBeRemoved(() =>
  80. screen.getByRole('button', { name: 'Confirming' })
  81. )
  82. }
  83. describe('<EmailsSection />', function () {
  84. beforeEach(function () {
  85. Object.assign(getMeta('ol-ExposedSettings'), {
  86. hasAffiliationsFeature: true,
  87. hasSamlFeature: true,
  88. samlInitPath: 'saml/init',
  89. })
  90. fetchMock.removeRoutes().clearHistory()
  91. })
  92. afterEach(function () {
  93. resetFetchMock()
  94. clearDomainCache()
  95. })
  96. it('renders "add another email" button', async function () {
  97. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  98. renderEmailsSection()
  99. await screen.findByRole('button', { name: 'Add another email' })
  100. })
  101. it('renders input', async function () {
  102. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  103. renderEmailsSection()
  104. await fetchMock.callHistory.flush(true)
  105. const button = await screen.findByRole<HTMLButtonElement>('button', {
  106. name: 'Add another email',
  107. })
  108. fireEvent.click(button)
  109. await screen.findByRole('textbox', { name: 'Email' })
  110. })
  111. it('renders "Start adding your address" until a valid email is typed', async function () {
  112. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  113. fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
  114. fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
  115. renderEmailsSection()
  116. await fetchMock.callHistory.flush(true)
  117. const button = await screen.findByRole<HTMLButtonElement>('button', {
  118. name: 'Add another email',
  119. })
  120. fireEvent.click(button)
  121. const input = screen.getByRole('textbox', { name: 'Email' })
  122. // initially the text is displayed and the "add email" button disabled
  123. screen.getByText('Start by adding your email address.')
  124. expect(
  125. screen.getByRole<HTMLButtonElement>('button', {
  126. name: 'Add new email',
  127. }).disabled
  128. ).to.be.true
  129. // no changes while writing the email address
  130. fireEvent.change(input, {
  131. target: { value: 'partial@email' },
  132. })
  133. screen.getByText('Start by adding your email address.')
  134. expect(
  135. screen.getByRole<HTMLButtonElement>('button', {
  136. name: 'Add new email',
  137. }).disabled
  138. ).to.be.true
  139. // the text is removed when the complete email address is typed, and the "add button" is reenabled
  140. fireEvent.change(input, {
  141. target: { value: 'valid@email.com' },
  142. })
  143. expect(screen.queryByText('Start by adding your email address.')).to.be.null
  144. expect(
  145. screen.getByRole<HTMLButtonElement>('button', {
  146. name: 'Add new email',
  147. }).disabled
  148. ).to.be.false
  149. })
  150. it('renders "add new email" button', async function () {
  151. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  152. renderEmailsSection()
  153. const button = await screen.findByRole<HTMLButtonElement>('button', {
  154. name: 'Add another email',
  155. })
  156. fireEvent.click(button)
  157. screen.getByRole('button', { name: 'Add new email' })
  158. })
  159. it('prevent users from adding new emails when the limit is reached', async function () {
  160. const emails = []
  161. for (let i = 0; i < 10; i++) {
  162. emails.push({ email: `bar${i}@overleaf.com` })
  163. }
  164. fetchMock.get('/user/emails?ensureAffiliation=true', emails)
  165. renderEmailsSection()
  166. const findByTextWithMarkup = withMarkup(screen.findByText)
  167. await findByTextWithMarkup(
  168. 'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
  169. )
  170. expect(screen.queryByRole('button', { name: 'Add another email' })).to.not
  171. .exist
  172. })
  173. it('adds new email address', async function () {
  174. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  175. renderEmailsSection()
  176. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  177. 'button',
  178. { name: 'Add another email' }
  179. )
  180. await fetchMock.callHistory.flush(true)
  181. resetFetchMock()
  182. fetchMock
  183. .get('/user/emails?ensureAffiliation=true', [userEmailData])
  184. .post('/user/emails/secondary', 200)
  185. .post('/user/emails/confirm-secondary', 200)
  186. fireEvent.click(addAnotherEmailBtn)
  187. const input = screen.getByRole('textbox', { name: 'Email' })
  188. fireEvent.change(input, {
  189. target: { value: userEmailData.email },
  190. })
  191. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  192. name: 'Add new email',
  193. })
  194. expect(submitBtn.disabled).to.be.false
  195. fireEvent.click(submitBtn)
  196. expect(submitBtn.disabled).to.be.true
  197. await waitForElementToBeRemoved(() =>
  198. screen.getByRole('button', {
  199. name: /adding/i,
  200. })
  201. )
  202. await confirmCodeForEmail(userEmailData.email)
  203. const elements = await screen.findAllByText(userEmailData.email)
  204. expect(elements.length).to.equal(2)
  205. })
  206. it('fails to add add new email address', async function () {
  207. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  208. renderEmailsSection()
  209. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  210. 'button',
  211. { name: 'Add another email' }
  212. )
  213. await fetchMock.callHistory.flush(true)
  214. resetFetchMock()
  215. fetchMock
  216. .get('/user/emails?ensureAffiliation=true', [])
  217. .post('/user/emails/secondary', 400)
  218. fireEvent.click(addAnotherEmailBtn)
  219. const input = screen.getByRole('textbox', { name: 'Email' })
  220. fireEvent.change(input, {
  221. target: { value: userEmailData.email },
  222. })
  223. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  224. name: 'Add new email',
  225. })
  226. expect(submitBtn.disabled).to.be.false
  227. fireEvent.click(submitBtn)
  228. expect(submitBtn.disabled).to.be.true
  229. await screen.findByText(
  230. 'Invalid Request. Please correct the data and try again.'
  231. )
  232. expect(submitBtn).to.not.be.null
  233. expect(submitBtn.disabled).to.be.false
  234. })
  235. it('can link email address to an existing SSO institution', async function () {
  236. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  237. renderEmailsSection()
  238. const button = await screen.findByRole<HTMLButtonElement>('button', {
  239. name: 'Add another email',
  240. })
  241. await fetchMock.callHistory.flush(true)
  242. fetchMock.removeRoutes().clearHistory()
  243. fetchMock.get('express:/institutions/domains', institutionDomainData)
  244. await userEvent.click(button)
  245. const input = screen.getByRole('textbox', { name: 'Email' })
  246. fireEvent.change(input, {
  247. target: { value: 'user@autocomplete.edu' },
  248. })
  249. await screen.findByRole('button', { name: 'Link accounts and add email' })
  250. })
  251. it('prevents user from linking to same SSO institution twice', async function () {
  252. fetchMock.get('/user/emails?ensureAffiliation=true', [
  253. { email: 'bar@autocomplete.edu', samlProviderId: '1234' },
  254. ])
  255. renderEmailsSection()
  256. const button = await screen.findByRole<HTMLButtonElement>('button', {
  257. name: 'Add another email',
  258. })
  259. await fetchMock.callHistory.flush(true)
  260. fetchMock.removeRoutes().clearHistory()
  261. fetchMock.get('express:/institutions/domains', institutionDomainData)
  262. await userEvent.click(button)
  263. const input = screen.getByRole('textbox', { name: 'Email' })
  264. fireEvent.change(input, {
  265. target: { value: 'baz@autocomplete.edu' },
  266. })
  267. await screen.findByText(
  268. 'This institution is already linked with your account via another email address.'
  269. )
  270. })
  271. it('adds new email address with existing institution and custom departments', async function () {
  272. const country = 'Germany'
  273. const customDepartment = 'Custom department'
  274. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  275. renderEmailsSection()
  276. const button = await screen.findByRole<HTMLButtonElement>('button', {
  277. name: 'Add another email',
  278. })
  279. await fetchMock.callHistory.flush(true)
  280. resetFetchMock()
  281. await userEvent.click(button)
  282. await userEvent.type(
  283. screen.getByRole('textbox', { name: 'Email' }),
  284. userEmailData.email
  285. )
  286. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  287. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  288. name: 'University',
  289. })
  290. expect(universityInput.disabled).to.be.true
  291. fetchMock.get('/institutions/list?country_code=de', [
  292. {
  293. id: userEmailData.affiliation.institution.id,
  294. name: userEmailData.affiliation.institution.name,
  295. country_code: 'de',
  296. departments: [customDepartment],
  297. },
  298. ])
  299. // Select the country from dropdown
  300. await userEvent.type(
  301. screen.getByRole('combobox', {
  302. name: 'Country',
  303. }),
  304. country
  305. )
  306. await userEvent.click(screen.getByText(country))
  307. expect(universityInput.disabled).to.be.false
  308. await fetchMock.callHistory.flush(true)
  309. resetFetchMock()
  310. // Select the university from dropdown
  311. await userEvent.click(universityInput)
  312. await userEvent.click(
  313. await screen.findByText(userEmailData.affiliation.institution.name)
  314. )
  315. const roleInput = screen.getByRole('combobox', { name: 'Role' })
  316. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  317. const departmentInput = screen.getByRole('combobox', {
  318. name: 'Department',
  319. })
  320. await userEvent.click(departmentInput)
  321. await userEvent.click(screen.getByText(customDepartment))
  322. const userEmailDataCopy = {
  323. ...userEmailData,
  324. affiliation: {
  325. ...userEmailData.affiliation,
  326. department: customDepartment,
  327. },
  328. }
  329. fetchMock
  330. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  331. .post('/user/emails/secondary', 200)
  332. .post('/user/emails/confirm-secondary', 200)
  333. await userEvent.click(
  334. screen.getByRole('button', {
  335. name: 'Add new email',
  336. })
  337. )
  338. const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
  339. expect(
  340. JSON.parse(request?.options.body?.toString() || '{}')
  341. ).to.deep.include({
  342. email: userEmailData.email,
  343. university: {
  344. id: userEmailData.affiliation?.institution.id,
  345. },
  346. role: userEmailData.affiliation?.role,
  347. department: customDepartment,
  348. })
  349. await screen.findByText(/Enter the 6-digit confirmation code sent to /)
  350. await confirmCodeForEmail(userEmailData.email)
  351. await screen.findByText(userEmailData.affiliation.role!, { exact: false })
  352. await screen.findByText(customDepartment, { exact: false })
  353. })
  354. it('autocompletes institution name', async function () {
  355. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  356. renderEmailsSection()
  357. const button = await screen.findByRole<HTMLButtonElement>('button', {
  358. name: 'Add another email',
  359. })
  360. await fetchMock.callHistory.flush(true)
  361. resetFetchMock()
  362. fetchMock.get('/institutions/list?country_code=de', [
  363. {
  364. id: 1,
  365. name: 'University of Bonn',
  366. },
  367. {
  368. id: 2,
  369. name: 'Bochum institute of Science',
  370. },
  371. ])
  372. // open "add new email" section and click "let us know" to open the Country/University form
  373. await userEvent.click(button)
  374. await userEvent.type(
  375. screen.getByRole('textbox', { name: 'Email' }),
  376. userEmailData.email
  377. )
  378. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  379. // select a country
  380. const countryInput = screen.getByRole<HTMLInputElement>('combobox', {
  381. name: 'Country',
  382. })
  383. await userEvent.click(countryInput)
  384. await userEvent.type(countryInput, 'Germ')
  385. await userEvent.click(await screen.findByText('Germany'))
  386. // match several universities on initial typing
  387. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  388. name: 'University',
  389. })
  390. await userEvent.click(universityInput)
  391. await userEvent.type(universityInput, 'bo')
  392. await screen.findByText('University of Bonn')
  393. await screen.findByText('Bochum institute of Science')
  394. // match a single university when typing to refine the search
  395. await userEvent.type(universityInput, 'nn')
  396. await screen.findByText('University of Bonn')
  397. expect(screen.queryByText('Bochum institute of Science')).to.be.null
  398. })
  399. it('adds new email address without existing institution', async function () {
  400. const country = 'Germany'
  401. const countryCode = 'de'
  402. const newUniversity = 'Abcdef'
  403. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  404. renderEmailsSection()
  405. const button = await screen.findByRole<HTMLButtonElement>('button', {
  406. name: 'Add another email',
  407. })
  408. await fetchMock.callHistory.flush(true)
  409. resetFetchMock()
  410. await userEvent.click(button)
  411. await userEvent.type(
  412. screen.getByRole('textbox', { name: 'Email' }),
  413. userEmailData.email
  414. )
  415. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  416. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  417. name: 'University',
  418. })
  419. expect(universityInput.disabled).to.be.true
  420. fetchMock.get(/\/institutions\/list/, [
  421. {
  422. id: userEmailData.affiliation.institution.id,
  423. name: userEmailData.affiliation.institution.name,
  424. country_code: 'de',
  425. departments: [],
  426. },
  427. ])
  428. // Select the country from dropdown
  429. await userEvent.type(
  430. screen.getByRole('combobox', {
  431. name: 'Country',
  432. }),
  433. country
  434. )
  435. await userEvent.click(screen.getByText(country))
  436. expect(universityInput.disabled).to.be.false
  437. await fetchMock.callHistory.flush(true)
  438. resetFetchMock()
  439. // Enter the university manually
  440. await userEvent.type(universityInput, newUniversity)
  441. const roleInput = screen.getByRole('combobox', { name: 'Role' })
  442. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  443. const departmentInput = screen.getByRole('combobox', {
  444. name: 'Department',
  445. })
  446. await userEvent.type(departmentInput, userEmailData.affiliation.department!)
  447. const userEmailDataCopy = {
  448. ...userEmailData,
  449. affiliation: {
  450. ...userEmailData.affiliation,
  451. institution: {
  452. ...userEmailData.affiliation.institution,
  453. name: newUniversity,
  454. },
  455. },
  456. }
  457. fetchMock
  458. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  459. .post('/user/emails/secondary', 200)
  460. .post('/user/emails/confirm-secondary', 200)
  461. await userEvent.click(
  462. screen.getByRole('button', {
  463. name: 'Add new email',
  464. })
  465. )
  466. await confirmCodeForEmail(userEmailData.email)
  467. const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
  468. expect(
  469. JSON.parse(request?.options.body?.toString() || '{}')
  470. ).to.deep.include({
  471. email: userEmailData.email,
  472. university: {
  473. name: newUniversity,
  474. country_code: countryCode,
  475. },
  476. role: userEmailData.affiliation?.role,
  477. department: userEmailData.affiliation?.department,
  478. })
  479. const elements = await screen.findAllByText(userEmailData.email)
  480. expect(elements.length).to.equal(2)
  481. await screen.findByText(newUniversity)
  482. await screen.findByText(userEmailData.affiliation.role!, { exact: false })
  483. await screen.findByText(userEmailData.affiliation.department!, {
  484. exact: false,
  485. })
  486. })
  487. it('shows country, university, role and department fields based on whether `change` was clicked or not', async function () {
  488. const institutionDomainDataCopy = [
  489. {
  490. ...institutionDomainData[0],
  491. university: {
  492. ...institutionDomainData[0].university,
  493. ssoEnabled: false,
  494. },
  495. },
  496. ]
  497. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  498. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  499. renderEmailsSection()
  500. const button = await screen.findByRole<HTMLButtonElement>('button', {
  501. name: 'Add another email',
  502. })
  503. await fetchMock.callHistory.flush(true)
  504. fetchMock.removeRoutes().clearHistory()
  505. fetchMock.get(
  506. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  507. institutionDomainDataCopy
  508. )
  509. await userEvent.click(button)
  510. await userEvent.type(
  511. screen.getByRole('textbox', { name: 'Email' }),
  512. `user@${hostnameFirstChar}`
  513. )
  514. await userEvent.keyboard('{Tab}')
  515. await fetchMock.callHistory.flush(true)
  516. fetchMock.removeRoutes().clearHistory()
  517. expect(
  518. screen.queryByRole('combobox', {
  519. name: 'Country',
  520. })
  521. ).to.be.null
  522. expect(
  523. screen.queryByRole('combobox', {
  524. name: 'University',
  525. })
  526. ).to.be.null
  527. screen.getByRole('combobox', {
  528. name: 'Role',
  529. })
  530. screen.getByRole('combobox', {
  531. name: 'Department',
  532. })
  533. await userEvent.click(screen.getByRole('button', { name: 'Change' }))
  534. screen.getByRole('combobox', {
  535. name: 'Country',
  536. })
  537. screen.getByRole('combobox', {
  538. name: 'University',
  539. })
  540. expect(
  541. screen.queryByRole('combobox', {
  542. name: 'Role',
  543. })
  544. ).to.be.null
  545. expect(
  546. screen.queryByRole('combobox', {
  547. name: 'Department',
  548. })
  549. ).to.be.null
  550. })
  551. it('displays institution name with change button when autocompleted and adds new record', async function () {
  552. const institutionDomainDataCopy = [
  553. {
  554. ...institutionDomainData[0],
  555. university: {
  556. ...institutionDomainData[0].university,
  557. ssoEnabled: false,
  558. },
  559. },
  560. ]
  561. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  562. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  563. renderEmailsSection()
  564. const button = await screen.findByRole<HTMLButtonElement>('button', {
  565. name: 'Add another email',
  566. })
  567. await fetchMock.callHistory.flush(true)
  568. fetchMock.removeRoutes().clearHistory()
  569. fetchMock.get(
  570. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  571. institutionDomainDataCopy
  572. )
  573. await userEvent.click(button)
  574. await userEvent.type(
  575. screen.getByRole('textbox', { name: 'Email' }),
  576. `user@${hostnameFirstChar}`
  577. )
  578. await userEvent.keyboard('{Tab}')
  579. await fetchMock.callHistory.flush(true)
  580. fetchMock.removeRoutes().clearHistory()
  581. screen.getByText(institutionDomainDataCopy[0].university.name)
  582. const userEmailDataCopy = {
  583. ...userEmailData,
  584. affiliation: {
  585. ...userEmailData.affiliation,
  586. institution: {
  587. ...userEmailData.affiliation.institution,
  588. name: institutionDomainDataCopy[0].university.name,
  589. },
  590. },
  591. }
  592. fetchMock
  593. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  594. .post('/user/emails/secondary', 200)
  595. .post('/user/emails/confirm-secondary', 200)
  596. await userEvent.type(
  597. screen.getByRole('combobox', { name: 'Role' }),
  598. userEmailData.affiliation.role!
  599. )
  600. await userEvent.type(
  601. screen.getByRole('combobox', { name: 'Department' }),
  602. userEmailData.affiliation.department!
  603. )
  604. await userEvent.click(
  605. screen.getByRole('button', {
  606. name: 'Add new email',
  607. })
  608. )
  609. await confirmCodeForEmail('user@autocomplete.edu')
  610. await fetchMock.callHistory.flush(true)
  611. fetchMock.removeRoutes().clearHistory()
  612. screen.getByText(userEmailDataCopy.affiliation.institution.name, {
  613. exact: false,
  614. })
  615. screen.getByText(userEmailDataCopy.affiliation.role!, { exact: false })
  616. screen.getByText(userEmailDataCopy.affiliation.department!, {
  617. exact: false,
  618. })
  619. })
  620. describe('when domain is captured by a group', function () {
  621. describe('and managed users is not enabled', function () {
  622. beforeEach(async function () {
  623. await fetchMock.callHistory.flush(true)
  624. fetchMock.removeRoutes().clearHistory()
  625. const institution = {
  626. university: {
  627. id: 1234,
  628. ssoEnabled: false,
  629. name: 'Auto Complete University',
  630. },
  631. hostname: 'autocomplete.edu',
  632. confirmed: true,
  633. group: {
  634. domainCaptureEnabled: true,
  635. ssoConfig: {
  636. enabled: true,
  637. },
  638. },
  639. }
  640. fetchMock.get('express:/institutions/domains', [institution])
  641. })
  642. it('can add email address via SSO', async function () {
  643. // note: this UI is a WIP
  644. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  645. renderEmailsSection()
  646. const button = await screen.findByRole<HTMLButtonElement>('button', {
  647. name: 'Add another email',
  648. })
  649. await userEvent.click(button)
  650. const input = screen.getByRole('textbox', { name: 'Email' })
  651. fireEvent.change(input, {
  652. target: { value: 'user@autocomplete.edu' },
  653. })
  654. await screen.findByText('This feature is currently unavailable.')
  655. })
  656. })
  657. describe('and managed users is enabled', function () {
  658. beforeEach(async function () {
  659. await fetchMock.callHistory.flush(true)
  660. fetchMock.removeRoutes().clearHistory()
  661. const institution = {
  662. university: {
  663. id: 1234,
  664. ssoEnabled: false,
  665. name: 'Auto Complete University',
  666. },
  667. hostname: 'autocomplete.edu',
  668. confirmed: true,
  669. group: {
  670. domainCaptureEnabled: true,
  671. managedUsersEnabled: true,
  672. ssoConfig: {
  673. enabled: true,
  674. },
  675. },
  676. }
  677. fetchMock.get('express:/institutions/domains', [institution])
  678. })
  679. it('renders error', async function () {
  680. // note: this UI is a WIP
  681. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  682. renderEmailsSection()
  683. const button = await screen.findByRole<HTMLButtonElement>('button', {
  684. name: 'Add another email',
  685. })
  686. await userEvent.click(button)
  687. const input = screen.getByRole('textbox', { name: 'Email' })
  688. fireEvent.change(input, {
  689. target: { value: 'user@autocomplete.edu' },
  690. })
  691. const notification = await screen.findByRole('alert')
  692. within(notification).getByText(
  693. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  694. { exact: false }
  695. )
  696. })
  697. })
  698. describe('if Commons SSO is enabled and managed users is not enabled, Commons takes priority over group UI', function () {
  699. beforeEach(async function () {
  700. await fetchMock.callHistory.flush(true)
  701. fetchMock.removeRoutes().clearHistory()
  702. const institution = {
  703. university: {
  704. id: 1234,
  705. ssoEnabled: true,
  706. name: 'Auto Complete University',
  707. },
  708. hostname: 'autocomplete.edu',
  709. confirmed: true,
  710. group: {
  711. domainCaptureEnabled: true,
  712. ssoConfig: {
  713. enabled: true,
  714. },
  715. },
  716. }
  717. fetchMock.get('express:/institutions/domains', [institution])
  718. })
  719. it('renders Commons UI', async function () {
  720. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  721. renderEmailsSection()
  722. const button = await screen.findByRole<HTMLButtonElement>('button', {
  723. name: 'Add another email',
  724. })
  725. await userEvent.click(button)
  726. const input = screen.getByRole('textbox', { name: 'Email' })
  727. fireEvent.change(input, {
  728. target: { value: 'user@autocomplete.edu' },
  729. })
  730. await screen.findByRole('button', {
  731. name: 'Link accounts and add email',
  732. })
  733. })
  734. })
  735. describe('if Commons SSO is enabled and managed users is enabled, group domain capture takes priority', function () {
  736. beforeEach(async function () {
  737. await fetchMock.callHistory.flush(true)
  738. fetchMock.removeRoutes().clearHistory()
  739. const institution = {
  740. university: {
  741. id: 1234,
  742. ssoEnabled: true,
  743. name: 'Auto Complete University',
  744. },
  745. hostname: 'autocomplete.edu',
  746. confirmed: true,
  747. group: {
  748. domainCaptureEnabled: true,
  749. managedUsersEnabled: true,
  750. ssoConfig: {
  751. enabled: true,
  752. },
  753. },
  754. }
  755. fetchMock.get('express:/institutions/domains', [institution])
  756. })
  757. it('renders group domain-capture error instead of Commons UI', async function () {
  758. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  759. renderEmailsSection()
  760. const button = await screen.findByRole<HTMLButtonElement>('button', {
  761. name: 'Add another email',
  762. })
  763. await userEvent.click(button)
  764. const input = screen.getByRole('textbox', { name: 'Email' })
  765. fireEvent.change(input, {
  766. target: { value: 'user@autocomplete.edu' },
  767. })
  768. const notification = await screen.findByRole('alert')
  769. within(notification).getByText(
  770. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  771. { exact: false }
  772. )
  773. expect(
  774. screen.queryByRole('button', {
  775. name: 'Link accounts and add email',
  776. })
  777. ).to.be.null
  778. })
  779. })
  780. describe('when the domain-captured-by-group feature flag is enabled', function () {
  781. beforeEach(function () {
  782. window.metaAttributesCache.set('ol-splitTestVariants', {
  783. 'domain-captured-by-group': 'enabled',
  784. })
  785. })
  786. afterEach(function () {
  787. window.metaAttributesCache.set('ol-splitTestVariants', {})
  788. })
  789. describe('and capturedByGroup is true with managedUsersEnabled', function () {
  790. beforeEach(async function () {
  791. await fetchMock.callHistory.flush(true)
  792. fetchMock.removeRoutes().clearHistory()
  793. const institution = {
  794. university: {
  795. id: 1234,
  796. ssoEnabled: false,
  797. name: 'Auto Complete University',
  798. },
  799. hostname: 'autocomplete.edu',
  800. confirmed: true,
  801. capturedByGroup: true,
  802. group: {
  803. domainCaptureEnabled: true,
  804. managedUsersEnabled: true,
  805. ssoConfig: { enabled: true },
  806. },
  807. }
  808. fetchMock.get('express:/institutions/domains', [institution])
  809. })
  810. it('renders the domain-capture error', async function () {
  811. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  812. renderEmailsSection()
  813. const button = await screen.findByRole<HTMLButtonElement>('button', {
  814. name: 'Add another email',
  815. })
  816. await userEvent.click(button)
  817. const input = screen.getByRole('textbox', { name: 'Email' })
  818. fireEvent.change(input, {
  819. target: { value: 'user@autocomplete.edu' },
  820. })
  821. const notification = await screen.findByRole('alert')
  822. within(notification).getByText(
  823. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  824. { exact: false }
  825. )
  826. })
  827. })
  828. describe('and capturedByGroup is false with managedUsersEnabled (domainCaptureEnabled alone is not enough)', function () {
  829. beforeEach(async function () {
  830. await fetchMock.callHistory.flush(true)
  831. fetchMock.removeRoutes().clearHistory()
  832. const institution = {
  833. university: {
  834. id: 1234,
  835. ssoEnabled: false,
  836. name: 'Auto Complete University',
  837. },
  838. hostname: 'autocomplete.edu',
  839. confirmed: true,
  840. capturedByGroup: false,
  841. group: {
  842. domainCaptureEnabled: true,
  843. managedUsersEnabled: true,
  844. ssoConfig: { enabled: true },
  845. },
  846. }
  847. fetchMock.get('express:/institutions/domains', [institution])
  848. })
  849. it('does not render the domain-capture error and shows regular institution fields', async function () {
  850. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  851. renderEmailsSection()
  852. const button = await screen.findByRole<HTMLButtonElement>('button', {
  853. name: 'Add another email',
  854. })
  855. await userEvent.click(button)
  856. const input = screen.getByRole('textbox', { name: 'Email' })
  857. fireEvent.change(input, {
  858. target: { value: 'user@autocomplete.edu' },
  859. })
  860. await screen.findByRole('button', { name: 'Add new email' })
  861. expect(screen.queryByRole('alert')).to.be.null
  862. })
  863. })
  864. describe('and capturedByGroup is true with ssoConfig enabled (no managedUsers)', function () {
  865. beforeEach(async function () {
  866. await fetchMock.callHistory.flush(true)
  867. fetchMock.removeRoutes().clearHistory()
  868. const institution = {
  869. university: {
  870. id: 1234,
  871. ssoEnabled: false,
  872. name: 'Auto Complete University',
  873. },
  874. hostname: 'autocomplete.edu',
  875. confirmed: true,
  876. capturedByGroup: true,
  877. group: {
  878. domainCaptureEnabled: true,
  879. managedUsersEnabled: false,
  880. ssoConfig: { enabled: true },
  881. },
  882. }
  883. fetchMock.get('express:/institutions/domains', [institution])
  884. })
  885. it('renders the group SSO unavailable message', async function () {
  886. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  887. renderEmailsSection()
  888. const button = await screen.findByRole<HTMLButtonElement>('button', {
  889. name: 'Add another email',
  890. })
  891. await userEvent.click(button)
  892. const input = screen.getByRole('textbox', { name: 'Email' })
  893. fireEvent.change(input, {
  894. target: { value: 'user@autocomplete.edu' },
  895. })
  896. await screen.findByText('This feature is currently unavailable.')
  897. })
  898. })
  899. describe('and capturedByGroup is false with ssoConfig enabled', function () {
  900. beforeEach(async function () {
  901. await fetchMock.callHistory.flush(true)
  902. fetchMock.removeRoutes().clearHistory()
  903. const institution = {
  904. university: {
  905. id: 1234,
  906. ssoEnabled: false,
  907. name: 'Auto Complete University',
  908. },
  909. hostname: 'autocomplete.edu',
  910. confirmed: true,
  911. capturedByGroup: false,
  912. group: {
  913. domainCaptureEnabled: true,
  914. managedUsersEnabled: false,
  915. ssoConfig: { enabled: true },
  916. },
  917. }
  918. fetchMock.get('express:/institutions/domains', [institution])
  919. })
  920. it('does not render SSO UI and shows regular institution fields', async function () {
  921. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  922. renderEmailsSection()
  923. const button = await screen.findByRole<HTMLButtonElement>('button', {
  924. name: 'Add another email',
  925. })
  926. await userEvent.click(button)
  927. const input = screen.getByRole('textbox', { name: 'Email' })
  928. fireEvent.change(input, {
  929. target: { value: 'user@autocomplete.edu' },
  930. })
  931. await screen.findByRole('button', { name: 'Add new email' })
  932. expect(screen.queryByText('This feature is currently unavailable.'))
  933. .to.be.null
  934. })
  935. })
  936. })
  937. })
  938. })