notifications.test.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  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. const renderWithinProjectListProvider = (Component: React.ComponentType) => {
  33. render(<Component />, {
  34. wrapper: ({ children }) => (
  35. <ProjectListProvider>
  36. <ul className="list-unstyled">{children}</ul>
  37. </ProjectListProvider>
  38. ),
  39. })
  40. }
  41. describe('<UserNotifications />', function () {
  42. const exposedSettings = {
  43. samlInitPath: '/fakeSaml/',
  44. appName: 'Overleaf',
  45. }
  46. beforeEach(function () {
  47. window.metaAttributesCache = window.metaAttributesCache || new Map()
  48. fetchMock.reset()
  49. // at least one project is required to show some notifications
  50. const projects = [{}] as Project[]
  51. fetchMock.post(/\/api\/project/, {
  52. status: 200,
  53. body: {
  54. projects,
  55. totalSize: projects.length,
  56. },
  57. })
  58. })
  59. afterEach(function () {
  60. window.metaAttributesCache = new Map()
  61. fetchMock.reset()
  62. })
  63. describe('<Common>', function () {
  64. beforeEach(function () {
  65. window.metaAttributesCache = window.metaAttributesCache || new Map()
  66. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  67. })
  68. afterEach(function () {
  69. window.metaAttributesCache = new Map()
  70. fetchMock.reset()
  71. })
  72. it('accepts project invite', async function () {
  73. const reconfiguredNotification: DeepPartial<Notification> = {
  74. _id: 1,
  75. templateKey: 'notification_project_invite',
  76. }
  77. window.metaAttributesCache.set('ol-notifications', [
  78. merge(cloneDeep(notification), reconfiguredNotification),
  79. ])
  80. renderWithinProjectListProvider(Common)
  81. await fetchMock.flush(true)
  82. const deleteMock = fetchMock.delete(
  83. `/notifications/${reconfiguredNotification._id}`,
  84. 200
  85. )
  86. const acceptMock = fetchMock.post(
  87. `project/${notification.messageOpts.projectId}/invite/token/${notification.messageOpts.token}/accept`,
  88. 200
  89. )
  90. screen.getByRole('alert')
  91. screen.getByText(/would like you to join/i)
  92. const joinBtn = screen.getByRole('button', {
  93. name: /join project/i,
  94. }) as HTMLButtonElement
  95. expect(joinBtn.disabled).to.be.false
  96. fireEvent.click(joinBtn)
  97. expect(joinBtn.disabled).to.be.true
  98. await waitForElementToBeRemoved(() =>
  99. screen.getByRole('button', { name: /joining/i })
  100. )
  101. expect(acceptMock.called()).to.be.true
  102. screen.getByText(/joined/i)
  103. expect(screen.queryByRole('button', { name: /join project/i })).to.be.null
  104. const openProject = screen.getByRole('link', { name: /open project/i })
  105. expect(openProject.getAttribute('href')).to.equal(
  106. `/project/${notification.messageOpts.projectId}`
  107. )
  108. const closeBtn = screen.getByRole('button', { name: /close/i })
  109. fireEvent.click(closeBtn)
  110. expect(deleteMock.called()).to.be.true
  111. expect(screen.queryByRole('alert')).to.be.null
  112. })
  113. it('fails to accept project invite', async function () {
  114. const reconfiguredNotification: DeepPartial<Notification> = {
  115. _id: 1,
  116. templateKey: 'notification_project_invite',
  117. }
  118. window.metaAttributesCache.set('ol-notifications', [
  119. merge(cloneDeep(notification), reconfiguredNotification),
  120. ])
  121. renderWithinProjectListProvider(Common)
  122. await fetchMock.flush(true)
  123. fetchMock.post(
  124. `project/${notification.messageOpts.projectId}/invite/token/${notification.messageOpts.token}/accept`,
  125. 500
  126. )
  127. screen.getByRole('alert')
  128. screen.getByText(/would like you to join/i)
  129. const joinBtn = screen.getByRole('button', {
  130. name: /join project/i,
  131. }) as HTMLButtonElement
  132. fireEvent.click(joinBtn)
  133. await waitForElementToBeRemoved(() =>
  134. screen.getByRole('button', { name: /joining/i })
  135. )
  136. expect(fetchMock.called()).to.be.true
  137. screen.getByRole('button', { name: /join project/i })
  138. expect(screen.queryByRole('link', { name: /open project/i })).to.be.null
  139. })
  140. it('shows WFH2020', async function () {
  141. const reconfiguredNotification: DeepPartial<Notification> = {
  142. _id: 1,
  143. templateKey: 'wfh_2020_upgrade_offer',
  144. }
  145. window.metaAttributesCache.set('ol-notifications', [
  146. merge(cloneDeep(notification), reconfiguredNotification),
  147. ])
  148. renderWithinProjectListProvider(Common)
  149. await fetchMock.flush(true)
  150. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  151. screen.getByRole('alert')
  152. screen.getByText(/your free WFH2020 upgrade came to an end on/i)
  153. const viewLink = screen.getByRole('link', { name: /view/i })
  154. expect(viewLink.getAttribute('href')).to.equal(
  155. 'https://www.overleaf.com/events/wfh2020'
  156. )
  157. const closeBtn = screen.getByRole('button', { name: /close/i })
  158. fireEvent.click(closeBtn)
  159. expect(fetchMock.called()).to.be.true
  160. expect(screen.queryByRole('alert')).to.be.null
  161. })
  162. it('shows IP matched affiliation with SSO enabled', async function () {
  163. const reconfiguredNotification: DeepPartial<Notification> = {
  164. _id: 1,
  165. templateKey: 'notification_ip_matched_affiliation',
  166. messageOpts: { ssoEnabled: true },
  167. }
  168. window.metaAttributesCache.set('ol-notifications', [
  169. merge(cloneDeep(notification), reconfiguredNotification),
  170. ])
  171. renderWithinProjectListProvider(Common)
  172. await fetchMock.flush(true)
  173. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  174. screen.getByRole('alert')
  175. screen.getByText(/it looks like you’re at/i)
  176. screen.getByText(/you can now log in through your institution/i)
  177. screen.getByText(
  178. /link an institutional email address to your account to get started/i
  179. )
  180. const findOutMore = screen.getByRole('link', { name: /find out more/i })
  181. expect(findOutMore.getAttribute('href')).to.equal(
  182. 'https://www.overleaf.com/learn/how-to/Institutional_Login'
  183. )
  184. const linkAccount = screen.getByRole('link', { name: /link account/i })
  185. expect(linkAccount.getAttribute('href')).to.equal(
  186. `${exposedSettings.samlInitPath}?university_id=${notification.messageOpts.institutionId}&auto=/project`
  187. )
  188. const closeBtn = screen.getByRole('button', { name: /close/i })
  189. fireEvent.click(closeBtn)
  190. expect(fetchMock.called()).to.be.true
  191. expect(screen.queryByRole('alert')).to.be.null
  192. })
  193. it('shows IP matched affiliation with SSO disabled', async function () {
  194. const reconfiguredNotification: DeepPartial<Notification> = {
  195. _id: 1,
  196. templateKey: 'notification_ip_matched_affiliation',
  197. messageOpts: { ssoEnabled: false },
  198. }
  199. window.metaAttributesCache.set('ol-notifications', [
  200. merge(cloneDeep(notification), reconfiguredNotification),
  201. ])
  202. renderWithinProjectListProvider(Common)
  203. await fetchMock.flush(true)
  204. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  205. screen.getByRole('alert')
  206. screen.getByText(/it looks like you’re at/i)
  207. screen.getByText(/did you know that/i)
  208. screen.getByText(
  209. /add an institutional email address to claim your features/i
  210. )
  211. const addAffiliation = screen.getByRole('link', {
  212. name: /add affiliation/i,
  213. })
  214. expect(addAffiliation.getAttribute('href')).to.equal(`/user/settings`)
  215. const closeBtn = screen.getByRole('button', { name: /close/i })
  216. fireEvent.click(closeBtn)
  217. expect(fetchMock.called()).to.be.true
  218. expect(screen.queryByRole('alert')).to.be.null
  219. })
  220. it('shows tpds file limit', async function () {
  221. const reconfiguredNotification: DeepPartial<Notification> = {
  222. templateKey: 'notification_tpds_file_limit',
  223. }
  224. window.metaAttributesCache.set('ol-notifications', [
  225. merge(cloneDeep(notification), reconfiguredNotification),
  226. ])
  227. renderWithinProjectListProvider(Common)
  228. await fetchMock.flush(true)
  229. screen.getByRole('alert')
  230. screen.getByText(/file limit/i)
  231. screen.getByText(
  232. /please decrease the size of your project to prevent further errors/i
  233. )
  234. const accountSettings = screen.getByRole('link', {
  235. name: /account settings/i,
  236. })
  237. expect(accountSettings.getAttribute('href')).to.equal('/user/settings')
  238. const closeBtn = screen.getByRole('button', { name: /close/i })
  239. fireEvent.click(closeBtn)
  240. expect(screen.queryByRole('alert')).to.be.null
  241. })
  242. it('shows dropbox duplicate project names warning', async function () {
  243. const reconfiguredNotification: DeepPartial<Notification> = {
  244. _id: 1,
  245. templateKey: 'notification_dropbox_duplicate_project_names',
  246. }
  247. window.metaAttributesCache.set('ol-notifications', [
  248. merge(cloneDeep(notification), reconfiguredNotification),
  249. ])
  250. renderWithinProjectListProvider(Common)
  251. await fetchMock.flush(true)
  252. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  253. screen.getByRole('alert')
  254. screen.getByText(/you have more than one project called/i)
  255. screen.getByText(/make your project names unique/i)
  256. const learnMore = screen.getByRole('link', { name: /learn more/i })
  257. expect(learnMore.getAttribute('href')).to.equal(
  258. '/learn/how-to/Dropbox_Synchronization#Troubleshooting'
  259. )
  260. const closeBtn = screen.getByRole('button', { name: /close/i })
  261. fireEvent.click(closeBtn)
  262. expect(fetchMock.called()).to.be.true
  263. expect(screen.queryByRole('alert')).to.be.null
  264. })
  265. it('shows dropbox unlinked tue to lapsed reconfirmation', async function () {
  266. const reconfiguredNotification: DeepPartial<Notification> = {
  267. _id: 1,
  268. templateKey:
  269. 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation',
  270. }
  271. window.metaAttributesCache.set('ol-notifications', [
  272. merge(cloneDeep(notification), reconfiguredNotification),
  273. ])
  274. renderWithinProjectListProvider(Common)
  275. await fetchMock.flush(true)
  276. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  277. screen.getByRole('alert')
  278. screen.getByText(/your Dropbox account has been unlinked/i)
  279. screen.getByText(
  280. /confirm you are still at the institution and on their license, or upgrade your account in order to relink your dropbox account/i
  281. )
  282. const learnMore = screen.getByRole('link', { name: /learn more/i })
  283. expect(learnMore.getAttribute('href')).to.equal(
  284. '/learn/how-to/Institutional_Email_Reconfirmation'
  285. )
  286. const closeBtn = screen.getByRole('button', { name: /close/i })
  287. fireEvent.click(closeBtn)
  288. expect(fetchMock.called()).to.be.true
  289. expect(screen.queryByRole('alert')).to.be.null
  290. })
  291. it('shows non specific notification', async function () {
  292. const reconfiguredNotification: DeepPartial<Notification> = {
  293. _id: 1,
  294. html: 'unspecific message',
  295. }
  296. window.metaAttributesCache.set('ol-notifications', [
  297. merge(cloneDeep(notification), reconfiguredNotification),
  298. ])
  299. renderWithinProjectListProvider(Common)
  300. await fetchMock.flush(true)
  301. fetchMock.delete(`/notifications/${reconfiguredNotification._id}`, 200)
  302. screen.getByRole('alert')
  303. screen.getByText(reconfiguredNotification.html!)
  304. const closeBtn = screen.getByRole('button', { name: /close/i })
  305. fireEvent.click(closeBtn)
  306. expect(fetchMock.called()).to.be.true
  307. expect(screen.queryByRole('alert')).to.be.null
  308. })
  309. })
  310. describe('<Institution>', function () {
  311. beforeEach(function () {
  312. window.metaAttributesCache = window.metaAttributesCache || new Map()
  313. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  314. fetchMock.reset()
  315. })
  316. afterEach(function () {
  317. fetchMock.reset()
  318. })
  319. it('shows sso available', function () {
  320. const institution: DeepPartial<InstitutionType> = {
  321. templateKey: 'notification_institution_sso_available',
  322. }
  323. window.metaAttributesCache.set('ol-notificationsInstitution', [
  324. { ...notificationsInstitution, ...institution },
  325. ])
  326. render(<Institution />)
  327. screen.getByRole('alert')
  328. screen.getByText(/you can now link/i)
  329. screen.getByText(/doing this will allow you to log in/i)
  330. const learnMore = screen.getByRole('link', { name: /learn more/i })
  331. expect(learnMore.getAttribute('href')).to.equal(
  332. '/learn/how-to/Institutional_Login'
  333. )
  334. const action = screen.getByRole('link', { name: /link account/i })
  335. expect(action.getAttribute('href')).to.equal(
  336. `${exposedSettings.samlInitPath}?university_id=${notificationsInstitution.institutionId}&auto=/project&email=${notificationsInstitution.email}`
  337. )
  338. })
  339. it('shows sso linked', function () {
  340. const institution: DeepPartial<InstitutionType> = {
  341. _id: 1,
  342. templateKey: 'notification_institution_sso_linked',
  343. }
  344. window.metaAttributesCache.set('ol-notificationsInstitution', [
  345. { ...notificationsInstitution, ...institution },
  346. ])
  347. render(<Institution />)
  348. fetchMock.delete(`/notifications/${institution._id}`, 200)
  349. screen.getByRole('alert')
  350. screen.getByText(/has been linked to your/i)
  351. const closeBtn = screen.getByRole('button', { name: /close/i })
  352. fireEvent.click(closeBtn)
  353. expect(fetchMock.called()).to.be.true
  354. expect(screen.queryByRole('alert')).to.be.null
  355. })
  356. it('shows sso non canonical', function () {
  357. const institution: DeepPartial<InstitutionType> = {
  358. _id: 1,
  359. templateKey: 'notification_institution_sso_non_canonical',
  360. }
  361. window.metaAttributesCache.set('ol-notificationsInstitution', [
  362. { ...notificationsInstitution, ...institution },
  363. ])
  364. render(<Institution />)
  365. fetchMock.delete(`/notifications/${institution._id}`, 200)
  366. screen.getByRole('alert')
  367. screen.getByText(/you’ve tried to login with/i)
  368. screen.getByText(
  369. /in order to match your institutional metadata, your account is associated with/i
  370. )
  371. const closeBtn = screen.getByRole('button', { name: /close/i })
  372. fireEvent.click(closeBtn)
  373. expect(fetchMock.called()).to.be.true
  374. expect(screen.queryByRole('alert')).to.be.null
  375. })
  376. it('shows sso already registered', function () {
  377. const institution: DeepPartial<InstitutionType> = {
  378. _id: 1,
  379. templateKey: 'notification_institution_sso_already_registered',
  380. }
  381. window.metaAttributesCache.set('ol-notificationsInstitution', [
  382. { ...notificationsInstitution, ...institution },
  383. ])
  384. render(<Institution />)
  385. fetchMock.delete(`/notifications/${institution._id}`, 200)
  386. screen.getByRole('alert')
  387. screen.getByText(/which is already registered with/i)
  388. const action = screen.getByRole('link', { name: /find out more/i })
  389. expect(action.getAttribute('href')).to.equal(
  390. '/learn/how-to/Institutional_Login'
  391. )
  392. const closeBtn = screen.getByRole('button', { name: /close/i })
  393. fireEvent.click(closeBtn)
  394. expect(fetchMock.called()).to.be.true
  395. expect(screen.queryByRole('alert')).to.be.null
  396. })
  397. it('shows sso error', function () {
  398. const institution: DeepPartial<InstitutionType> = {
  399. templateKey: 'notification_institution_sso_error',
  400. error: {
  401. message: 'fake message',
  402. tryAgain: true,
  403. },
  404. }
  405. window.metaAttributesCache.set('ol-notificationsInstitution', [
  406. { ...notificationsInstitution, ...institution },
  407. ])
  408. render(<Institution />)
  409. screen.getByRole('alert')
  410. screen.getByText(/something went wrong/i)
  411. screen.getByText(institution.error!.message!)
  412. screen.getByText(/please try again/i)
  413. const closeBtn = screen.getByRole('button', { name: /close/i })
  414. fireEvent.click(closeBtn)
  415. expect(screen.queryByRole('alert')).to.be.null
  416. })
  417. })
  418. describe('<ConfirmEmail/>', function () {
  419. beforeEach(async function () {
  420. window.metaAttributesCache.set('ol-ExposedSettings', {
  421. emailConfirmationDisabled: false,
  422. })
  423. window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
  424. renderWithinProjectListProvider(ConfirmEmail)
  425. await fetchMock.flush(true)
  426. })
  427. afterEach(function () {
  428. fetchMock.reset()
  429. })
  430. it('sends successfully', async function () {
  431. fetchMock.post('/user/emails/resend_confirmation', 200)
  432. const resendButton = screen.getByRole('button', { name: /resend/i })
  433. fireEvent.click(resendButton)
  434. expect(screen.queryByRole('button', { name: /resend/i })).to.be.null
  435. await waitForElementToBeRemoved(() =>
  436. screen.getByText(/resending confirmation email/i)
  437. )
  438. expect(fetchMock.called()).to.be.true
  439. expect(screen.queryByRole('alert')).to.be.null
  440. })
  441. it('fails to send', async function () {
  442. fetchMock.post('/user/emails/resend_confirmation', 500)
  443. const resendButton = screen.getByRole('button', { name: /resend/i })
  444. fireEvent.click(resendButton)
  445. await waitForElementToBeRemoved(() =>
  446. screen.getByText(/resending confirmation email/i)
  447. )
  448. expect(fetchMock.called()).to.be.true
  449. screen.getByText(/something went wrong/i)
  450. })
  451. })
  452. describe('<Affiliation/>', function () {
  453. const locationStub = sinon.stub()
  454. const originalLocation = window.location
  455. beforeEach(function () {
  456. window.metaAttributesCache = window.metaAttributesCache || new Map()
  457. window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings)
  458. Object.defineProperty(window, 'location', {
  459. value: { assign: locationStub },
  460. })
  461. fetchMock.reset()
  462. })
  463. afterEach(function () {
  464. window.metaAttributesCache = new Map()
  465. Object.defineProperty(window, 'location', {
  466. value: originalLocation,
  467. })
  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(locationStub)
  518. sinon.assert.calledWithMatch(
  519. locationStub,
  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. })
  552. afterEach(function () {
  553. fetchMock.reset()
  554. window.metaAttributesCache = window.metaAttributesCache || new Map()
  555. })
  556. it('does not show the banner for users that are in group or are affiliated or assigned in the `default` variant', async function () {
  557. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', false)
  558. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  559. await fetchMock.flush(true)
  560. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
  561. })
  562. it('does not show the banner for users that have already dismissed it', async function () {
  563. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  564. localStorage.setItem('has_dismissed_groups_and_enterprise_banner', true)
  565. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  566. await fetchMock.flush(true)
  567. expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
  568. })
  569. describe('users that are not in group and are not affiliated', function () {
  570. beforeEach(function () {
  571. localStorage.clear()
  572. fetchMock.reset()
  573. // at least one project is required to show some notifications
  574. const projects = [{}] as Project[]
  575. fetchMock.post(/\/api\/project/, {
  576. status: 200,
  577. body: {
  578. projects,
  579. totalSize: projects.length,
  580. },
  581. })
  582. window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
  583. })
  584. afterEach(function () {
  585. fetchMock.reset()
  586. window.metaAttributesCache = window.metaAttributesCache || new Map()
  587. })
  588. after(function () {
  589. localStorage.clear()
  590. })
  591. it('will show the correct text for the `save` split test variant', async function () {
  592. window.metaAttributesCache.set(
  593. 'ol-groupsAndEnterpriseBannerVariant',
  594. 'save'
  595. )
  596. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  597. await fetchMock.flush(true)
  598. screen.getByText(
  599. /Groups, Companies and Research Organizations can save money with our Group and Enterprise plans — request information or a quote./
  600. )
  601. const link = screen.getByRole('link', { name: 'Contact Sales' })
  602. expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
  603. })
  604. it('will show the correct text for the `did-you-know` split test variant', async function () {
  605. window.metaAttributesCache.set(
  606. 'ol-groupsAndEnterpriseBannerVariant',
  607. 'did-you-know'
  608. )
  609. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  610. await fetchMock.flush(true)
  611. screen.getByText(
  612. 'Did you know that Overleaf offers group and organization-wide subscription options? Request information or a quote.'
  613. )
  614. const link = screen.getByRole('link', { name: 'Contact Sales' })
  615. expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
  616. })
  617. it('will show the correct text for the `empower` split test variant', async function () {
  618. window.metaAttributesCache.set(
  619. 'ol-groupsAndEnterpriseBannerVariant',
  620. 'empower'
  621. )
  622. renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
  623. await fetchMock.flush(true)
  624. screen.getByText(
  625. 'Empower your organization to work in Overleaf! Get a group or organizational plan.'
  626. )
  627. const link = screen.getByRole('link', { name: 'Contact Sales' })
  628. expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
  629. })
  630. })
  631. })
  632. })