mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-08 14:51:56 +00:00
Merge pull request #8022 from overleaf/msm-user-email-context-tests
[Settings] `user-email-context` unit test GitOrigin-RevId: 55e3ec6a99f1714a27d698a60fcf64711066ab6c
This commit is contained in:
parent
233469b233
commit
4d44d9b417
4 changed files with 419 additions and 56 deletions
|
@ -105,6 +105,9 @@ const setLoadingAction = (state: State, action: ActionSetLoading) => ({
|
|||
})
|
||||
|
||||
const makePrimaryAction = (state: State, action: ActionMakePrimary) => {
|
||||
if (!state.data.byId[action.payload]) {
|
||||
return state
|
||||
}
|
||||
const byId: State['data']['byId'] = {}
|
||||
for (const id of Object.keys(state.data.byId)) {
|
||||
byId[id] = {
|
||||
|
@ -137,19 +140,29 @@ const deleteEmailAction = (state: State, action: ActionDeleteEmail) => {
|
|||
const setEmailAffiliationBeingEditedAction = (
|
||||
state: State,
|
||||
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 = (
|
||||
state: State,
|
||||
action: ActionUpdateAffiliation
|
||||
) => {
|
||||
const { email, role, department } = action.payload
|
||||
|
||||
if (action.payload && !state.data.byId[email]) {
|
||||
return state
|
||||
}
|
||||
|
||||
const affiliation = state.data.byId[email].affiliation
|
||||
|
||||
return {
|
||||
|
@ -213,11 +226,13 @@ function useUserEmails() {
|
|||
useAsync<UserEmailData[]>()
|
||||
|
||||
const getEmails = useCallback(() => {
|
||||
dispatch(ActionCreators.setLoading(true))
|
||||
runAsync(getJSON('/user/emails?ensureAffiliation=true'))
|
||||
.then(data => {
|
||||
dispatch(ActionCreators.setData(data))
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => dispatch(ActionCreators.setLoading(false)))
|
||||
}, [runAsync, dispatch])
|
||||
|
||||
// Get emails on page load
|
||||
|
@ -308,4 +323,6 @@ const useUserEmailsContext = () => {
|
|||
return context
|
||||
}
|
||||
|
||||
export { UserEmailsProvider, useUserEmailsContext }
|
||||
type EmailContextType = ReturnType<typeof useUserEmailsContext>
|
||||
|
||||
export { UserEmailsProvider, useUserEmailsContext, EmailContextType }
|
||||
|
|
|
@ -9,54 +9,12 @@ import {
|
|||
import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
|
||||
import { expect } from 'chai'
|
||||
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 () {
|
||||
beforeEach(function () {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 },
|
||||
]
|
Loading…
Add table
Reference in a new issue