unlink-commons-sso-modal.test.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { expect } from 'chai'
  2. import sinon from 'sinon'
  3. import {
  4. fireEvent,
  5. render,
  6. screen,
  7. waitFor,
  8. within,
  9. } from '@testing-library/react'
  10. import fetchMock from 'fetch-mock'
  11. import UnlinkCommonsSSOModal from '../../../../../../frontend/js/features/settings/components/emails/unlink-commons-sso-modal'
  12. import { location } from '../../../../../../frontend/js/shared/components/location'
  13. describe('<UnlinkCommonsSSOModal/>', function () {
  14. beforeEach(function () {
  15. fetchMock.removeRoutes().clearHistory()
  16. this.locationSandbox = sinon.createSandbox()
  17. this.locationStub = this.locationSandbox.stub(location)
  18. window.metaAttributesCache.set('ol-hasPassword', false)
  19. })
  20. afterEach(function () {
  21. fetchMock.removeRoutes().clearHistory()
  22. this.locationSandbox.restore()
  23. window.metaAttributesCache.delete('ol-hasPassword')
  24. })
  25. function renderModal(props = {}) {
  26. const onClose = sinon.stub()
  27. render(
  28. <UnlinkCommonsSSOModal
  29. show
  30. onClose={onClose}
  31. institutionName="Overleaf University"
  32. institutionEmail="user@overleaf.com"
  33. hasLicence
  34. {...props}
  35. />
  36. )
  37. return { onClose }
  38. }
  39. it('renders modal content including licence warning', function () {
  40. renderModal()
  41. screen.getByRole('dialog')
  42. screen.getByText('Unlink institutional login')
  43. screen.getByText(/remove your Overleaf University login/i)
  44. screen.getByText(/Removing and re-adding your institutional login/i)
  45. screen.getByText(/will lose your institutional premium features/i)
  46. screen.getByText(/can still access your work by setting up/i)
  47. })
  48. it('does not render licence warning when hasLicence is false', function () {
  49. renderModal({ hasLicence: false })
  50. expect(screen.queryByText(/will lose your institutional premium features/i))
  51. .to.be.null
  52. })
  53. it('does not render password reset hint when user already has a password', function () {
  54. window.metaAttributesCache.set('ol-hasPassword', true)
  55. renderModal()
  56. expect(screen.queryByText(/can still access your work by setting up/i)).to
  57. .be.null
  58. })
  59. it('posts to unlink endpoint with correct body, closes modal, and reloads page', async function () {
  60. const { onClose } = renderModal()
  61. fetchMock.post('/saml/unlink-commons', 204)
  62. const modal = screen.getByRole('dialog')
  63. fireEvent.click(within(modal).getByRole('button', { name: 'Unlink' }))
  64. await fetchMock.callHistory.flush(true)
  65. expect(fetchMock.callHistory.called('/saml/unlink-commons')).to.be.true
  66. const call = fetchMock.callHistory.calls('/saml/unlink-commons')[0]
  67. expect(JSON.parse(call.options!.body as string)).to.deep.equal({
  68. institutionEmail: 'user@overleaf.com',
  69. })
  70. await waitFor(() => {
  71. expect(onClose).to.have.been.calledOnce
  72. expect(this.locationStub.reload).to.have.been.calledOnce
  73. })
  74. })
  75. it('disables buttons and shows loading label while request is in flight', async function () {
  76. renderModal()
  77. // Use a deferred promise so the request stays pending
  78. let resolveRequest!: () => void
  79. fetchMock.post(
  80. '/saml/unlink-commons',
  81. new Promise<void>(resolve => {
  82. resolveRequest = resolve
  83. }).then(() => 204)
  84. )
  85. const modal = screen.getByRole('dialog')
  86. const unlinkButton = within(modal).getByRole('button', { name: 'Unlink' })
  87. const cancelButton = within(modal).getByRole('button', { name: 'Cancel' })
  88. fireEvent.click(unlinkButton)
  89. await screen.findByText('Unlinking')
  90. expect(unlinkButton).to.have.property('disabled', true)
  91. expect(cancelButton).to.have.property('disabled', true)
  92. resolveRequest()
  93. await fetchMock.callHistory.flush(true)
  94. })
  95. it('shows an error notification when unlink fails', async function () {
  96. renderModal()
  97. fetchMock.post('/saml/unlink-commons', 500)
  98. const modal = screen.getByRole('dialog')
  99. fireEvent.click(within(modal).getByRole('button', { name: 'Unlink' }))
  100. await screen.findByText('Unlinking failed. Please try again.')
  101. })
  102. it('clears error state when modal is closed via Cancel after a failure', async function () {
  103. const { onClose } = renderModal()
  104. fetchMock.post('/saml/unlink-commons', 500)
  105. const modal = screen.getByRole('dialog')
  106. fireEvent.click(within(modal).getByRole('button', { name: 'Unlink' }))
  107. await screen.findByText('Unlinking failed. Please try again.')
  108. // Cancel should call handleClose (which calls reset()) not just onClose
  109. fireEvent.click(within(modal).getByRole('button', { name: 'Cancel' }))
  110. expect(onClose).to.have.been.calledOnce
  111. })
  112. })