notifications.test.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. import { expect } from 'chai'
  2. import sinon from 'sinon'
  3. import {
  4. fireEvent,
  5. render,
  6. screen,
  7. waitForElementToBeRemoved,
  8. within,
  9. } from '@testing-library/react'
  10. import fetchMock from 'fetch-mock'
  11. import { merge, cloneDeep } from 'lodash'
  12. import {
  13. professionalUserData,
  14. unconfirmedUserData,
  15. unconfirmedCommonsUserData,
  16. } from '../../settings/fixtures/test-user-email-data'
  17. import {
  18. notification,
  19. notificationsInstitution,
  20. } from '../fixtures/notifications-data'
  21. import Common from '../../../../../frontend/js/features/project-list/components/notifications/groups/common'
  22. import Institution from '../../../../../frontend/js/features/project-list/components/notifications/groups/institution'
  23. import ConfirmEmail from '../../../../../frontend/js/features/project-list/components/notifications/groups/confirm-email'
  24. import ReconfirmationInfo from '../../../../../frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info'
  25. import { ProjectListProvider } from '../../../../../frontend/js/features/project-list/context/project-list-context'
  26. import {
  27. Notification,
  28. Institution as InstitutionType,
  29. } from '../../../../../types/project/dashboard/notification'
  30. import { DeepPartial } from '../../../../../types/utils'
  31. import { Project } from '../../../../../types/project/dashboard/api'
  32. import GroupsAndEnterpriseBanner from '../../../../../frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner'
  33. import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
  34. import * as useLocationModule from '../../../../../frontend/js/shared/hooks/use-location'
  35. const renderWithinProjectListProvider = (Component: React.ComponentType) => {
  36. render(<Component />, {
  37. wrapper: ({ children }) => (
  38. <ProjectListProvider>
  39. <ul className="list-unstyled">{children}</ul>
  40. </ProjectListProvider>
  41. ),
  42. })
  43. }
  44. describe('<UserNotifications />', function () {
  45. const exposedSettings = {
  46. samlInitPath: '/fakeSaml/',
  47. appName: 'Overleaf',
  48. }
  49. beforeEach(function () {
  50. window.metaAttributesCache = window.metaAttributesCache || new Map()
  51. fetchMock.reset()
  52. // at least one project is required to show some notifications
  53. const projects = [{}] as Project[]
  54. fetchMock.post(/\/api\/project/, {
  55. status: 200,
  56. body: {
  57. projects,
  58. totalSize: projects.length,
  59. },
  60. })
  61. })
  62. afterEach(function () {
  63. window.metaAttributesCache = new Map()
  64. fetchMock.reset()
  65. })
  66. describe('<Common>', function () {
  67. beforeEach(function () {
  68. window.metaAttributesCache = window.metaAttributesCache || new Map()
  69. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  70. })
  71. afterEach(function () {
  72. window.metaAttributesCache = new Map()
  73. fetchMock.reset()
  74. })
  75. it('accepts project invite', async function () {
  76. const reconfiguredNotification: DeepPartial<Notification> = {
  77. _id: 1,
  78. templateKey: 'notification_project_invite',
  79. }
  80. window.metaAttributesCache.set('ol-notifications', [
  81. merge(cloneDeep(notification), reconfiguredNotification),
  82. ])
  83. renderWithinProjectListProvider(Common)
  84. await fetchMock.flush(true)
  85. const deleteMock = fetchMock.delete(
  86. `/notifications/${reconfiguredNotification._id}`,
  87. 200
  88. )
  89. const acceptMock = fetchMock.post(
  90. `project/${notification.messageOpts.projectId}/invite/token/${notification.messageOpts.token}/accept`,
  91. 200
  92. )
  93. screen.getByRole('alert')
  94. screen.getByText(/would like you to join/i)
  95. const joinBtn = screen.getByRole('button', {
  96. name: /join project/i,
  97. }) as HTMLButtonElement
  98. expect(joinBtn.disabled).to.be.false
  99. fireEvent.click(joinBtn)
  100. expect(joinBtn.disabled).to.be.true
  101. await waitForElementToBeRemoved(() =>
  102. screen.getByRole('button', { name: /joining/i })
  103. )
  104. expect(acceptMock.called()).to.be.true
  105. screen.getByText(/joined/i)
  106. expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
  107. const openProject = screen.getByRole('link', { name: /open project/i })
  108. expect(openProject.getAttribute('href')).to.equal(
  109. `/project/${notification.messageOpts.projectId}`
  110. )
  111. const closeBtn = screen.getByRole('button', { name: /close/i })
  112. fireEvent.click(closeBtn)
  113. expect(deleteMock.called()).to.be.true
  114. expect(screen.queryByRole('alert')).to.be.null
  115. })
  116. it('fails to accept project invite', async function () {
  117. const reconfiguredNotification: DeepPartial<Notification> = {
  118. _id: 1,
  119. templateKey: 'notification_project_invite',
  120. }
  121. window.metaAttributesCache.set('ol-notifications', [
  122. merge(cloneDeep(notification), reconfiguredNotification),
  123. ])
  124. renderWithinProjectListProvider(Common)
  125. await fetchMock.flush(true)
  126. fetchMock.post(
  127. `project/${notification.messageOpts.projectId}/invite/token/${notification.messageOpts.token}/accept`,
  128. 500
  129. )
  130. screen.getByRole('alert')
  131. screen.getByText(/would like you to join/i)
  132. const joinBtn = screen.getByRole('button', {
  133. name: /join project/i,
  134. }) as HTMLButtonElement
  135. fireEvent.click(joinBtn)
  136. await waitForElementToBeRemoved(() =>
  137. screen.getByRole('button', { name: /joining/i })
  138. )
  139. expect(fetchMock.called()).to.be.true
  140. screen.getByRole('button', { name: /join project/i })
  141. expect(screen.queryByRole('link', { name: /open project/i })).to.be.null
  142. })
  143. it('shows WFH2020', async function () {
  144. const reconfiguredNotification: DeepPartial<Notification> = {
  145. _id: 1,
  146. templateKey: 'wfh_2020_upgrade_offer',
  147. }
  148. window.metaAttributesCache.set('ol-notifications', [
  149. merge(cloneDeep(notification), reconfiguredNotification),
  150. ])
  151. renderWithinProjectListProvider(Common)
  152. await fetchMock.flush(true)
  153. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  154. screen.getByRole('alert')
  155. screen.getByText(/your free WFH2020 upgrade came to an end on/i)
  156. const viewLink = screen.getByRole('link', { name: /view/i })
  157. expect(viewLink.getAttribute('href')).to.equal(
  158. 'https://www.overleaf.com/events/wfh2020'
  159. )
  160. const closeBtn = screen.getByRole('button', { name: /close/i })
  161. fireEvent.click(closeBtn)
  162. expect(fetchMock.called()).to.be.true
  163. expect(screen.queryByRole('alert')).to.be.null
  164. })
  165. it('shows IP matched affiliation with SSO enabled', async function () {
  166. const reconfiguredNotification: DeepPartial<Notification> = {
  167. _id: 1,
  168. templateKey: 'notification_ip_matched_affiliation',
  169. messageOpts: { ssoEnabled: true },
  170. }
  171. window.metaAttributesCache.set('ol-notifications', [
  172. merge(cloneDeep(notification), reconfiguredNotification),
  173. ])
  174. renderWithinProjectListProvider(Common)
  175. await fetchMock.flush(true)
  176. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  177. screen.getByRole('alert')
  178. screen.getByText(/it looks like you’re at/i)
  179. screen.getByText(/you can now log in through your institution/i)
  180. screen.getByText(
  181. /link an institutional email address to your account to get started/i
  182. )
  183. const findOutMore = screen.getByRole('link', { name: /find out more/i })
  184. expect(findOutMore.getAttribute('href')).to.equal(
  185. 'https://www.overleaf.com/learn/how-to/Institutional_Login'
  186. )
  187. const linkAccount = screen.getByRole('link', { name: /link account/i })
  188. expect(linkAccount.getAttribute('href')).to.equal(
  189. `${exposedSettings.samlInitPath}?university_id=${notification.messageOpts.institutionId}&auto=/project`
  190. )
  191. const closeBtn = screen.getByRole('button', { name: /close/i })
  192. fireEvent.click(closeBtn)
  193. expect(fetchMock.called()).to.be.true
  194. expect(screen.queryByRole('alert')).to.be.null
  195. })
  196. it('shows IP matched affiliation with SSO disabled', async function () {
  197. const reconfiguredNotification: DeepPartial<Notification> = {
  198. _id: 1,
  199. templateKey: 'notification_ip_matched_affiliation',
  200. messageOpts: { ssoEnabled: false },
  201. }
  202. window.metaAttributesCache.set('ol-notifications', [
  203. merge(cloneDeep(notification), reconfiguredNotification),
  204. ])
  205. renderWithinProjectListProvider(Common)
  206. await fetchMock.flush(true)
  207. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  208. screen.getByRole('alert')
  209. screen.getByText(/it looks like you’re at/i)
  210. screen.getByText(/did you know that/i)
  211. screen.getByText(
  212. /add an institutional email address to claim your features/i
  213. )
  214. const addAffiliation = screen.getByRole('link', {
  215. name: /add affiliation/i,
  216. })
  217. expect(addAffiliation.getAttribute('href')).to.equal(`/user/settings`)
  218. const closeBtn = screen.getByRole('button', { name: /close/i })
  219. fireEvent.click(closeBtn)
  220. expect(fetchMock.called()).to.be.true
  221. expect(screen.queryByRole('alert')).to.be.null
  222. })
  223. it('shows tpds file limit', async function () {
  224. const reconfiguredNotification: DeepPartial<Notification> = {
  225. templateKey: 'notification_tpds_file_limit',
  226. }
  227. window.metaAttributesCache.set('ol-notifications', [
  228. merge(cloneDeep(notification), reconfiguredNotification),
  229. ])
  230. renderWithinProjectListProvider(Common)
  231. await fetchMock.flush(true)
  232. screen.getByRole('alert')
  233. screen.getByText(/file limit/i)
  234. screen.getByText(
  235. /please decrease the size of your project to prevent further errors/i
  236. )
  237. const accountSettings = screen.getByRole('link', {
  238. name: /account settings/i,
  239. })
  240. expect(accountSettings.getAttribute('href')).to.equal('/user/settings')
  241. const closeBtn = screen.getByRole('button', { name: /close/i })
  242. fireEvent.click(closeBtn)
  243. expect(screen.queryByRole('alert')).to.be.null
  244. })
  245. it('shows dropbox duplicate project names warning', async function () {
  246. const reconfiguredNotification: DeepPartial<Notification> = {
  247. _id: 1,
  248. templateKey: 'notification_dropbox_duplicate_project_names',
  249. }
  250. window.metaAttributesCache.set('ol-notifications', [
  251. merge(cloneDeep(notification), reconfiguredNotification),
  252. ])
  253. renderWithinProjectListProvider(Common)
  254. await fetchMock.flush(true)
  255. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  256. screen.getByRole('alert')
  257. screen.getByText(/you have more than one project called/i)
  258. screen.getByText(/make your project names unique/i)
  259. const learnMore = screen.getByRole('link', { name: /learn more/i })
  260. expect(learnMore.getAttribute('href')).to.equal(
  261. '/learn/how-to/Dropbox_Synchronization#Troubleshooting'
  262. )
  263. const closeBtn = screen.getByRole('button', { name: /close/i })
  264. fireEvent.click(closeBtn)
  265. expect(fetchMock.called()).to.be.true
  266. expect(screen.queryByRole('alert')).to.be.null
  267. })
  268. it('shows dropbox unlinked tue to lapsed reconfirmation', async function () {
  269. const reconfiguredNotification: DeepPartial<Notification> = {
  270. _id: 1,
  271. templateKey:
  272. 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation',
  273. }
  274. window.metaAttributesCache.set('ol-notifications', [
  275. merge(cloneDeep(notification), reconfiguredNotification),
  276. ])
  277. renderWithinProjectListProvider(Common)
  278. await fetchMock.flush(true)
  279. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  280. screen.getByRole('alert')
  281. screen.getByText(/your Dropbox account has been unlinked/i)
  282. screen.getByText(
  283. /confirm you are still at the institution and on their license, or upgrade your account in order to relink your dropbox account/i
  284. )
  285. const learnMore = screen.getByRole('link', { name: /learn more/i })
  286. expect(learnMore.getAttribute('href')).to.equal(
  287. '/learn/how-to/Institutional_Email_Reconfirmation'
  288. )
  289. const closeBtn = screen.getByRole('button', { name: /close/i })
  290. fireEvent.click(closeBtn)
  291. expect(fetchMock.called()).to.be.true
  292. expect(screen.queryByRole('alert')).to.be.null
  293. })
  294. it('shows non specific notification', async function () {
  295. const reconfiguredNotification: DeepPartial<Notification> = {
  296. _id: 1,
  297. html: 'unspecific message',
  298. }
  299. window.metaAttributesCache.set('ol-notifications', [
  300. merge(cloneDeep(notification), reconfiguredNotification),
  301. ])
  302. renderWithinProjectListProvider(Common)
  303. await fetchMock.flush(true)
  304. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  305. screen.getByRole('alert')
  306. screen.getByText(reconfiguredNotification.html!)
  307. const closeBtn = screen.getByRole('button', { name: /close/i })
  308. fireEvent.click(closeBtn)
  309. expect(fetchMock.called()).to.be.true
  310. expect(screen.queryByRole('alert')).to.be.null
  311. })
  312. })
  313. describe('<Institution>', function () {
  314. beforeEach(function () {
  315. window.metaAttributesCache = window.metaAttributesCache || new Map()
  316. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  317. fetchMock.reset()
  318. })
  319. afterEach(function () {
  320. fetchMock.reset()
  321. })
  322. it('shows sso available', function () {
  323. const institution: DeepPartial<InstitutionType> = {
  324. templateKey: 'notification_institution_sso_available',
  325. }
  326. window.metaAttributesCache.set('ol-notificationsInstitution', [
  327. { ...notificationsInstitution, ...institution },
  328. ])
  329. render(<Institution />)
  330. screen.getByRole('alert')
  331. screen.getByText(/you can now link/i)
  332. screen.getByText(/doing this will allow you to log in/i)
  333. const learnMore = screen.getByRole('link', { name: /learn more/i })
  334. expect(learnMore.getAttribute('href')).to.equal(
  335. '/learn/how-to/Institutional_Login'
  336. )
  337. const action = screen.getByRole('link', { name: /link account/i })
  338. expect(action.getAttribute('href')).to.equal(
  339. `${exposedSettings.samlInitPath}?university_id=${notificationsInstitution.institutionId}&auto=/project&email=${notificationsInstitution.email}`
  340. )
  341. })
  342. it('shows sso linked', function () {
  343. const institution: DeepPartial<InstitutionType> = {
  344. _id: 1,
  345. templateKey: 'notification_institution_sso_linked',
  346. }
  347. window.metaAttributesCache.set('ol-notificationsInstitution', [
  348. { ...notificationsInstitution, ...institution },
  349. ])
  350. render(<Institution />)
  351. fetchMock.delete(`/notifications/${institution._id}`, 200)
  352. screen.getByRole('alert')
  353. screen.getByText(/has been linked to your/i)
  354. const closeBtn = screen.getByRole('button', { name: /close/i })
  355. fireEvent.click(closeBtn)
  356. expect(fetchMock.called()).to.be.true
  357. expect(screen.queryByRole('alert')).to.be.null
  358. })
  359. it('shows sso non canonical', function () {
  360. const institution: DeepPartial<InstitutionType> = {
  361. _id: 1,
  362. templateKey: 'notification_institution_sso_non_canonical',
  363. }
  364. window.metaAttributesCache.set('ol-notificationsInstitution', [
  365. { ...notificationsInstitution, ...institution },
  366. ])
  367. render(<Institution />)
  368. fetchMock.delete(`/notifications/${institution._id}`, 200)
  369. screen.getByRole('alert')
  370. screen.getByText(/you’ve tried to log in with/i)
  371. screen.getByText(
  372. /in order to match your institutional metadata, your account is associated with/i
  373. )
  374. const closeBtn = screen.getByRole('button', { name: /close/i })
  375. fireEvent.click(closeBtn)
  376. expect(fetchMock.called()).to.be.true
  377. expect(screen.queryByRole('alert')).to.be.null
  378. })
  379. it('shows sso already registered', function () {
  380. const institution: DeepPartial<InstitutionType> = {
  381. _id: 1,
  382. templateKey: 'notification_institution_sso_already_registered',
  383. }
  384. window.metaAttributesCache.set('ol-notificationsInstitution', [
  385. { ...notificationsInstitution, ...institution },
  386. ])
  387. render(<Institution />)
  388. fetchMock.delete(`/notifications/${institution._id}`, 200)
  389. screen.getByRole('alert')
  390. screen.getByText(/which is already registered with/i)
  391. const action = screen.getByRole('link', { name: /find out more/i })
  392. expect(action.getAttribute('href')).to.equal(
  393. '/learn/how-to/Institutional_Login'
  394. )
  395. const closeBtn = screen.getByRole('button', { name: /close/i })
  396. fireEvent.click(closeBtn)
  397. expect(fetchMock.called()).to.be.true
  398. expect(screen.queryByRole('alert')).to.be.null
  399. })
  400. it('shows sso error', function () {
  401. const institution: DeepPartial<InstitutionType> = {
  402. templateKey: 'notification_institution_sso_error',
  403. error: {
  404. message: 'fake message',
  405. tryAgain: true,
  406. },
  407. }
  408. window.metaAttributesCache.set('ol-notificationsInstitution', [
  409. { ...notificationsInstitution, ...institution },
  410. ])
  411. render(<Institution />)
  412. screen.getByRole('alert')
  413. screen.getByText(/something went wrong/i)
  414. screen.getByText(institution.error!.message!)
  415. screen.getByText(/please try again/i)
  416. const closeBtn = screen.getByRole('button', { name: /close/i })
  417. fireEvent.click(closeBtn)
  418. expect(screen.queryByRole('alert')).to.be.null
  419. })
  420. })
  421. describe('<ConfirmEmail/>', function () {
  422. beforeEach(async function () {
  423. window.metaAttributesCache.set('ol-ExposedSettings', {
  424. emailConfirmationDisabled: false,
  425. })
  426. window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
  427. })
  428. afterEach(function () {
  429. fetchMock.reset()
  430. })
  431. it('sends successfully', async function () {
  432. renderWithinProjectListProvider(ConfirmEmail)
  433. await fetchMock.flush(true)
  434. fetchMock.post('/user/emails/resend_confirmation', 200)
  435. const email = unconfirmedUserData.email
  436. const notificationBody = screen.getByTestId('pro-notification-body')
  437. expect(notificationBody.textContent).to.contain(
  438. `Please confirm your email ${email} by clicking on the link in the confirmation email`
  439. )
  440. const resendButton = screen.getByRole('button', { name: /resend/i })
  441. fireEvent.click(resendButton)
  442. expect(screen.queryByRole('button', { name: /resend/i })).to.be.null
  443. await waitForElementToBeRemoved(() =>
  444. screen.getByText(/resending confirmation email/i)
  445. )
  446. expect(fetchMock.called()).to.be.true
  447. expect(screen.queryByRole('alert')).to.be.null
  448. })
  449. it('fails to send', async function () {
  450. renderWithinProjectListProvider(ConfirmEmail)
  451. await fetchMock.flush(true)
  452. fetchMock.post('/user/emails/resend_confirmation', 500)
  453. const resendButton = screen.getByRole('button', { name: /resend/i })
  454. fireEvent.click(resendButton)
  455. await waitForElementToBeRemoved(() =>
  456. screen.getByText(/resending confirmation email/i)
  457. )
  458. expect(fetchMock.called()).to.be.true
  459. screen.getByText(/something went wrong/i)
  460. })
  461. it('shows notification for commons account', async function () {
  462. window.metaAttributesCache.set('ol-userEmails', [
  463. unconfirmedCommonsUserData,
  464. ])
  465. renderWithinProjectListProvider(ConfirmEmail)
  466. await fetchMock.flush(true)
  467. const alert = screen.getByRole('alert')
  468. const email = unconfirmedCommonsUserData.email
  469. const notificationBody = within(alert).getByTestId('notification-body')
  470. expect(notificationBody.textContent).to.contain(
  471. 'You are one step away from accessing Overleaf Professional features'
  472. )
  473. expect(notificationBody.textContent).to.contain(
  474. `Overleaf has an Overleaf subscription. Click the confirmation link sent to ${email} to upgrade to Overleaf Professional`
  475. )
  476. })
  477. })
  478. describe('<Affiliation/>', function () {
  479. let assignStub: sinon.SinonStub
  480. beforeEach(function () {
  481. window.metaAttributesCache = window.metaAttributesCache || new Map()
  482. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  483. assignStub = sinon.stub()
  484. this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
  485. assign: assignStub,
  486. reload: sinon.stub(),
  487. })
  488. fetchMock.reset()
  489. })
  490. afterEach(function () {
  491. window.metaAttributesCache = new Map()
  492. this.locationStub.restore()
  493. fetchMock.reset()
  494. })
  495. it('shows reconfirm message with SSO disabled', async function () {
  496. window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
  497. { ...professionalUserData, samlProviderId: 'Saml Provider' },
  498. ])
  499. render(<ReconfirmationInfo />)
  500. screen.getByRole('alert')
  501. screen.getByText(
  502. /take a moment to confirm your institutional email address/i
  503. )
  504. const removeLink = screen.getByRole('link', { name: /remove it/i })
  505. expect(removeLink.getAttribute('href')).to.equal(
  506. `/user/settings?remove=${professionalUserData.email}`
  507. )
  508. const learnMore = screen.getByRole('link', { name: /learn more/i })
  509. expect(learnMore.getAttribute('href')).to.equal(
  510. '/learn/how-to/Institutional_Email_Reconfirmation'
  511. )
  512. const sendReconfirmationMock = fetchMock.post(
  513. '/user/emails/send-reconfirmation',
  514. 200
  515. )
  516. fireEvent.click(
  517. screen.getByRole('button', { name: /confirm affiliation/i })
  518. )
  519. await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
  520. screen.getByText(/check your email inbox to confirm/i)
  521. expect(screen.queryByRole('button', { name: /confirm affiliation/i })).to
  522. .be.null
  523. expect(screen.queryByRole('link', { name: /remove it/i })).to.be.null
  524. expect(screen.queryByRole('link', { name: /learn more/i })).to.be.null
  525. expect(sendReconfirmationMock.called()).to.be.true
  526. fireEvent.click(
  527. screen.getByRole('button', { name: /resend confirmation email/i })
  528. )
  529. await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
  530. expect(sendReconfirmationMock.calls()).to.have.lengthOf(2)
  531. })
  532. it('shows reconfirm message with SSO enabled', async function () {
  533. window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
  534. merge(cloneDeep(professionalUserData), {
  535. affiliation: { institution: { ssoEnabled: true } },
  536. }),
  537. ])
  538. render(<ReconfirmationInfo />)
  539. fireEvent.click(
  540. screen.getByRole('button', { name: /confirm affiliation/i })
  541. )
  542. sinon.assert.calledOnce(assignStub)
  543. sinon.assert.calledWithMatch(
  544. assignStub,
  545. `${exposedSettings.samlInitPath}?university_id=${professionalUserData.affiliation.institution.id}&reconfirm=/project`
  546. )
  547. })
  548. it('shows success reconfirmation message', function () {
  549. window.metaAttributesCache.set('ol-userEmails', [
  550. { ...professionalUserData, samlProviderId: 'Saml Provider' },
  551. ])
  552. window.metaAttributesCache.set('ol-reconfirmedViaSAML', 'Saml Provider')
  553. const { rerender } = render(<ReconfirmationInfo />)
  554. screen.getByRole('alert')
  555. screen.getByText(professionalUserData.affiliation!.institution!.name!)
  556. screen.getByText(/affiliation is confirmed/i)
  557. window.metaAttributesCache.set('ol-reconfirmedViaSAML', '')
  558. rerender(<ReconfirmationInfo />)
  559. expect(screen.queryByRole('alert')).to.be.null
  560. })
  561. })
  562. describe('<GroupsAndEnterpriseBanner />', function () {
  563. beforeEach(function () {
  564. window.metaAttributesCache = window.metaAttributesCache || new Map()
  565. localStorage.clear()
  566. fetchMock.reset()
  567. // at least one project is required to show some notifications
  568. const projects = [{}] as Project[]
  569. fetchMock.post(/\/api\/project/, {
  570. status: 200,
  571. body: {
  572. projects,
  573. totalSize: projects.length,
  574. },
  575. })
  576. window.metaAttributesCache.set(
  577. 'ol-groupsAndEnterpriseBannerVariant',
  578. 'did-you-know'
  579. )
  580. })
  581. afterEach(function () {
  582. fetchMock.reset()
  583. window.metaAttributesCache = window.metaAttributesCache || new Map()
  584. })
  585. it('does not show the banner for users that are in group or are affiliated', async function () {
  586. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', false)
  587. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  588. await fetchMock.flush(true)
  589. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
  590. })
  591. it('shows the banner for users that have dismissed the previous banners', async function () {
  592. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  593. localStorage.setItem('has_dismissed_groups_and_enterprise_banner', true)
  594. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  595. await fetchMock.flush(true)
  596. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
  597. .null
  598. })
  599. it('shows the banner for users that have dismissed the banner more than 30 days ago', async function () {
  600. const dismissed = new Date()
  601. dismissed.setDate(dismissed.getDate() - 31) // 31 days
  602. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  603. localStorage.setItem(
  604. 'has_dismissed_groups_and_enterprise_banner',
  605. dismissed
  606. )
  607. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  608. await fetchMock.flush(true)
  609. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
  610. .null
  611. })
  612. it('does not show the banner for users that have dismissed the banner within the last 30 days', async function () {
  613. const dismissed = new Date()
  614. dismissed.setDate(dismissed.getDate() - 29) // 29 days
  615. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  616. localStorage.setItem(
  617. 'has_dismissed_groups_and_enterprise_banner',
  618. dismissed
  619. )
  620. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  621. await fetchMock.flush(true)
  622. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
  623. })
  624. describe('users that are not in group and are not affiliated', function () {
  625. beforeEach(function () {
  626. localStorage.clear()
  627. fetchMock.reset()
  628. // at least one project is required to show some notifications
  629. const projects = [{}] as Project[]
  630. fetchMock.post(/\/api\/project/, {
  631. status: 200,
  632. body: {
  633. projects,
  634. totalSize: projects.length,
  635. },
  636. })
  637. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  638. })
  639. afterEach(function () {
  640. fetchMock.reset()
  641. window.metaAttributesCache = window.metaAttributesCache || new Map()
  642. })
  643. after(function () {
  644. localStorage.clear()
  645. })
  646. it('will show the correct text for the `did-you-know` variant', async function () {
  647. window.metaAttributesCache.set(
  648. 'ol-groupsAndEnterpriseBannerVariant',
  649. 'did-you-know'
  650. )
  651. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  652. await fetchMock.flush(true)
  653. screen.getByText(
  654. 'Did you know that Overleaf offers group and organization-wide subscription options? Request information or a quote.'
  655. )
  656. const link = screen.getByRole('link', { name: 'Contact Sales' })
  657. expect(link.getAttribute('href')).to.equal(`/for/contact-sales-1`)
  658. })
  659. it('will show the correct text for the `on-premise` variant', async function () {
  660. window.metaAttributesCache.set(
  661. 'ol-groupsAndEnterpriseBannerVariant',
  662. 'on-premise'
  663. )
  664. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  665. await fetchMock.flush(true)
  666. screen.getByText(
  667. 'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
  668. )
  669. const link = screen.getByRole('link', { name: 'Contact Sales' })
  670. expect(link.getAttribute('href')).to.equal(`/for/contact-sales-2`)
  671. })
  672. it('will show the correct text for the `people` variant', async function () {
  673. window.metaAttributesCache.set(
  674. 'ol-groupsAndEnterpriseBannerVariant',
  675. 'people'
  676. )
  677. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  678. await fetchMock.flush(true)
  679. screen.getByText(
  680. 'Other people at your company may already be using Overleaf. Save money with Overleaf group and company-wide subscriptions. Request more information.'
  681. )
  682. const link = screen.getByRole('link', { name: 'Contact Sales' })
  683. expect(link.getAttribute('href')).to.equal(`/for/contact-sales-3`)
  684. })
  685. it('will show the correct text for the `FOMO` variant', async function () {
  686. window.metaAttributesCache.set(
  687. 'ol-groupsAndEnterpriseBannerVariant',
  688. 'FOMO'
  689. )
  690. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  691. await fetchMock.flush(true)
  692. screen.getByText(
  693. 'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
  694. )
  695. const link = screen.getByRole('link', { name: 'Contact Sales' })
  696. expect(link.getAttribute('href')).to.equal(`/for/contact-sales-4`)
  697. })
  698. })
  699. })
  700. })