notifications.test.tsx 34 KB

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