notifications.test.tsx 28 KB

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