Forráskód Böngészése

Merge pull request #8022 from overleaf/msm-user-email-context-tests

[Settings] `user-email-context` unit test

GitOrigin-RevId: 55e3ec6a99f1714a27d698a60fcf64711066ab6c
Timothée Alby 4 éve
szülő
commit
4d44d9b417

+ 25 - 8
services/web/frontend/js/features/settings/context/user-email-context.tsx

@@ -105,6 +105,9 @@ const setLoadingAction = (state: State, action: ActionSetLoading) => ({
 })
 })
 
 
 const makePrimaryAction = (state: State, action: ActionMakePrimary) => {
 const makePrimaryAction = (state: State, action: ActionMakePrimary) => {
+  if (!state.data.byId[action.payload]) {
+    return state
+  }
   const byId: State['data']['byId'] = {}
   const byId: State['data']['byId'] = {}
   for (const id of Object.keys(state.data.byId)) {
   for (const id of Object.keys(state.data.byId)) {
     byId[id] = {
     byId[id] = {
@@ -137,19 +140,29 @@ const deleteEmailAction = (state: State, action: ActionDeleteEmail) => {
 const setEmailAffiliationBeingEditedAction = (
 const setEmailAffiliationBeingEditedAction = (
   state: State,
   state: State,
   action: ActionSetEmailAffiliationBeingEdited
   action: ActionSetEmailAffiliationBeingEdited
-) => ({
-  ...state,
-  data: {
-    ...state.data,
-    emailAffiliationBeingEdited: action.payload,
-  },
-})
+) => {
+  if (action.payload && !state.data.byId[action.payload]) {
+    return state
+  }
+  return {
+    ...state,
+    data: {
+      ...state.data,
+      emailAffiliationBeingEdited: action.payload,
+    },
+  }
+}
 
 
 const updateAffiliationAction = (
 const updateAffiliationAction = (
   state: State,
   state: State,
   action: ActionUpdateAffiliation
   action: ActionUpdateAffiliation
 ) => {
 ) => {
   const { email, role, department } = action.payload
   const { email, role, department } = action.payload
+
+  if (action.payload && !state.data.byId[email]) {
+    return state
+  }
+
   const affiliation = state.data.byId[email].affiliation
   const affiliation = state.data.byId[email].affiliation
 
 
   return {
   return {
@@ -213,11 +226,13 @@ function useUserEmails() {
     useAsync<UserEmailData[]>()
     useAsync<UserEmailData[]>()
 
 
   const getEmails = useCallback(() => {
   const getEmails = useCallback(() => {
+    dispatch(ActionCreators.setLoading(true))
     runAsync(getJSON('/user/emails?ensureAffiliation=true'))
     runAsync(getJSON('/user/emails?ensureAffiliation=true'))
       .then(data => {
       .then(data => {
         dispatch(ActionCreators.setData(data))
         dispatch(ActionCreators.setData(data))
       })
       })
       .catch(() => {})
       .catch(() => {})
+      .finally(() => dispatch(ActionCreators.setLoading(false)))
   }, [runAsync, dispatch])
   }, [runAsync, dispatch])
 
 
   // Get emails on page load
   // Get emails on page load
@@ -308,4 +323,6 @@ const useUserEmailsContext = () => {
   return context
   return context
 }
 }
 
 
-export { UserEmailsProvider, useUserEmailsContext }
+type EmailContextType = ReturnType<typeof useUserEmailsContext>
+
+export { UserEmailsProvider, useUserEmailsContext, EmailContextType }

+ 6 - 48
services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx

@@ -9,54 +9,12 @@ import {
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
 import { expect } from 'chai'
 import { expect } from 'chai'
 import fetchMock from 'fetch-mock'
 import fetchMock from 'fetch-mock'
-import { UserEmailData } from '../../../../../../types/user-email'
-
-const confirmedUserData: UserEmailData = {
-  confirmedAt: '2022-03-10T10:59:44.139Z',
-  email: 'bar@overleaf.com',
-  default: false,
-}
-
-const unconfirmedUserData: UserEmailData = {
-  email: 'baz@overleaf.com',
-  default: false,
-}
-
-const professionalUserData: UserEmailData = {
-  affiliation: {
-    cachedConfirmedAt: null,
-    cachedEntitlement: null,
-    cachedLastDayToReconfirm: null,
-    cachedPastReconfirmDate: false,
-    cachedReconfirmedAt: null,
-    department: 'Art History',
-    institution: {
-      commonsAccount: false,
-      confirmed: true,
-      id: 1,
-      isUniversity: false,
-      maxConfirmationMonths: null,
-      name: 'Overleaf',
-      ssoEnabled: false,
-      ssoBeta: false,
-    },
-    inReconfirmNotificationPeriod: false,
-    inferred: false,
-    licence: 'pro_plus',
-    pastReconfirmDate: false,
-    portal: { slug: '', templates_count: 1 },
-    role: 'Reader',
-  },
-  confirmedAt: '2022-03-09T10:59:44.139Z',
-  email: 'foo@overleaf.com',
-  default: true,
-}
-
-const fakeUsersData = [
-  { ...confirmedUserData },
-  { ...unconfirmedUserData },
-  { ...professionalUserData },
-]
+import {
+  confirmedUserData,
+  fakeUsersData,
+  professionalUserData,
+  unconfirmedUserData,
+} from '../../fixtures/test-user-email-data'
 
 
 describe('<EmailsSection />', function () {
 describe('<EmailsSection />', function () {
   beforeEach(function () {
   beforeEach(function () {

+ 340 - 0
services/web/test/frontend/features/settings/context/user-email-context.test.tsx

@@ -0,0 +1,340 @@
+import { expect } from 'chai'
+import { cloneDeep } from 'lodash'
+import { renderHook } from '@testing-library/react-hooks'
+import {
+  EmailContextType,
+  UserEmailsProvider,
+  useUserEmailsContext,
+} from '../../../../../frontend/js/features/settings/context/user-email-context'
+import fetchMock from 'fetch-mock'
+import {
+  confirmedUserData,
+  professionalUserData,
+  unconfirmedUserData,
+  fakeUsersData,
+} from '../fixtures/test-user-email-data'
+import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
+
+const renderUserEmailsContext = () =>
+  renderHook(() => useUserEmailsContext(), {
+    wrapper: ({ children }) => (
+      <UserEmailsProvider>{children}</UserEmailsProvider>
+    ),
+  })
+
+describe('UserEmailContext', function () {
+  beforeEach(function () {
+    fetchMock.reset()
+  })
+
+  describe('context bootstrap', function () {
+    it('should start with an "in progress" initialisation state', function () {
+      const { result } = renderUserEmailsContext()
+
+      expect(result.current.isInitializing).to.equal(true)
+      expect(result.current.isInitializingSuccess).to.equal(false)
+      expect(result.current.isInitializingError).to.equal(false)
+    })
+
+    it('should start with an empty state', function () {
+      const { result } = renderUserEmailsContext()
+
+      expect(result.current.state.data.byId).to.deep.equal({})
+      expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
+      expect(result.current.state.data.linkedInstitutionIds).to.have.length(0)
+    })
+
+    it('should load all user emails and update the initialisation state to "success"', async function () {
+      fetchMock.get(/\/user\/emails/, fakeUsersData)
+      const { result } = renderUserEmailsContext()
+      await fetchMock.flush(true)
+      expect(fetchMock.calls()).to.have.lengthOf(1)
+      expect(result.current.state.data.byId).to.deep.equal({
+        'bar@overleaf.com': confirmedUserData,
+        'baz@overleaf.com': unconfirmedUserData,
+        'foo@overleaf.com': professionalUserData,
+      })
+
+      expect(result.current.isInitializing).to.equal(false)
+      expect(result.current.isInitializingSuccess).to.equal(true)
+    })
+
+    it('when loading user email fails, it should update the initialisation state to "failed"', async function () {
+      fetchMock.get(/\/user\/emails/, 500)
+      const { result } = renderUserEmailsContext()
+      await fetchMock.flush()
+
+      expect(result.current.isInitializing).to.equal(false)
+      expect(result.current.isInitializingError).to.equal(true)
+    })
+
+    describe('state.isLoading', function () {
+      it('should be `true` on bootstrap', function () {
+        const { result } = renderUserEmailsContext()
+        expect(result.current.state.isLoading).to.equal(true)
+      })
+
+      it('should be updated with `setLoading`', function () {
+        const { result } = renderUserEmailsContext()
+        result.current.setLoading(true)
+        expect(result.current.state.isLoading).to.equal(true)
+        result.current.setLoading(false)
+        expect(result.current.state.isLoading).to.equal(false)
+      })
+    })
+  })
+  describe('context initialised', function () {
+    let result: { current: EmailContextType }
+
+    beforeEach(async function () {
+      fetchMock.get(/\/user\/emails/, fakeUsersData)
+      const value = renderUserEmailsContext()
+      result = value.result
+      await fetchMock.flush(true)
+    })
+
+    describe('getEmails()', function () {
+      beforeEach(async function () {
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [
+          {
+            email: 'new@email.com',
+            default: true,
+          },
+        ])
+      })
+
+      it('should set `isLoading === true`', function () {
+        result.current.getEmails()
+        expect(result.current.state.isLoading).to.be.true
+      })
+
+      it('requests a new set of emails', async function () {
+        result.current.getEmails()
+        await fetchMock.flush(true)
+        expect(result.current.state.data.byId).to.deep.equal({
+          'new@email.com': {
+            email: 'new@email.com',
+            default: true,
+          },
+        })
+      })
+    })
+
+    describe('makePrimary()', function () {
+      it('sets an email as `default`', function () {
+        expect(result.current.state.data.byId['bar@overleaf.com'].default).to.be
+          .false
+        result.current.makePrimary('bar@overleaf.com')
+        expect(result.current.state.data.byId['bar@overleaf.com'].default).to.be
+          .true
+      })
+
+      it('sets `default=false` for the current primary email ', function () {
+        expect(result.current.state.data.byId['foo@overleaf.com'].default).to.be
+          .true
+        result.current.makePrimary('bar@overleaf.com')
+        expect(result.current.state.data.byId['foo@overleaf.com'].default).to.be
+          .false
+      })
+
+      it('produces no effect when passing a non-existing email', function () {
+        const emails = cloneDeep(result.current.state.data.byId)
+        result.current.makePrimary('non-existing@email.com')
+        expect(result.current.state.data.byId).to.deep.equal(emails)
+      })
+    })
+
+    describe('deleteEmail()', function () {
+      it('removes data from the deleted email', function () {
+        result.current.deleteEmail('bar@overleaf.com')
+        expect(result.current.state.data.byId['bar@overleaf.com']).to.be
+          .undefined
+      })
+
+      it('produces no effect when passing a non-existing email', function () {
+        const emails = cloneDeep(result.current.state.data.byId)
+        result.current.deleteEmail('non-existing@email.com')
+        expect(result.current.state.data.byId).to.deep.equal(emails)
+      })
+    })
+
+    describe('setEmailAffiliationBeingEdited()', function () {
+      it('sets an email as currently being edited', function () {
+        result.current.setEmailAffiliationBeingEdited('bar@overleaf.com')
+        expect(result.current.state.data.emailAffiliationBeingEdited).to.equal(
+          'bar@overleaf.com'
+        )
+
+        result.current.setEmailAffiliationBeingEdited(null)
+        expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
+      })
+
+      it('produces no effect when passing a non-existing email', function () {
+        expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
+        result.current.setEmailAffiliationBeingEdited('non-existing@email.com')
+        expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
+      })
+    })
+
+    describe('updateAffiliation()', function () {
+      it('updates affiliation data for an email', function () {
+        result.current.updateAffiliation(
+          'foo@overleaf.com',
+          'new role',
+          'new department'
+        )
+        expect(
+          result.current.state.data.byId['foo@overleaf.com'].affiliation.role
+        ).to.equal('new role')
+        expect(
+          result.current.state.data.byId['foo@overleaf.com'].affiliation
+            .department
+        ).to.equal('new department')
+      })
+
+      it('clears an email from currently being edited', function () {
+        result.current.setEmailAffiliationBeingEdited('foo@overleaf.com')
+        result.current.updateAffiliation(
+          'foo@overleaf.com',
+          'new role',
+          'new department'
+        )
+        expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
+      })
+
+      it('produces no effect when passing an email with no affiliation', function () {
+        const emails = cloneDeep(result.current.state.data.byId)
+        result.current.updateAffiliation(
+          'bar@overleaf.com',
+          'new role',
+          'new department'
+        )
+        expect(result.current.state.data.byId).to.deep.equal(emails)
+      })
+
+      it('produces no effect when passing a non-existing email', function () {
+        const emails = cloneDeep(result.current.state.data.byId)
+        result.current.updateAffiliation(
+          'non-existing@email.com',
+          'new role',
+          'new department'
+        )
+        expect(result.current.state.data.byId).to.deep.equal(emails)
+      })
+    })
+
+    describe('resetLeaversSurveyExpiration()', function () {
+      beforeEach(function () {
+        localStorage.removeItem('showInstitutionalLeaversSurveyUntil')
+      })
+
+      it('when the leaver has institution license, and there is another email with institution license, it should not reset the survey expiration date', async function () {
+        const affiliatedEmail1 = cloneDeep(professionalUserData)
+        affiliatedEmail1.email = 'institution-test@example.com'
+        affiliatedEmail1.emailHasInstitutionLicence = true
+
+        const affiliatedEmail2 = cloneDeep(professionalUserData)
+        affiliatedEmail2.emailHasInstitutionLicence = true
+
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
+
+        result.current.getEmails()
+        await fetchMock.flush(true)
+
+        // `resetLeaversSurveyExpiration` always happens after deletion
+        result.current.deleteEmail(affiliatedEmail1.email)
+        result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
+
+        const expiration = localStorage.getItem(
+          'showInstitutionalLeaversSurveyUntil'
+        ) as number
+        expect(expiration).to.be.null
+      })
+
+      it("when the leaver's affiliation is past reconfirmation date, and there is another email with institution license, it should not reset the survey expiration date", async function () {
+        const affiliatedEmail1 = cloneDeep(professionalUserData)
+        affiliatedEmail1.email = 'institution-test@example.com'
+        affiliatedEmail1.affiliation.pastReconfirmDate = true
+
+        const affiliatedEmail2 = cloneDeep(professionalUserData)
+        affiliatedEmail2.emailHasInstitutionLicence = true
+
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
+
+        result.current.getEmails()
+        await fetchMock.flush(true)
+
+        // `resetLeaversSurveyExpiration` always happens after deletion
+        result.current.deleteEmail(affiliatedEmail1.email)
+        result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
+
+        const expiration = localStorage.getItem(
+          'showInstitutionalLeaversSurveyUntil'
+        ) as number
+        expect(expiration).to.be.null
+      })
+
+      it('when there are no other emails with institution license, it should reset the survey expiration date', async function () {
+        const affiliatedEmail1 = cloneDeep(professionalUserData)
+        affiliatedEmail1.emailHasInstitutionLicence = true
+        affiliatedEmail1.email = 'institution-test@example.com'
+        affiliatedEmail1.affiliation.pastReconfirmDate = true
+
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
+
+        result.current.getEmails()
+        await fetchMock.flush(true)
+
+        // `resetLeaversSurveyExpiration` always happens after deletion
+        result.current.deleteEmail(affiliatedEmail1.email)
+        result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
+
+        expect(
+          localStorage.getItem('showInstitutionalLeaversSurveyUntil')
+        ).to.be.greaterThan(Date.now())
+      })
+
+      it("when the leaver has no institution license, it shouldn't reset the survey expiration date", async function () {
+        const emailWithInstitutionLicense = cloneDeep(professionalUserData)
+        emailWithInstitutionLicense.email = 'institution-licensed@example.com'
+        emailWithInstitutionLicense.emailHasInstitutionLicence = false
+
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
+
+        result.current.getEmails()
+        await fetchMock.flush(true)
+
+        // `resetLeaversSurveyExpiration` always happens after deletion
+        result.current.deleteEmail(emailWithInstitutionLicense.email)
+        result.current.resetLeaversSurveyExpiration(professionalUserData)
+
+        expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
+          .be.null
+      })
+
+      it("when the leaver is not past its reconfirmation date, it shouldn't reset the survey expiration date", async function () {
+        const emailWithInstitutionLicense = cloneDeep(professionalUserData)
+        emailWithInstitutionLicense.email = 'institution-licensed@example.com'
+        emailWithInstitutionLicense.affiliation.pastReconfirmDate = false
+
+        fetchMock.reset()
+        fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
+
+        result.current.getEmails()
+        await fetchMock.flush(true)
+
+        // `resetLeaversSurveyExpiration` always happens after deletion
+        result.current.deleteEmail(emailWithInstitutionLicense.email)
+        result.current.resetLeaversSurveyExpiration(professionalUserData)
+
+        expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
+          .be.null
+      })
+    })
+  })
+})

+ 48 - 0
services/web/test/frontend/features/settings/fixtures/test-user-email-data.ts

@@ -0,0 +1,48 @@
+import { UserEmailData } from '../../../../../types/user-email'
+
+export const confirmedUserData: UserEmailData = {
+  confirmedAt: '2022-03-10T10:59:44.139Z',
+  email: 'bar@overleaf.com',
+  default: false,
+}
+
+export const unconfirmedUserData: UserEmailData = {
+  email: 'baz@overleaf.com',
+  default: false,
+}
+
+export const professionalUserData: UserEmailData = {
+  affiliation: {
+    cachedConfirmedAt: null,
+    cachedEntitlement: null,
+    cachedLastDayToReconfirm: null,
+    cachedPastReconfirmDate: false,
+    cachedReconfirmedAt: null,
+    department: 'Art History',
+    institution: {
+      commonsAccount: false,
+      confirmed: true,
+      id: 1,
+      isUniversity: false,
+      maxConfirmationMonths: null,
+      name: 'Overleaf',
+      ssoEnabled: false,
+      ssoBeta: false,
+    },
+    inReconfirmNotificationPeriod: false,
+    inferred: false,
+    licence: 'pro_plus',
+    pastReconfirmDate: false,
+    portal: { slug: '', templates_count: 1 },
+    role: 'Reader',
+  },
+  confirmedAt: '2022-03-09T10:59:44.139Z',
+  email: 'foo@overleaf.com',
+  default: true,
+}
+
+export const fakeUsersData = [
+  { ...confirmedUserData },
+  { ...unconfirmedUserData },
+  { ...professionalUserData },
+]