first commit
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import Input, {
|
||||
clearDomainCache,
|
||||
} from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
|
||||
import domainBlocklist from '../../../../../../frontend/js/features/settings/domain-blocklist'
|
||||
|
||||
const testInstitutionData = [
|
||||
{ university: { id: 124 }, hostname: 'domain.edu' },
|
||||
]
|
||||
|
||||
describe('<AddEmailInput/>', function () {
|
||||
const defaultProps = {
|
||||
onChange: (value: string) => {},
|
||||
handleAddNewEmail: () => {},
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
clearDomainCache()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
describe('on initial render', function () {
|
||||
it('should render an input with a placeholder', function () {
|
||||
render(<Input {...defaultProps} />)
|
||||
screen.getByPlaceholderText('e.g. johndoe@mit.edu')
|
||||
})
|
||||
|
||||
it('should not dispatch any `change` event', function () {
|
||||
const onChangeStub = sinon.stub()
|
||||
render(<Input {...defaultProps} onChange={onChangeStub} />)
|
||||
expect(onChangeStub.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when typing text that does not contain any potential domain match', function () {
|
||||
let onChangeStub: sinon.SinonStub
|
||||
let handleAddNewEmailStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
fetchMock.get('express:/institutions/domains', 200)
|
||||
onChangeStub = sinon.stub()
|
||||
handleAddNewEmailStub = sinon.stub()
|
||||
render(
|
||||
<Input
|
||||
{...defaultProps}
|
||||
onChange={onChangeStub}
|
||||
handleAddNewEmail={handleAddNewEmailStub}
|
||||
/>
|
||||
)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed', function () {
|
||||
const input = screen.getByTestId('affiliations-email') as HTMLInputElement
|
||||
expect(input.value).to.equal('user')
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event on every stroke', function () {
|
||||
expect(onChangeStub.calledWith('user')).to.equal(true)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 's' },
|
||||
})
|
||||
expect(onChangeStub.calledWith('s')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should not make any request for institution domains', function () {
|
||||
expect(fetchMock.callHistory.called()).to.be.false
|
||||
})
|
||||
|
||||
it('should submit on Enter if email looks valid', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@domain.com' },
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
expect(handleAddNewEmailStub.calledWith()).to.equal(true)
|
||||
})
|
||||
|
||||
it('should not submit on Enter if email does not look valid', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@' },
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
expect(handleAddNewEmailStub.calledWith()).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when typing text that contains a potential domain match', function () {
|
||||
let onChangeStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
onChangeStub = sinon.stub()
|
||||
render(<Input {...defaultProps} onChange={onChangeStub} />)
|
||||
})
|
||||
|
||||
describe('when there are no matches', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.get('express:/institutions/domains', 200)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed', function () {
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@d')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a domain match', function () {
|
||||
beforeEach(async function () {
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
// Wait for component to process the change and update the shadow input
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the text being typed along with the suggestion', async function () {
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@d')
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event with the typed text', function () {
|
||||
expect(onChangeStub.calledWith('user@d')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event with institution data when the typed email contains the institution domain', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@domain.edu' },
|
||||
})
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(
|
||||
onChangeStub.calledWith(
|
||||
'user@domain.edu',
|
||||
sinon.match(testInstitutionData[0])
|
||||
)
|
||||
).to.equal(true)
|
||||
})
|
||||
|
||||
it('should clear the suggestion when the potential domain match is completely deleted', async function () {
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: '' },
|
||||
})
|
||||
expect(screen.queryByText('user@domain.edu')).to.be.null
|
||||
})
|
||||
|
||||
describe('when there is a suggestion and "Tab" key is pressed', function () {
|
||||
beforeEach(async function () {
|
||||
// wait until autocompletion available
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Tab',
|
||||
})
|
||||
})
|
||||
|
||||
it('it should autocomplete the input', async function () {
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event with the domain matched', async function () {
|
||||
expect(
|
||||
onChangeStub.calledWith(
|
||||
'user@domain.edu',
|
||||
sinon.match(testInstitutionData[0])
|
||||
)
|
||||
).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a suggestion and "Enter" key is pressed', function () {
|
||||
beforeEach(async function () {
|
||||
// wait until autocompletion available
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
fireEvent.keyDown(screen.getByTestId('affiliations-email'), {
|
||||
key: 'Enter',
|
||||
})
|
||||
})
|
||||
|
||||
it('it should autocomplete the input', async function () {
|
||||
const input = screen.getByTestId(
|
||||
'affiliations-email'
|
||||
) as HTMLInputElement
|
||||
expect(input.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
it('should dispatch a `change` event with the domain matched', async function () {
|
||||
expect(
|
||||
onChangeStub.calledWith(
|
||||
'user@domain.edu',
|
||||
sinon.match(testInstitutionData[0])
|
||||
)
|
||||
).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should cache the result and skip subsequent requests', async function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
// clear input
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: '' },
|
||||
})
|
||||
// type a hint to trigger the domain search
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
|
||||
expect(fetchMock.callHistory.called()).to.be.false
|
||||
expect(onChangeStub.calledWith('user@d')).to.equal(true)
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a match for a blocklisted domain', function () {
|
||||
const [blockedDomain] = domainBlocklist
|
||||
|
||||
afterEach(function () {
|
||||
clearDomainCache()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('should not render the suggestion with blocked domain', async function () {
|
||||
const blockedInstitution = [
|
||||
{ university: { id: 1 }, hostname: blockedDomain },
|
||||
]
|
||||
fetchMock.get('express:/institutions/domains', blockedInstitution)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: `user@${blockedDomain.split('.')[0]}` },
|
||||
})
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(screen.queryByText(`user@${blockedDomain}`)).to.be.null
|
||||
})
|
||||
|
||||
it('should not render the suggestion with blocked domain having a subdomain', async function () {
|
||||
const blockedInstitution = [
|
||||
{
|
||||
university: { id: 1 },
|
||||
hostname: `subdomain.${blockedDomain}`,
|
||||
},
|
||||
]
|
||||
fetchMock.get('express:/institutions/domains', blockedInstitution)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: {
|
||||
value: `user@subdomain.${blockedDomain.split('.')[0]}`,
|
||||
},
|
||||
})
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(screen.queryByText(`user@subdomain.${blockedDomain}`)).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('while waiting for a response', function () {
|
||||
beforeEach(async function () {
|
||||
// type an initial suggestion
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
// make sure the next suggestions are delayed
|
||||
clearDomainCache()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('express:/institutions/domains', 200, { delay: 1000 })
|
||||
})
|
||||
|
||||
it('should keep the suggestion if the hint matches the previously matched domain', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@do' },
|
||||
})
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
it('should remove the suggestion if the hint does not match the previously matched domain', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@foo' },
|
||||
})
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request to fetch institution domains fail', function () {
|
||||
let onChangeStub
|
||||
|
||||
beforeEach(async function () {
|
||||
// initial request populates the suggestion
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
onChangeStub = sinon.stub()
|
||||
render(<Input {...defaultProps} onChange={onChangeStub} />)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@d' },
|
||||
})
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('user@domain.edu')
|
||||
})
|
||||
|
||||
// subsequent requests fail
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('express:/institutions/domains', 500)
|
||||
})
|
||||
|
||||
it('should clear suggestions', async function () {
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@dom' },
|
||||
})
|
||||
|
||||
const input = screen.getByTestId('affiliations-email') as HTMLInputElement
|
||||
expect(input.value).to.equal('user@dom')
|
||||
|
||||
await waitFor(() => {
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
|
||||
expect(fetchMock.callHistory.called()).to.be.true // ensures `domainCache` hasn't been hit
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the request to fetch institution is not matching input', function () {
|
||||
it('should clear suggestion', async function () {
|
||||
fetchMock.get('express:/institutions/domains', testInstitutionData)
|
||||
render(<Input {...defaultProps} onChange={sinon.stub()} />)
|
||||
fireEvent.change(screen.getByTestId('affiliations-email'), {
|
||||
target: { value: 'user@other' },
|
||||
})
|
||||
await fetchMock.callHistory.flush(true)
|
||||
const shadowInput = screen.getByTestId(
|
||||
'affiliations-email-shadow'
|
||||
) as HTMLInputElement
|
||||
expect(shadowInput.value).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,121 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import EmailsRow from '../../../../../../frontend/js/features/settings/components/emails/row'
|
||||
import {
|
||||
professionalUserData,
|
||||
unconfirmedUserData,
|
||||
} from '../../fixtures/test-user-email-data'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import { UserEmailsProvider } from '../../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import { Affiliation } from '../../../../../../types/affiliation'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function renderEmailsRow(data: UserEmailData) {
|
||||
return render(
|
||||
<UserEmailsProvider>
|
||||
<EmailsRow userEmailData={data} />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function getByTextContent(text: string) {
|
||||
return screen.getAllByText(
|
||||
(content, node) =>
|
||||
content === text || node?.children[0]?.textContent === text
|
||||
)
|
||||
}
|
||||
|
||||
describe('<EmailsRow/>', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
samlInitPath: '/saml',
|
||||
hasSamlBeta: true,
|
||||
})
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
describe('with unaffiliated email data', function () {
|
||||
it('renders email info', function () {
|
||||
renderEmailsRow(unconfirmedUserData)
|
||||
screen.getByText('baz@overleaf.com')
|
||||
})
|
||||
|
||||
it('renders actions', function () {
|
||||
renderEmailsRow(unconfirmedUserData)
|
||||
screen.getByRole('button', { name: 'Make Primary' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with affiliated email data', function () {
|
||||
it('renders email info', function () {
|
||||
renderEmailsRow(professionalUserData)
|
||||
screen.getByText('foo@overleaf.com')
|
||||
screen.getByText('Primary')
|
||||
})
|
||||
|
||||
it('renders actions', function () {
|
||||
renderEmailsRow(professionalUserData)
|
||||
screen.getByRole('button', { name: 'Remove' })
|
||||
})
|
||||
|
||||
it('renders institution info', function () {
|
||||
renderEmailsRow(professionalUserData)
|
||||
screen.getByText('Overleaf')
|
||||
screen.getByText('Reader, Art History')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with email data affiliated to an institution with SSO available', function () {
|
||||
let affiliatedEmail: UserEmailData & { affiliation: Affiliation }
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.get('ol-ExposedSettings').hasSamlFeature = true
|
||||
|
||||
// make sure the institution has SSO available
|
||||
affiliatedEmail = cloneDeep(professionalUserData)
|
||||
affiliatedEmail.affiliation.institution.confirmed = true
|
||||
affiliatedEmail.affiliation.institution.isUniversity = true
|
||||
affiliatedEmail.affiliation.institution.ssoEnabled = true
|
||||
})
|
||||
|
||||
describe('when the email is not yet linked to the institution', function () {
|
||||
beforeEach(async function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
|
||||
await fetchMock.callHistory.flush(true)
|
||||
})
|
||||
|
||||
it('prompts the user to link to their institutional account', function () {
|
||||
renderEmailsRow(affiliatedEmail)
|
||||
getByTextContent(
|
||||
'You can now link your Overleaf account to your Overleaf institutional account.'
|
||||
)
|
||||
screen.getByRole('button', { name: 'Link Accounts' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the email is already linked to the institution', function () {
|
||||
beforeEach(async function () {
|
||||
affiliatedEmail.samlProviderId = '1'
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail, unconfirmedUserData])
|
||||
await fetchMock.callHistory.flush(true)
|
||||
})
|
||||
|
||||
it('prompts the user to login using their institutional account', function () {
|
||||
renderEmailsRow(affiliatedEmail)
|
||||
getByTextContent(
|
||||
'You can log in to Overleaf through your Overleaf institutional login.'
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Link Accounts' })).to.be
|
||||
.null
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
fireEvent,
|
||||
} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { expect } from 'chai'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
|
||||
import { Institution } from '../../../../../../types/institution'
|
||||
import { Affiliation } from '../../../../../../types/affiliation'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const userEmailData: UserEmailData = {
|
||||
confirmedAt: '2022-03-10T10:59:44.139Z',
|
||||
email: 'bar@overleaf.com',
|
||||
default: false,
|
||||
}
|
||||
|
||||
const userEmailData2: UserEmailData & { affiliation: Affiliation } = {
|
||||
affiliation: {
|
||||
inReconfirmNotificationPeriod: false,
|
||||
institution: {
|
||||
confirmed: false,
|
||||
} as Institution,
|
||||
} as Affiliation,
|
||||
confirmedAt: '2022-03-10T10:59:44.139Z',
|
||||
email: 'bar@overleaf.com',
|
||||
default: false,
|
||||
}
|
||||
|
||||
describe('email actions - make primary', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
describe('disabled `make primary` button', function () {
|
||||
it('when renders with unconfirmed email', async function () {
|
||||
const userEmailDataCopy = { ...userEmailData2 }
|
||||
const { confirmedAt: _, ...userEmailData } = userEmailDataCopy
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = (await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})) as HTMLButtonElement
|
||||
expect(button.disabled).to.be.true
|
||||
})
|
||||
|
||||
it('when renders with email in reconfirmation period', async function () {
|
||||
const userEmailDataCopy = {
|
||||
...userEmailData2,
|
||||
affiliation: {
|
||||
...userEmailData2.affiliation,
|
||||
inReconfirmNotificationPeriod: true,
|
||||
},
|
||||
}
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = (await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})) as HTMLButtonElement
|
||||
expect(button.disabled).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('button tooltips', function () {
|
||||
it('when the email is in reconfirmation period', async function () {
|
||||
const userEmailDataCopy = {
|
||||
...userEmailData2,
|
||||
affiliation: {
|
||||
...userEmailData2.affiliation,
|
||||
inReconfirmNotificationPeriod: true,
|
||||
},
|
||||
}
|
||||
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = (await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})) as HTMLButtonElement
|
||||
|
||||
await userEvent.hover(button.parentElement!)
|
||||
|
||||
await screen.findByText(
|
||||
/Please confirm your affiliation before making this the primary/i
|
||||
)
|
||||
})
|
||||
|
||||
it('when the email is confirmed', async function () {
|
||||
const userEmailDataCopy = { ...userEmailData2 }
|
||||
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = (await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})) as HTMLButtonElement
|
||||
|
||||
await userEvent.hover(button.parentElement!)
|
||||
|
||||
await screen.findByText('Make this the primary email, used to log in', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('when not linked to institution', async function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
hasSamlFeature: true,
|
||||
})
|
||||
|
||||
const userEmailDataCopy = { ...userEmailData2 }
|
||||
const { confirmedAt: _, ...userEmailData } = userEmailDataCopy
|
||||
const userEmailDataCopy1 = { ...userEmailData }
|
||||
const userEmailDataCopy2 = {
|
||||
...userEmailData,
|
||||
email: 'baz@overleaf.com',
|
||||
affiliation: {
|
||||
...userEmailData.affiliation,
|
||||
institution: {
|
||||
...userEmailData.affiliation.institution,
|
||||
id: 123,
|
||||
confirmed: true,
|
||||
isUniversity: true,
|
||||
ssoEnabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [
|
||||
userEmailDataCopy1,
|
||||
userEmailDataCopy2,
|
||||
])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const buttons = (await screen.findAllByRole('button', {
|
||||
name: /make primary/i,
|
||||
})) as HTMLButtonElement[]
|
||||
|
||||
await userEvent.hover(buttons[1].parentElement!)
|
||||
|
||||
await screen.findByText(
|
||||
'Please confirm your email by linking to your institutional account before making it the primary email',
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('make an email primary', function () {
|
||||
const confirmPrimaryEmail = async () => {
|
||||
const button = await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
const withinModal = within(screen.getByRole('dialog'))
|
||||
fireEvent.click(
|
||||
withinModal.getByRole('button', { name: 'Change primary email' })
|
||||
)
|
||||
await waitForElementToBeRemoved(() => screen.getByRole('dialog'))
|
||||
}
|
||||
|
||||
it('shows confirmation modal and closes it', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', {
|
||||
name: /make primary/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
const withinModal = within(screen.getByRole('dialog'))
|
||||
withinModal.getByText(
|
||||
/do you want to change your primary email address to .*/i
|
||||
)
|
||||
withinModal.getByText(
|
||||
/this will be the email address to use if you log in with an email address and password/i
|
||||
)
|
||||
withinModal.getByText(
|
||||
/important .* notifications will be sent to this email address/i
|
||||
)
|
||||
withinModal.getByRole('button', { name: 'Change primary email' })
|
||||
|
||||
fireEvent.click(withinModal.getByRole('button', { name: /cancel/i }))
|
||||
|
||||
await waitForElementToBeRemoved(screen.getByRole('dialog'))
|
||||
})
|
||||
|
||||
it('shows loader and removes button', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/default?delete-unconfirmed-primary', 200)
|
||||
render(<EmailsSection />)
|
||||
|
||||
await confirmPrimaryEmail()
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
/an error has occurred while performing your request/i
|
||||
)
|
||||
).to.be.null
|
||||
expect(screen.queryByRole('button', { name: /make primary/i })).to.be.null
|
||||
})
|
||||
|
||||
it('shows error', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/default?delete-unconfirmed-primary', 503)
|
||||
render(<EmailsSection />)
|
||||
|
||||
await confirmPrimaryEmail()
|
||||
|
||||
screen.getByText(/sorry, something went wrong/i)
|
||||
await screen.findByRole('button', { name: /make primary/i })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('email actions - delete', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('shows loader when deleting and removes the row', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/delete', 200)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', { name: /remove/i })
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.queryByText(userEmailData.email)
|
||||
)
|
||||
})
|
||||
|
||||
it('shows error when deleting', async function () {
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/delete', 503)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole('button', { name: /remove/i })
|
||||
fireEvent.click(button)
|
||||
|
||||
await screen.queryByText(/sorry, something went wrong/i)
|
||||
screen.getByRole('button', { name: /remove/i })
|
||||
})
|
||||
})
|
@@ -0,0 +1,697 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
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'
|
||||
import { Affiliation } from '../../../../../../types/affiliation'
|
||||
import withMarkup from '../../../../helpers/with-markup'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const userEmailData: UserEmailData & { affiliation: Affiliation } = {
|
||||
affiliation: {
|
||||
cachedConfirmedAt: null,
|
||||
cachedEntitlement: null,
|
||||
cachedLastDayToReconfirm: null,
|
||||
cachedPastReconfirmDate: false,
|
||||
cachedReconfirmedAt: null,
|
||||
department: 'Art History',
|
||||
institution: {
|
||||
commonsAccount: false,
|
||||
writefullCommonsAccount: false,
|
||||
confirmed: true,
|
||||
id: 1,
|
||||
isUniversity: true,
|
||||
maxConfirmationMonths: null,
|
||||
name: 'Overleaf',
|
||||
ssoEnabled: false,
|
||||
ssoBeta: false,
|
||||
},
|
||||
inReconfirmNotificationPeriod: false,
|
||||
inferred: false,
|
||||
licence: 'pro_plus',
|
||||
pastReconfirmDate: false,
|
||||
portal: { slug: '', templates_count: 1 },
|
||||
role: 'Reader',
|
||||
},
|
||||
email: 'baz@overleaf.com',
|
||||
default: false,
|
||||
}
|
||||
|
||||
const institutionDomainData = [
|
||||
{
|
||||
university: {
|
||||
id: 1234,
|
||||
ssoEnabled: true,
|
||||
name: 'Auto Complete University',
|
||||
},
|
||||
hostname: 'autocomplete.edu',
|
||||
confirmed: true,
|
||||
},
|
||||
] as const
|
||||
|
||||
function resetFetchMock() {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('express:/institutions/domains', [])
|
||||
}
|
||||
|
||||
async function confirmCodeForEmail(email: string) {
|
||||
screen.getByText(`Enter the 6-digit confirmation code sent to ${email}.`)
|
||||
const inputCode = screen.getByLabelText(/6-digit confirmation code/i)
|
||||
fireEvent.change(inputCode, { target: { value: '123456' } })
|
||||
const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: 'Confirm',
|
||||
})
|
||||
fireEvent.click(submitCodeBtn)
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', { name: /confirming/i })
|
||||
)
|
||||
}
|
||||
|
||||
describe('<EmailsSection />', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: 'saml/init',
|
||||
})
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
resetFetchMock()
|
||||
})
|
||||
|
||||
it('renders "add another email" button', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByRole('button', { name: /add another email/i })
|
||||
})
|
||||
|
||||
it('renders input', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
await screen.findByLabelText(/email/i)
|
||||
})
|
||||
|
||||
it('renders "Start adding your address" until a valid email is typed', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
|
||||
fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
|
||||
render(<EmailsSection />)
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByLabelText(/email/i)
|
||||
|
||||
// initially the text is displayed and the "add email" button disabled
|
||||
screen.getByText('Start by adding your email address.')
|
||||
expect(
|
||||
screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: /add new email/i,
|
||||
}).disabled
|
||||
).to.be.true
|
||||
|
||||
// no changes while writing the email address
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'partial@email' },
|
||||
})
|
||||
screen.getByText('Start by adding your email address.')
|
||||
expect(
|
||||
screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: /add new email/i,
|
||||
}).disabled
|
||||
).to.be.true
|
||||
|
||||
// the text is removed when the complete email address is typed, and the "add button" is reenabled
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'valid@email.com' },
|
||||
})
|
||||
expect(screen.queryByText('Start by adding your email address.')).to.be.null
|
||||
expect(
|
||||
screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: /add new email/i,
|
||||
}).disabled
|
||||
).to.be.false
|
||||
})
|
||||
|
||||
it('renders "add new email" button', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
screen.getByRole('button', { name: /add new email/i })
|
||||
})
|
||||
|
||||
it('prevent users from adding new emails when the limit is reached', async function () {
|
||||
const emails = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
emails.push({ email: `bar${i}@overleaf.com` })
|
||||
}
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', emails)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const findByTextWithMarkup = withMarkup(screen.findByText)
|
||||
await findByTextWithMarkup(
|
||||
'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /add another email/i })).to.not
|
||||
.exist
|
||||
})
|
||||
|
||||
it('adds new email address', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
|
||||
'button',
|
||||
{ name: /add another email/i }
|
||||
)
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails/secondary', 200)
|
||||
.post('/user/emails/confirm-secondary', 200)
|
||||
|
||||
fireEvent.click(addAnotherEmailBtn)
|
||||
const input = screen.getByLabelText(/email/i)
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: userEmailData.email },
|
||||
})
|
||||
|
||||
const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: /add new email/i,
|
||||
})
|
||||
|
||||
expect(submitBtn.disabled).to.be.false
|
||||
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
expect(submitBtn.disabled).to.be.true
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', {
|
||||
name: /Loading/i,
|
||||
})
|
||||
)
|
||||
|
||||
await confirmCodeForEmail(userEmailData.email)
|
||||
await screen.findByText(userEmailData.email)
|
||||
})
|
||||
|
||||
it('fails to add add new email address', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
|
||||
'button',
|
||||
{ name: /add another email/i }
|
||||
)
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [])
|
||||
.post('/user/emails/secondary', 400)
|
||||
|
||||
fireEvent.click(addAnotherEmailBtn)
|
||||
const input = screen.getByLabelText(/email/i)
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: userEmailData.email },
|
||||
})
|
||||
|
||||
const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
|
||||
name: /add new email/i,
|
||||
})
|
||||
|
||||
expect(submitBtn.disabled).to.be.false
|
||||
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
expect(submitBtn.disabled).to.be.true
|
||||
|
||||
await screen.findByText(
|
||||
/Invalid Request. Please correct the data and try again./i
|
||||
)
|
||||
expect(submitBtn).to.not.be.null
|
||||
expect(submitBtn.disabled).to.be.false
|
||||
})
|
||||
|
||||
it('can link email address to an existing SSO institution', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('express:/institutions/domains', institutionDomainData)
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
const input = screen.getByLabelText(/email/i)
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'user@autocomplete.edu' },
|
||||
})
|
||||
|
||||
await screen.findByRole('button', { name: 'Link Accounts and Add Email' })
|
||||
})
|
||||
|
||||
it('adds new email address with existing institution and custom departments', async function () {
|
||||
const country = 'Germany'
|
||||
const customDepartment = 'Custom department'
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
|
||||
|
||||
const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
|
||||
name: /university/i,
|
||||
})
|
||||
|
||||
expect(universityInput.disabled).to.be.true
|
||||
|
||||
fetchMock.get('/institutions/list?country_code=de', [
|
||||
{
|
||||
id: userEmailData.affiliation.institution.id,
|
||||
name: userEmailData.affiliation.institution.name,
|
||||
country_code: 'de',
|
||||
departments: [customDepartment],
|
||||
},
|
||||
])
|
||||
|
||||
// Select the country from dropdown
|
||||
await userEvent.type(
|
||||
screen.getByRole('textbox', {
|
||||
name: /country/i,
|
||||
}),
|
||||
country
|
||||
)
|
||||
await userEvent.click(screen.getByText(country))
|
||||
|
||||
expect(universityInput.disabled).to.be.false
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
|
||||
// Select the university from dropdown
|
||||
await userEvent.click(universityInput)
|
||||
await userEvent.click(
|
||||
await screen.findByText(userEmailData.affiliation.institution.name)
|
||||
)
|
||||
|
||||
const roleInput = screen.getByRole('textbox', { name: /role/i })
|
||||
await userEvent.type(roleInput, userEmailData.affiliation.role!)
|
||||
const departmentInput = screen.getByRole('textbox', { name: /department/i })
|
||||
await userEvent.click(departmentInput)
|
||||
await userEvent.click(screen.getByText(customDepartment))
|
||||
|
||||
const userEmailDataCopy = {
|
||||
...userEmailData,
|
||||
affiliation: {
|
||||
...userEmailData.affiliation,
|
||||
department: customDepartment,
|
||||
},
|
||||
}
|
||||
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
.post('/user/emails/secondary', 200)
|
||||
.post('/user/emails/confirm-secondary', 200)
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /add new email/i,
|
||||
})
|
||||
)
|
||||
|
||||
const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
|
||||
|
||||
expect(
|
||||
JSON.parse(request?.options.body?.toString() || '{}')
|
||||
).to.deep.include({
|
||||
email: userEmailData.email,
|
||||
university: {
|
||||
id: userEmailData.affiliation?.institution.id,
|
||||
},
|
||||
role: userEmailData.affiliation?.role,
|
||||
department: customDepartment,
|
||||
})
|
||||
|
||||
screen.getByText(
|
||||
`Enter the 6-digit confirmation code sent to ${userEmailData.email}.`
|
||||
)
|
||||
|
||||
await confirmCodeForEmail(userEmailData.email)
|
||||
|
||||
await screen.findByText(userEmailData.affiliation.role!, { exact: false })
|
||||
await screen.findByText(customDepartment, { exact: false })
|
||||
})
|
||||
|
||||
it('autocompletes institution name', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
|
||||
fetchMock.get('/institutions/list?country_code=de', [
|
||||
{
|
||||
id: 1,
|
||||
name: 'University of Bonn',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bochum institute of Science',
|
||||
},
|
||||
])
|
||||
|
||||
// open "add new email" section and click "let us know" to open the Country/University form
|
||||
await userEvent.click(button)
|
||||
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
|
||||
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
|
||||
|
||||
// select a country
|
||||
const countryInput = screen.getByRole<HTMLInputElement>('textbox', {
|
||||
name: /country/i,
|
||||
})
|
||||
await userEvent.click(countryInput)
|
||||
await userEvent.type(countryInput, 'Germ')
|
||||
await userEvent.click(await screen.findByText('Germany'))
|
||||
|
||||
// match several universities on initial typing
|
||||
const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
|
||||
name: /university/i,
|
||||
})
|
||||
await userEvent.click(universityInput)
|
||||
await userEvent.type(universityInput, 'bo')
|
||||
await screen.findByText('University of Bonn')
|
||||
await screen.findByText('Bochum institute of Science')
|
||||
|
||||
// match a single university when typing to refine the search
|
||||
await userEvent.type(universityInput, 'nn')
|
||||
await screen.findByText('University of Bonn')
|
||||
expect(screen.queryByText('Bochum institute of Science')).to.be.null
|
||||
})
|
||||
|
||||
it('adds new email address without existing institution', async function () {
|
||||
const country = 'Germany'
|
||||
const countryCode = 'de'
|
||||
const newUniversity = 'Abcdef'
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
|
||||
|
||||
const universityInput = screen.getByRole<HTMLInputElement>('textbox', {
|
||||
name: /university/i,
|
||||
})
|
||||
|
||||
expect(universityInput.disabled).to.be.true
|
||||
|
||||
fetchMock.get(/\/institutions\/list/, [
|
||||
{
|
||||
id: userEmailData.affiliation.institution.id,
|
||||
name: userEmailData.affiliation.institution.name,
|
||||
country_code: 'de',
|
||||
departments: [],
|
||||
},
|
||||
])
|
||||
|
||||
// Select the country from dropdown
|
||||
await userEvent.type(
|
||||
screen.getByRole('textbox', {
|
||||
name: /country/i,
|
||||
}),
|
||||
country
|
||||
)
|
||||
await userEvent.click(screen.getByText(country))
|
||||
|
||||
expect(universityInput.disabled).to.be.false
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
resetFetchMock()
|
||||
|
||||
// Enter the university manually
|
||||
await userEvent.type(universityInput, newUniversity)
|
||||
|
||||
const roleInput = screen.getByRole('textbox', { name: /role/i })
|
||||
await userEvent.type(roleInput, userEmailData.affiliation.role!)
|
||||
const departmentInput = screen.getByRole('textbox', { name: /department/i })
|
||||
await userEvent.type(departmentInput, userEmailData.affiliation.department!)
|
||||
|
||||
const userEmailDataCopy = {
|
||||
...userEmailData,
|
||||
affiliation: {
|
||||
...userEmailData.affiliation,
|
||||
institution: {
|
||||
...userEmailData.affiliation.institution,
|
||||
name: newUniversity,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
.post('/user/emails/secondary', 200)
|
||||
.post('/user/emails/confirm-secondary', 200)
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /add new email/i,
|
||||
})
|
||||
)
|
||||
|
||||
await confirmCodeForEmail(userEmailData.email)
|
||||
|
||||
const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
|
||||
|
||||
expect(
|
||||
JSON.parse(request?.options.body?.toString() || '{}')
|
||||
).to.deep.include({
|
||||
email: userEmailData.email,
|
||||
university: {
|
||||
name: newUniversity,
|
||||
country_code: countryCode,
|
||||
},
|
||||
role: userEmailData.affiliation?.role,
|
||||
department: userEmailData.affiliation?.department,
|
||||
})
|
||||
|
||||
await screen.findByText(userEmailData.email)
|
||||
await screen.findByText(newUniversity)
|
||||
await screen.findByText(userEmailData.affiliation.role!, { exact: false })
|
||||
await screen.findByText(userEmailData.affiliation.department!, {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows country, university, role and department fields based on whether `change` was clicked or not', async function () {
|
||||
const institutionDomainDataCopy = [
|
||||
{
|
||||
...institutionDomainData[0],
|
||||
university: {
|
||||
...institutionDomainData[0].university,
|
||||
ssoEnabled: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(
|
||||
`/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
|
||||
institutionDomainDataCopy
|
||||
)
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByLabelText(/email/i),
|
||||
`user@${hostnameFirstChar}`
|
||||
)
|
||||
|
||||
await userEvent.keyboard('{Tab}')
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
expect(
|
||||
screen.queryByRole('textbox', {
|
||||
name: /country/i,
|
||||
})
|
||||
).to.be.null
|
||||
expect(
|
||||
screen.queryByRole('textbox', {
|
||||
name: /university/i,
|
||||
})
|
||||
).to.be.null
|
||||
screen.getByRole('textbox', {
|
||||
name: /role/i,
|
||||
})
|
||||
screen.getByRole('textbox', {
|
||||
name: /department/i,
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /change/i }))
|
||||
|
||||
screen.getByRole('textbox', {
|
||||
name: /country/i,
|
||||
})
|
||||
screen.getByRole('textbox', {
|
||||
name: /university/i,
|
||||
})
|
||||
expect(
|
||||
screen.queryByRole('textbox', {
|
||||
name: /role/i,
|
||||
})
|
||||
).to.be.null
|
||||
expect(
|
||||
screen.queryByRole('textbox', {
|
||||
name: /department/i,
|
||||
})
|
||||
).to.be.null
|
||||
})
|
||||
|
||||
it('displays institution name with change button when autocompleted and adds new record', async function () {
|
||||
const institutionDomainDataCopy = [
|
||||
{
|
||||
...institutionDomainData[0],
|
||||
university: {
|
||||
...institutionDomainData[0].university,
|
||||
ssoEnabled: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const button = await screen.findByRole<HTMLButtonElement>('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(
|
||||
`/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
|
||||
institutionDomainDataCopy
|
||||
)
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByLabelText(/email/i),
|
||||
`user@${hostnameFirstChar}`
|
||||
)
|
||||
|
||||
await userEvent.keyboard('{Tab}')
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
screen.getByText(institutionDomainDataCopy[0].university.name)
|
||||
|
||||
const userEmailDataCopy = {
|
||||
...userEmailData,
|
||||
affiliation: {
|
||||
...userEmailData.affiliation,
|
||||
institution: {
|
||||
...userEmailData.affiliation.institution,
|
||||
name: institutionDomainDataCopy[0].university.name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
|
||||
.post('/user/emails/secondary', 200)
|
||||
.post('/user/emails/confirm-secondary', 200)
|
||||
|
||||
await userEvent.type(
|
||||
screen.getByRole('textbox', { name: /role/i }),
|
||||
userEmailData.affiliation.role!
|
||||
)
|
||||
await userEvent.type(
|
||||
screen.getByRole('textbox', { name: /department/i }),
|
||||
userEmailData.affiliation.department!
|
||||
)
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /add new email/i,
|
||||
})
|
||||
)
|
||||
|
||||
await confirmCodeForEmail('user@autocomplete.edu')
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
screen.getByText(userEmailDataCopy.affiliation.institution.name, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByText(userEmailDataCopy.affiliation.role!, { exact: false })
|
||||
screen.getByText(userEmailDataCopy.affiliation.department!, {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import InstitutionAndRole from '../../../../../../frontend/js/features/settings/components/emails/institution-and-role'
|
||||
import { UserEmailsProvider } from '../../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
|
||||
import { Affiliation } from '../../../../../../types/affiliation'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
const userData1: UserEmailData & { affiliation: Affiliation } = {
|
||||
affiliation: {
|
||||
cachedConfirmedAt: null,
|
||||
cachedEntitlement: null,
|
||||
cachedLastDayToReconfirm: null,
|
||||
cachedPastReconfirmDate: false,
|
||||
cachedReconfirmedAt: null,
|
||||
department: null,
|
||||
institution: {
|
||||
commonsAccount: false,
|
||||
writefullCommonsAccount: 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: null,
|
||||
},
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
email: 'foo@overleaf.com',
|
||||
default: true,
|
||||
}
|
||||
|
||||
const userData2: UserEmailData & { affiliation: Affiliation } = {
|
||||
affiliation: {
|
||||
cachedConfirmedAt: null,
|
||||
cachedEntitlement: null,
|
||||
cachedLastDayToReconfirm: null,
|
||||
cachedPastReconfirmDate: false,
|
||||
cachedReconfirmedAt: null,
|
||||
department: 'Art History',
|
||||
institution: {
|
||||
commonsAccount: false,
|
||||
writefullCommonsAccount: 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',
|
||||
},
|
||||
email: 'baz@overleaf.com',
|
||||
default: false,
|
||||
}
|
||||
|
||||
describe('user role and institution', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [], {
|
||||
name: 'get user emails',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('renders affiliation name with add role/department button', function () {
|
||||
const userEmailData = userData1
|
||||
render(
|
||||
<UserEmailsProvider>
|
||||
<InstitutionAndRole userEmailData={userEmailData} />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
|
||||
screen.getByText(userEmailData.affiliation.institution.name, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByRole('button', { name: /add role and department/i })
|
||||
expect(screen.queryByRole('button', { name: /change/i })).to.not.exist
|
||||
})
|
||||
|
||||
it('renders affiliation name, role and department with change button', function () {
|
||||
const userEmailData = userData2
|
||||
render(
|
||||
<UserEmailsProvider>
|
||||
<InstitutionAndRole userEmailData={userEmailData} />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
|
||||
screen.getByText(userEmailData.affiliation.institution.name, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByText(userEmailData.affiliation.department!, { exact: false })
|
||||
screen.getByText(userEmailData.affiliation.role!, { exact: false })
|
||||
screen.getByRole('button', { name: /change/i })
|
||||
expect(screen.queryByRole('button', { name: /add role and department/i }))
|
||||
.to.not.exist
|
||||
})
|
||||
|
||||
it('fetches institution data and replaces departments dropdown on add/change', async function () {
|
||||
const userEmailData = userData1
|
||||
fetchMock.modifyRoute('get user emails', { response: [userEmailData] })
|
||||
render(<EmailsSection />)
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
const fakeDepartment = 'Fake department'
|
||||
const institution = userEmailData.affiliation.institution
|
||||
fetchMock.get(`/institutions/list/${institution.id}`, {
|
||||
id: institution.id,
|
||||
name: institution.name,
|
||||
country_code: 'de',
|
||||
departments: [fakeDepartment],
|
||||
})
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /add role and department/i })
|
||||
)
|
||||
|
||||
await fetchMock.callHistory.flush(true)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
|
||||
fireEvent.click(screen.getByRole('textbox', { name: /department/i }))
|
||||
|
||||
screen.getByText(fakeDepartment)
|
||||
})
|
||||
|
||||
it('adds new role and department', async function () {
|
||||
fetchMock
|
||||
.modifyRoute('get user emails', { response: [userData1] })
|
||||
.get(/\/institutions\/list/, { departments: [] })
|
||||
.post('/user/emails/endorse', 200)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const addBtn = await screen.findByRole('button', {
|
||||
name: /add role and department/i,
|
||||
})
|
||||
fireEvent.click(addBtn)
|
||||
|
||||
const submitBtn = screen.getByRole('button', {
|
||||
name: /save/i,
|
||||
}) as HTMLButtonElement
|
||||
expect(submitBtn.disabled).to.be.true
|
||||
|
||||
const roleValue = 'Dummy role'
|
||||
const departmentValue = 'Dummy department'
|
||||
|
||||
const roleInput = screen.getByPlaceholderText(/role/i)
|
||||
fireEvent.change(roleInput, {
|
||||
target: { value: roleValue },
|
||||
})
|
||||
|
||||
expect(submitBtn.disabled).to.be.true
|
||||
|
||||
const departmentInput = screen.getByPlaceholderText(/department/i)
|
||||
fireEvent.change(departmentInput, {
|
||||
target: { value: departmentValue },
|
||||
})
|
||||
|
||||
expect(submitBtn.disabled).to.be.false
|
||||
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
expect(submitBtn.disabled).to.be.true
|
||||
|
||||
await waitForElementToBeRemoved(() =>
|
||||
screen.getByRole('button', { name: /saving/i })
|
||||
)
|
||||
|
||||
await screen.findByText(roleValue, { exact: false })
|
||||
await screen.findByText(departmentValue, { exact: false })
|
||||
})
|
||||
})
|
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
|
||||
import { expect } from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {
|
||||
confirmedUserData,
|
||||
fakeUsersData,
|
||||
professionalUserData,
|
||||
unconfirmedUserData,
|
||||
} from '../../fixtures/test-user-email-data'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<EmailsSection />', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('renders translated heading', function () {
|
||||
render(<EmailsSection />)
|
||||
|
||||
screen.getByRole('heading', { name: /emails and affiliations/i })
|
||||
})
|
||||
|
||||
it('renders translated description', function () {
|
||||
render(<EmailsSection />)
|
||||
|
||||
screen.getByText(/add additional email addresses/i)
|
||||
screen.getByText(/to change your primary email/i)
|
||||
})
|
||||
|
||||
it('renders a loading message when loading', async function () {
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByText(/loading/i)
|
||||
})
|
||||
|
||||
it('renders an error message and hides loading message on error', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', 500)
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByText(
|
||||
/an error has occurred while performing your request/i
|
||||
)
|
||||
expect(screen.queryByText(/loading/i)).to.be.null
|
||||
})
|
||||
|
||||
it('renders user emails', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', fakeUsersData)
|
||||
render(<EmailsSection />)
|
||||
|
||||
await waitFor(() => {
|
||||
fakeUsersData.forEach(userData => {
|
||||
screen.getByText(new RegExp(userData.email, 'i'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('renders primary status', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByText(`${professionalUserData.email}`)
|
||||
screen.getByText('Primary')
|
||||
})
|
||||
|
||||
it('shows confirmation status for unconfirmed users', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByText(/unconfirmed/i)
|
||||
})
|
||||
|
||||
it('hides confirmation status for confirmed users', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [confirmedUserData])
|
||||
render(<EmailsSection />)
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
|
||||
|
||||
expect(screen.queryByText(/please check your inbox/i)).to.be.null
|
||||
})
|
||||
|
||||
it('renders resend link', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
await screen.findByRole('button', { name: /resend confirmation code/i })
|
||||
})
|
||||
|
||||
it('renders professional label', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
|
||||
render(<EmailsSection />)
|
||||
|
||||
const node = await screen.findByText(professionalUserData.email, {
|
||||
exact: false,
|
||||
})
|
||||
expect(within(node).getByText(/professional/i)).to.exist
|
||||
})
|
||||
|
||||
it('shows loader when resending email', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
|
||||
|
||||
render(<EmailsSection />)
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
|
||||
|
||||
fetchMock.post('/user/emails/send-confirmation-code', 200)
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /resend confirmation code/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: /resend confirmation code/i,
|
||||
})
|
||||
).to.be.null
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
|
||||
|
||||
expect(
|
||||
screen.queryByText(/an error has occurred while performing your request/i)
|
||||
).to.be.null
|
||||
|
||||
await screen.findAllByRole('button', {
|
||||
name: /resend confirmation code/i,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error when resending email fails', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
|
||||
|
||||
render(<EmailsSection />)
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
|
||||
|
||||
fetchMock.post('/user/emails/send-confirmation-code', 503)
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /resend confirmation code/i,
|
||||
})
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /resend confirmation code/i }))
|
||||
.to.be.null
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
|
||||
|
||||
screen.getByText(/sorry, something went wrong/i)
|
||||
screen.getByRole('button', { name: /resend confirmation code/i })
|
||||
})
|
||||
})
|
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react'
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import ReconfirmationInfo from '../../../../../../frontend/js/features/settings/components/emails/reconfirmation-info'
|
||||
import { ssoUserData } from '../../fixtures/test-user-email-data'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import { UserEmailsProvider } from '../../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function renderReconfirmationInfo(data: UserEmailData) {
|
||||
return render(
|
||||
<UserEmailsProvider>
|
||||
<ReconfirmationInfo userEmailData={data} />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('<ReconfirmationInfo/>', function () {
|
||||
let assignStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
samlInitPath: '/saml',
|
||||
})
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
setHash: sinon.stub(),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
this.locationStub.restore()
|
||||
})
|
||||
|
||||
describe('reconfirmed via SAML', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set(
|
||||
'ol-reconfirmedViaSAML',
|
||||
'sso-prof-saml-id'
|
||||
)
|
||||
})
|
||||
|
||||
it('show reconfirmed confirmation', function () {
|
||||
renderReconfirmationInfo(ssoUserData)
|
||||
screen.getByText('SSO University')
|
||||
screen.getByText(/affiliation is confirmed/)
|
||||
screen.getByText(/Thank you!/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('in reconfirm notification period', function () {
|
||||
let inReconfirmUserData: UserEmailData
|
||||
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
samlInitPath: '/saml',
|
||||
})
|
||||
|
||||
inReconfirmUserData = cloneDeep(ssoUserData)
|
||||
if (inReconfirmUserData.affiliation) {
|
||||
inReconfirmUserData.affiliation.inReconfirmNotificationPeriod = true
|
||||
}
|
||||
})
|
||||
|
||||
it('renders prompt', function () {
|
||||
renderReconfirmationInfo(inReconfirmUserData)
|
||||
screen.getByText(/Are you still at/)
|
||||
screen.getByText('SSO University')
|
||||
screen.getByText(
|
||||
/Please take a moment to confirm your institutional email address/
|
||||
)
|
||||
screen.getByRole('link', { name: 'Learn more' })
|
||||
expect(screen.queryByText(/add a new primary email address/)).to.not.exist
|
||||
})
|
||||
|
||||
it('renders default emails prompt', function () {
|
||||
inReconfirmUserData.default = true
|
||||
renderReconfirmationInfo(inReconfirmUserData)
|
||||
screen.getByText(/add a new primary email address/)
|
||||
})
|
||||
|
||||
describe('SAML reconfirmations', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: '/saml/init',
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to SAML flow', async function () {
|
||||
renderReconfirmationInfo(inReconfirmUserData)
|
||||
const confirmButton = screen.getByRole('button', {
|
||||
name: 'Confirm Affiliation',
|
||||
}) as HTMLButtonElement
|
||||
|
||||
await waitFor(() => {
|
||||
expect(confirmButton.disabled).to.be.false
|
||||
})
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
})
|
||||
sinon.assert.calledOnce(assignStub)
|
||||
sinon.assert.calledWithMatch(
|
||||
assignStub,
|
||||
'/saml/init?university_id=2&reconfirm=/user/settings'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Email reconfirmations', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasSamlFeature: false,
|
||||
})
|
||||
fetchMock.post('/user/emails/send-reconfirmation', 200)
|
||||
})
|
||||
|
||||
it('sends and resends confirmation email', async function () {
|
||||
renderReconfirmationInfo(inReconfirmUserData)
|
||||
const confirmButton = (await screen.findByRole('button', {
|
||||
name: 'Confirm Affiliation',
|
||||
})) as HTMLButtonElement
|
||||
|
||||
await waitFor(() => {
|
||||
expect(confirmButton.disabled).to.be.false
|
||||
})
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(confirmButton.disabled).to.be.true
|
||||
})
|
||||
expect(fetchMock.callHistory.called()).to.be.true
|
||||
|
||||
// the confirmation text should now be displayed
|
||||
await screen.findByText(/Please check your email inbox to confirm/)
|
||||
|
||||
// try the resend button
|
||||
fetchMock.clearHistory()
|
||||
const resendButton = await screen.findByRole('button', {
|
||||
name: 'Resend confirmation email',
|
||||
})
|
||||
|
||||
fireEvent.click(resendButton)
|
||||
|
||||
// commented out as it's already gone by this point
|
||||
// await screen.findByText(/Sending/)
|
||||
expect(fetchMock.callHistory.called()).to.be.true
|
||||
await waitForElementToBeRemoved(() => screen.getByText('Sending…'))
|
||||
await screen.findByRole('button', {
|
||||
name: 'Resend confirmation email',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,96 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { expect } from 'chai'
|
||||
import { SSOAlert } from '../../../../../../frontend/js/features/settings/components/emails/sso-alert'
|
||||
|
||||
describe('<SSOAlert/>', function () {
|
||||
describe('when there is no institutional linking information', function () {
|
||||
it('should be empty', function () {
|
||||
render(<SSOAlert />)
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is institutional linking information', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-institutionLinked', {
|
||||
universityName: 'Overleaf University',
|
||||
})
|
||||
})
|
||||
|
||||
it('should render an information alert with the university name', function () {
|
||||
render(<SSOAlert />)
|
||||
screen.getByRole('alert')
|
||||
screen.getByText('account was successfully linked', { exact: false })
|
||||
screen.getByText('Overleaf University', { exact: false })
|
||||
})
|
||||
|
||||
it('when entitled, it should render access granted to "professional" features', function () {
|
||||
window.metaAttributesCache.get('ol-institutionLinked').hasEntitlement =
|
||||
true
|
||||
render(<SSOAlert />)
|
||||
screen.getByText('this grants you access', { exact: false })
|
||||
screen.getByText('Professional')
|
||||
})
|
||||
|
||||
it('when the email is not canonical it should also render a warning alert', function () {
|
||||
window.metaAttributesCache.set(
|
||||
'ol-institutionEmailNonCanonical',
|
||||
'user@example.com'
|
||||
)
|
||||
render(<SSOAlert />)
|
||||
const alerts = screen.getAllByRole('alert')
|
||||
expect(alerts.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('the alerts should be closeable', function () {
|
||||
window.metaAttributesCache.set(
|
||||
'ol-institutionEmailNonCanonical',
|
||||
'user@example.com'
|
||||
)
|
||||
render(<SSOAlert />)
|
||||
const closeButtons = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
})
|
||||
fireEvent.click(closeButtons[0])
|
||||
fireEvent.click(closeButtons[1])
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a SAML Error', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-samlError', {
|
||||
message: 'there was an error',
|
||||
})
|
||||
})
|
||||
|
||||
it('should render an error alert', function () {
|
||||
render(<SSOAlert />)
|
||||
screen.getByRole('alert')
|
||||
screen.getByText('there was an error')
|
||||
})
|
||||
|
||||
it('should render translated error if available', function () {
|
||||
window.metaAttributesCache.get('ol-samlError').translatedMessage =
|
||||
'translated error'
|
||||
render(<SSOAlert />)
|
||||
screen.getByText('translated error')
|
||||
expect(screen.queryByText('there was an error')).to.be.null
|
||||
})
|
||||
|
||||
it('should render a "try again" label when requested by the error payload', function () {
|
||||
window.metaAttributesCache.get('ol-samlError').tryAgain = true
|
||||
render(<SSOAlert />)
|
||||
screen.getByText('Please try again')
|
||||
})
|
||||
|
||||
it('the alert should be closeable', function () {
|
||||
render(<SSOAlert />)
|
||||
const closeButton = screen.getByRole('button', {
|
||||
name: 'Close',
|
||||
})
|
||||
fireEvent.click(closeButton)
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user