first commit
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { expect } from 'chai'
|
||||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import AccountInfoSection from '../../../../../frontend/js/features/settings/components/account-info-section'
|
||||
import { UserProvider } from '../../../../../frontend/js/shared/context/user-context'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
function renderSectionWithUserProvider() {
|
||||
render(<AccountInfoSection />, {
|
||||
wrapper: ({ children }) => <UserProvider>{children}</UserProvider>,
|
||||
})
|
||||
}
|
||||
|
||||
describe('<AccountInfoSection />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
email: 'sherlock@holmes.co.uk',
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: false,
|
||||
})
|
||||
window.metaAttributesCache.set(
|
||||
'ol-isExternalAuthenticationSystemUsed',
|
||||
false
|
||||
)
|
||||
window.metaAttributesCache.set('ol-shouldAllowEditingDetails', true)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('submits all inputs', async function () {
|
||||
const updateMock = fetchMock.post('/user/settings', 200)
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Email'), {
|
||||
target: { value: 'john@watson.co.uk' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('First name'), {
|
||||
target: { value: 'John' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('Last name'), {
|
||||
target: { value: 'Watson' },
|
||||
})
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
expect(updateMock.callHistory.called()).to.be.true
|
||||
expect(
|
||||
JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
|
||||
).to.deep.equal({
|
||||
email: 'john@watson.co.uk',
|
||||
first_name: 'John',
|
||||
last_name: 'Watson',
|
||||
})
|
||||
})
|
||||
|
||||
it('disables button on invalid email', async function () {
|
||||
const updateMock = fetchMock.post('/user/settings', 200)
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Email'), {
|
||||
target: { value: 'john' },
|
||||
})
|
||||
const button = screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
}) as HTMLButtonElement
|
||||
|
||||
expect(button.disabled).to.be.true
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(updateMock.callHistory.called()).to.be.false
|
||||
})
|
||||
|
||||
it('shows inflight state and success message', async function () {
|
||||
let finishUpdateCall: (value: any) => void = () => {}
|
||||
fetchMock.post(
|
||||
'/user/settings',
|
||||
new Promise(resolve => (finishUpdateCall = resolve))
|
||||
)
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
await screen.findByRole('button', { name: /saving/i })
|
||||
|
||||
finishUpdateCall(200)
|
||||
await screen.findByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
screen.getByText('Thanks, your settings have been updated.')
|
||||
})
|
||||
|
||||
it('shows server error', async function () {
|
||||
fetchMock.post('/user/settings', 500)
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
await screen.findByText('Something went wrong. Please try again.')
|
||||
})
|
||||
|
||||
it('shows invalid error', async function () {
|
||||
fetchMock.post('/user/settings', 400)
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
await screen.findByText(
|
||||
'Invalid Request. Please correct the data and try again.'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows conflict error', async function () {
|
||||
fetchMock.post('/user/settings', {
|
||||
status: 409,
|
||||
body: {
|
||||
message: 'This email is already registered',
|
||||
},
|
||||
})
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
await screen.findByText('This email is already registered')
|
||||
})
|
||||
|
||||
it('hides email input', async function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
})
|
||||
const updateMock = fetchMock.post('/user/settings', 200)
|
||||
|
||||
renderSectionWithUserProvider()
|
||||
expect(screen.queryByLabelText('Email')).to.not.exist
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
expect(
|
||||
JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
|
||||
).to.deep.equal({
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
})
|
||||
|
||||
it('disables email input', async function () {
|
||||
window.metaAttributesCache.set(
|
||||
'ol-isExternalAuthenticationSystemUsed',
|
||||
true
|
||||
)
|
||||
const updateMock = fetchMock.post('/user/settings', 200)
|
||||
|
||||
renderSectionWithUserProvider()
|
||||
expect(screen.getByLabelText('Email')).to.have.property('readOnly', true)
|
||||
expect(screen.getByLabelText('First name')).to.have.property(
|
||||
'readOnly',
|
||||
false
|
||||
)
|
||||
expect(screen.getByLabelText('Last name')).to.have.property(
|
||||
'readOnly',
|
||||
false
|
||||
)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
expect(
|
||||
JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
|
||||
).to.deep.equal({
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
})
|
||||
|
||||
it('disables names input', async function () {
|
||||
window.metaAttributesCache.set('ol-shouldAllowEditingDetails', false)
|
||||
const updateMock = fetchMock.post('/user/settings', 200)
|
||||
|
||||
renderSectionWithUserProvider()
|
||||
expect(screen.getByLabelText('Email')).to.have.property('readOnly', false)
|
||||
expect(screen.getByLabelText('First name')).to.have.property(
|
||||
'readOnly',
|
||||
true
|
||||
)
|
||||
expect(screen.getByLabelText('Last name')).to.have.property(
|
||||
'readOnly',
|
||||
true
|
||||
)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Update',
|
||||
})
|
||||
)
|
||||
expect(
|
||||
JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
|
||||
).to.deep.equal({
|
||||
email: 'sherlock@holmes.co.uk',
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,42 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import BetaProgramSection from '../../../../../frontend/js/features/settings/components/beta-program-section'
|
||||
import { UserProvider } from '../../../../../frontend/js/shared/context/user-context'
|
||||
|
||||
function renderSectionWithUserProvider() {
|
||||
render(<BetaProgramSection />, {
|
||||
wrapper: ({ children }) => <UserProvider>{children}</UserProvider>,
|
||||
})
|
||||
}
|
||||
|
||||
describe('<BetaProgramSection />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
betaProgram: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows link to sessions', async function () {
|
||||
renderSectionWithUserProvider()
|
||||
|
||||
const link = screen.getByRole('link', {
|
||||
name: 'Manage Beta Program Membership',
|
||||
})
|
||||
|
||||
expect(link.getAttribute('href')).to.equal('/beta/participate')
|
||||
})
|
||||
|
||||
it('shows enrolled status', async function () {
|
||||
renderSectionWithUserProvider()
|
||||
screen.getByText('You are enrolled in the Beta Program')
|
||||
})
|
||||
|
||||
it('shows not enrolled status', async function () {
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
betaProgram: false,
|
||||
})
|
||||
renderSectionWithUserProvider()
|
||||
screen.getByText('You are not enrolled in the Beta Program')
|
||||
screen.getByText(/By joining this program you can have/)
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
fireEvent,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
render,
|
||||
} from '@testing-library/react'
|
||||
|
||||
import LeaveSection from '../../../../../frontend/js/features/settings/components/leave-section'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<LeaveSection />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-usersEmail', 'foo@bar.com')
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
})
|
||||
|
||||
it('opens modal', async function () {
|
||||
render(<LeaveSection />)
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: 'Delete your account',
|
||||
})
|
||||
|
||||
fireEvent.click(button)
|
||||
await screen.findByText('Delete Account')
|
||||
})
|
||||
|
||||
it('closes modal', async function () {
|
||||
render(<LeaveSection />)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Delete your account',
|
||||
})
|
||||
)
|
||||
|
||||
const cancelButton = screen.getByRole('button', {
|
||||
name: 'Close',
|
||||
})
|
||||
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByText('Delete Account'))
|
||||
})
|
||||
})
|
@@ -0,0 +1,50 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import LeaveModalContent from '../../../../../../frontend/js/features/settings/components/leave/modal-content'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<LeaveModalContent />', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('disable delete button if form is not valid', function () {
|
||||
render(
|
||||
<LeaveModalContent
|
||||
handleHide={() => {}}
|
||||
inFlight={false}
|
||||
setInFlight={() => {}}
|
||||
/>
|
||||
)
|
||||
screen.getByLabelText('Email')
|
||||
|
||||
const deleteButton = screen.getByRole('button', {
|
||||
name: 'Delete',
|
||||
})
|
||||
expect(deleteButton.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
|
||||
it('shows no password message', function () {
|
||||
window.metaAttributesCache.set('ol-isSaas', true)
|
||||
window.metaAttributesCache.set('ol-hasPassword', false)
|
||||
render(
|
||||
<LeaveModalContent
|
||||
handleHide={() => {}}
|
||||
inFlight={false}
|
||||
setInFlight={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
const link = screen.getByRole('link', {
|
||||
name: 'Please use the password reset form to set a password before deleting your account',
|
||||
})
|
||||
expect(link.getAttribute('href')).to.equal('/user/password/reset')
|
||||
})
|
||||
})
|
@@ -0,0 +1,196 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, screen, render, waitFor } from '@testing-library/react'
|
||||
import fetchMock, { type FetchMock } from 'fetch-mock'
|
||||
|
||||
import LeaveModalForm from '../../../../../../frontend/js/features/settings/components/leave/modal-form'
|
||||
import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<LeaveModalForm />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-usersEmail', 'foo@bar.com')
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('validates form', async function () {
|
||||
const setIsFormValid = sinon.stub()
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={() => {}}
|
||||
isFormValid={false}
|
||||
setIsFormValid={setIsFormValid}
|
||||
/>
|
||||
)
|
||||
|
||||
const emailInput = screen.getByLabelText('Email')
|
||||
fireEvent.change(emailInput, { target: { value: 'FOO@bar.com' } })
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password')
|
||||
fireEvent.change(passwordInput, { target: { value: 'foobar' } })
|
||||
|
||||
const checkbox = screen.getByLabelText(
|
||||
'I understand this will delete all projects in my Overleaf account with email address foo@bar.com'
|
||||
)
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
const setIsFormValidCalls = setIsFormValid.getCalls()
|
||||
const lastSetIsFormValidCall = setIsFormValidCalls.pop()
|
||||
expect(lastSetIsFormValidCall!.args[0]).to.be.true
|
||||
|
||||
for (const setIsFormValidCall of setIsFormValidCalls) {
|
||||
expect(setIsFormValidCall.args[0]).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
describe('submits', async function () {
|
||||
let setInFlight: sinon.SinonStub
|
||||
let setIsFormValid: sinon.SinonStub
|
||||
let deleteMock: FetchMock
|
||||
let assignStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
setInFlight = sinon.stub()
|
||||
setIsFormValid = sinon.stub()
|
||||
deleteMock = fetchMock.post('/user/delete', 200)
|
||||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
setHash: sinon.stub(),
|
||||
})
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
this.locationStub.restore()
|
||||
})
|
||||
|
||||
it('with valid form', async function () {
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={setInFlight}
|
||||
isFormValid
|
||||
setIsFormValid={setIsFormValid}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
sinon.assert.calledOnce(setInFlight)
|
||||
sinon.assert.calledWithMatch(setInFlight, true)
|
||||
expect(deleteMock.callHistory.called()).to.be.true
|
||||
await waitFor(() => {
|
||||
sinon.assert.calledTwice(setInFlight)
|
||||
sinon.assert.calledWithMatch(setInFlight, false)
|
||||
sinon.assert.calledOnce(assignStub)
|
||||
sinon.assert.calledWith(assignStub, '/')
|
||||
})
|
||||
})
|
||||
|
||||
it('with invalid form', async function () {
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={setInFlight}
|
||||
isFormValid={false}
|
||||
setIsFormValid={setIsFormValid}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
expect(deleteMock.callHistory.called()).to.be.false
|
||||
sinon.assert.notCalled(setInFlight)
|
||||
})
|
||||
})
|
||||
|
||||
it('handles credentials error without Saas tip', async function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: false })
|
||||
fetchMock.post('/user/delete', 403)
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={() => {}}
|
||||
isFormValid
|
||||
setIsFormValid={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/Your email or password is incorrect. Please try again/)
|
||||
})
|
||||
expect(screen.queryByText(/If you cannot remember your password/)).to.not
|
||||
.exist
|
||||
})
|
||||
|
||||
it('handles credentials error with Saas tip', async function () {
|
||||
fetchMock.post('/user/delete', 403)
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={() => {}}
|
||||
isFormValid
|
||||
setIsFormValid={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/Your email or password is incorrect. Please try again/)
|
||||
})
|
||||
screen.getByText(/If you cannot remember your password/)
|
||||
const link = screen.getByRole('link', { name: 'reset your password' })
|
||||
expect(link.getAttribute('href')).to.equal('/user/password/reset')
|
||||
})
|
||||
|
||||
it('handles subscription error', async function () {
|
||||
fetchMock.post('/user/delete', {
|
||||
status: 422,
|
||||
body: {
|
||||
error: 'SubscriptionAdminDeletionError',
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={() => {}}
|
||||
isFormValid
|
||||
setIsFormValid={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(
|
||||
'You cannot delete your account while on a subscription. Please cancel your subscription and try again. If you keep seeing this message please contact us.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('handles generic error', async function () {
|
||||
fetchMock.post('/user/delete', 500)
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={() => {}}
|
||||
isFormValid
|
||||
setIsFormValid={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(
|
||||
'Sorry, something went wrong deleting your account. Please try again in a minute.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,66 @@
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, screen, render, waitFor } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import LeaveModal from '../../../../../../frontend/js/features/settings/components/leave/modal'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<LeaveModal />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-usersEmail', 'foo@bar.com')
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('closes modal on cancel', async function () {
|
||||
const handleClose = sinon.stub()
|
||||
render(<LeaveModal isOpen handleClose={handleClose} />)
|
||||
|
||||
const cancelButton = screen.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
})
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
sinon.assert.calledOnce(handleClose)
|
||||
})
|
||||
|
||||
it('does not close modal while in flight', async function () {
|
||||
fetchMock.post('/user/delete', new Promise(() => {}))
|
||||
const handleClose = sinon.stub()
|
||||
render(<LeaveModal isOpen handleClose={handleClose} />)
|
||||
|
||||
fillValidForm()
|
||||
|
||||
const deleteButton = screen.getByRole('button', {
|
||||
name: 'Delete',
|
||||
})
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByRole('button', {
|
||||
name: 'Deleting…',
|
||||
})
|
||||
})
|
||||
|
||||
const cancelButton = screen.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
})
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
sinon.assert.notCalled(handleClose)
|
||||
})
|
||||
})
|
||||
|
||||
function fillValidForm() {
|
||||
fireEvent.change(screen.getByLabelText('Email'), {
|
||||
target: { value: 'foo@bar.com' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('Password'), {
|
||||
target: { value: 'foobar' },
|
||||
})
|
||||
fireEvent.click(screen.getByLabelText(/I understand/))
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import { UserEmailsProvider } from '../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import { LeaversSurveyAlert } from '../../../../../frontend/js/features/settings/components/leavers-survey-alert'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
function renderWithProvider() {
|
||||
render(<LeaversSurveyAlert />, {
|
||||
wrapper: ({ children }) => (
|
||||
<UserEmailsProvider>{children}</UserEmailsProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
describe('<LeaversSurveyAlert/>', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('should render before the expiration date', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
screen.getByRole('alert')
|
||||
screen.getByText(/Provide some quick feedback/)
|
||||
screen.getByRole('link', { name: 'Take a short survey' })
|
||||
})
|
||||
|
||||
it('should not render after the expiration date', function () {
|
||||
const yesterday = Date.now() - 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', yesterday)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
})
|
||||
|
||||
it('should not render if it has been hidden', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', true)
|
||||
renderWithProvider()
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
})
|
||||
|
||||
it('should reset the expiration date when it is closed', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
screen.getByRole('alert')
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
|
||||
expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to.be
|
||||
.null
|
||||
})
|
||||
|
||||
describe('event tracking', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
|
||||
beforeEach(function () {
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('should sent a `view` event on load', function () {
|
||||
expect(sendMBSpy).to.be.calledOnce
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'view', page: '/' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should sent a `click` event when the link is clicked', function () {
|
||||
fireEvent.click(screen.getByRole('link'))
|
||||
expect(sendMBSpy).to.be.calledTwice
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'click', page: '/' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should sent a `close` event when it is closed', function () {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(sendMBSpy).to.be.calledTwice
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'close', page: '/' }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,97 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import LinkingSection from '@/features/settings/components/linking-section'
|
||||
import { UserProvider } from '@/shared/context/user-context'
|
||||
import { SSOProvider } from '@/features/settings/context/sso-context'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
|
||||
function renderSectionWithProviders() {
|
||||
render(<LinkingSection />, {
|
||||
wrapper: ({ children }) => (
|
||||
<SplitTestProvider>
|
||||
<UserProvider>
|
||||
<SSOProvider>{children}</SSOProvider>
|
||||
</UserProvider>
|
||||
</SplitTestProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
const mockOauthProviders = {
|
||||
google: {
|
||||
descriptionKey: 'login_with_service',
|
||||
descriptionOptions: { service: 'Google' },
|
||||
name: 'Google',
|
||||
linkPath: '/auth/google',
|
||||
},
|
||||
orcid: {
|
||||
descriptionKey: 'oauth_orcid_description',
|
||||
descriptionOptions: {
|
||||
link: '/blog/434',
|
||||
appName: 'Overleaf',
|
||||
},
|
||||
name: 'ORCID',
|
||||
linkPath: '/auth/orcid',
|
||||
},
|
||||
}
|
||||
|
||||
describe('<LinkingSection />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-user', {})
|
||||
|
||||
// suppress integrations and references widgets as they cannot be tested in
|
||||
// all environments
|
||||
window.metaAttributesCache.set('ol-hideLinkingWidgets', true)
|
||||
|
||||
window.metaAttributesCache.set('ol-thirdPartyIds', {
|
||||
google: 'google-id',
|
||||
})
|
||||
|
||||
window.metaAttributesCache.set('ol-oauthProviders', mockOauthProviders)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('shows header', async function () {
|
||||
renderSectionWithProviders()
|
||||
|
||||
screen.getByText('Integrations')
|
||||
screen.getByText(
|
||||
'You can link your Overleaf account with other services to enable the features described below.'
|
||||
)
|
||||
})
|
||||
|
||||
it('lists SSO providers', async function () {
|
||||
renderSectionWithProviders()
|
||||
screen.getByText('linked accounts')
|
||||
|
||||
screen.getByText('Google')
|
||||
screen.getByText('Log in with Google.')
|
||||
screen.getByRole('button', { name: 'Unlink' })
|
||||
|
||||
screen.getByText('ORCID')
|
||||
screen.getByText(
|
||||
/Securely establish your identity by linking your ORCID iD/
|
||||
)
|
||||
const helpLink = screen.getByRole('link', { name: 'Learn more' })
|
||||
expect(helpLink.getAttribute('href')).to.equal('/blog/434')
|
||||
const linkButton = screen.getByRole('button', { name: 'Link' })
|
||||
expect(linkButton.getAttribute('href')).to.equal('/auth/orcid?intent=link')
|
||||
})
|
||||
|
||||
it('shows SSO error message', async function () {
|
||||
window.metaAttributesCache.set('ol-ssoErrorMessage', 'You no SSO')
|
||||
renderSectionWithProviders()
|
||||
screen.getByText('Error linking account: You no SSO')
|
||||
})
|
||||
|
||||
it('does not show providers section when empty', async function () {
|
||||
window.metaAttributesCache.delete('ol-oauthProviders')
|
||||
renderSectionWithProviders()
|
||||
|
||||
expect(screen.queryByText('linked accounts')).to.not.exist
|
||||
})
|
||||
})
|
@@ -0,0 +1,111 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { screen, fireEvent, render, within } from '@testing-library/react'
|
||||
import { IntegrationLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/integration-widget'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
|
||||
describe('<IntegrationLinkingWidgetTest/>', function () {
|
||||
const defaultProps = {
|
||||
logo: <div />,
|
||||
title: 'Integration',
|
||||
description: 'paragraph1',
|
||||
helpPath: '/learn',
|
||||
linkPath: '/link',
|
||||
unlinkPath: '/unlink',
|
||||
unlinkConfirmationTitle: 'confirm unlink',
|
||||
unlinkConfirmationText: 'you will be unlinked',
|
||||
}
|
||||
|
||||
describe('when the feature is not available', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
beforeEach(function () {
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
render(<IntegrationLinkingWidget {...defaultProps} hasFeature={false} />)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
})
|
||||
|
||||
it("should render 'Premium feature' label", function () {
|
||||
screen.getByText('Premium feature')
|
||||
})
|
||||
|
||||
it('should render an upgrade link and track clicks', function () {
|
||||
const upgradeLink = screen.getByRole('button', { name: 'Upgrade' })
|
||||
expect(upgradeLink.getAttribute('href')).to.equal(
|
||||
'/user/subscription/plans'
|
||||
)
|
||||
fireEvent.click(upgradeLink)
|
||||
expect(sendMBSpy).to.be.calledOnce
|
||||
expect(sendMBSpy).calledWith('settings-upgrade-click')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the integration is not linked', function () {
|
||||
beforeEach(function () {
|
||||
render(
|
||||
<IntegrationLinkingWidget {...defaultProps} hasFeature linked={false} />
|
||||
)
|
||||
})
|
||||
|
||||
it('should render a link to initiate integration linking', function () {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Link' }).getAttribute('href')
|
||||
).to.equal('/link')
|
||||
})
|
||||
|
||||
it("should not render 'premium feature' labels", function () {
|
||||
expect(screen.queryByText('premium_feature')).to.not.exist
|
||||
expect(screen.queryByText('integration_is_a_premium_feature')).to.not
|
||||
.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the integration is linked', function () {
|
||||
beforeEach(function () {
|
||||
render(
|
||||
<IntegrationLinkingWidget
|
||||
{...defaultProps}
|
||||
hasFeature
|
||||
linked
|
||||
statusIndicator={<div>status indicator</div>}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render a status indicator', function () {
|
||||
screen.getByText('status indicator')
|
||||
})
|
||||
|
||||
it("should not render 'premium feature' labels", function () {
|
||||
expect(screen.queryByText('premium_feature')).to.not.exist
|
||||
expect(screen.queryByText('integration_is_a_premium_feature')).to.not
|
||||
.exist
|
||||
})
|
||||
|
||||
it('should display an `unlink` button', function () {
|
||||
screen.getByRole('button', { name: 'Unlink' })
|
||||
})
|
||||
|
||||
it('should open a modal with a link to confirm integration unlinking', function () {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
const withinModal = within(screen.getByRole('dialog'))
|
||||
withinModal.getByText('confirm unlink')
|
||||
withinModal.getByText('you will be unlinked')
|
||||
withinModal.getByRole('button', { name: 'Cancel' })
|
||||
withinModal.getByRole('button', { name: 'Unlink' })
|
||||
})
|
||||
|
||||
it('should cancel unlinking when clicking "cancel" in the confirmation modal', async function () {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
screen.getByText('confirm unlink')
|
||||
const cancelBtn = screen.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
hidden: false,
|
||||
})
|
||||
fireEvent.click(cancelBtn)
|
||||
await screen.findByRole('button', { name: 'Cancel', hidden: true })
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,136 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import {
|
||||
screen,
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react'
|
||||
import { FetchError } from '../../../../../../frontend/js/infrastructure/fetch-json'
|
||||
import { SSOLinkingWidget } from '../../../../../../frontend/js/features/settings/components/linking/sso-widget'
|
||||
|
||||
describe('<SSOLinkingWidget />', function () {
|
||||
const defaultProps = {
|
||||
providerId: 'integration_id',
|
||||
title: 'integration',
|
||||
description: 'integration description',
|
||||
helpPath: '/help/integration',
|
||||
linkPath: '/integration/link',
|
||||
onUnlink: () => Promise.resolve(),
|
||||
}
|
||||
|
||||
it('should render', function () {
|
||||
render(<SSOLinkingWidget {...defaultProps} />)
|
||||
screen.getByText('integration')
|
||||
screen.getByText('integration description')
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'Learn more' }).getAttribute('href')
|
||||
).to.equal('/help/integration')
|
||||
})
|
||||
|
||||
describe('when unlinked', function () {
|
||||
it('should render a link to `linkPath`', function () {
|
||||
render(<SSOLinkingWidget {...defaultProps} linked={false} />)
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Link' }).getAttribute('href')
|
||||
).to.equal('/integration/link?intent=link')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when linked', function () {
|
||||
let unlinkFunction: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
unlinkFunction = sinon.stub()
|
||||
render(
|
||||
<SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
|
||||
)
|
||||
})
|
||||
|
||||
it('should display an `unlink` button', function () {
|
||||
screen.getByRole('button', { name: 'Unlink' })
|
||||
})
|
||||
|
||||
it('should open a modal to confirm integration unlinking', function () {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
screen.getByText('Unlink integration Account')
|
||||
screen.getByText(
|
||||
'Warning: When you unlink your account from integration you will not be able to sign in using integration anymore.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should cancel unlinking when clicking cancel in the confirmation modal', async function () {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
const cancelBtn = screen.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
hidden: false,
|
||||
})
|
||||
fireEvent.click(cancelBtn)
|
||||
await screen.findByRole('button', { name: 'Cancel', hidden: true })
|
||||
expect(unlinkFunction).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlinking an account', function () {
|
||||
let confirmBtn: HTMLElement, unlinkFunction: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
unlinkFunction = sinon.stub()
|
||||
render(
|
||||
<SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
confirmBtn = within(screen.getByRole('dialog')).getByRole('button', {
|
||||
name: 'Unlink',
|
||||
hidden: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should make an `unlink` request', function () {
|
||||
unlinkFunction.resolves()
|
||||
fireEvent.click(confirmBtn)
|
||||
expect(unlinkFunction).to.have.been.called
|
||||
})
|
||||
|
||||
it('should display feedback while the request is inflight', async function () {
|
||||
unlinkFunction.returns(
|
||||
new Promise<void>(resolve => {
|
||||
setTimeout(resolve, 500)
|
||||
})
|
||||
)
|
||||
fireEvent.click(confirmBtn)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: 'Unlinking' }))
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when unlinking fails', function () {
|
||||
beforeEach(function () {
|
||||
const unlinkFunction = sinon
|
||||
.stub()
|
||||
.rejects(new FetchError('unlinking failed', ''))
|
||||
render(
|
||||
<SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
|
||||
const confirmBtn = within(screen.getByRole('dialog')).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Unlink',
|
||||
hidden: false,
|
||||
}
|
||||
)
|
||||
fireEvent.click(confirmBtn)
|
||||
})
|
||||
|
||||
it('should display an error message ', async function () {
|
||||
await screen.findByText('Something went wrong. Please try again.')
|
||||
})
|
||||
|
||||
it('should display the unlink button ', async function () {
|
||||
await screen.findByRole('button', { name: 'Unlink' })
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,16 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
|
||||
import NewsletterSection from '../../../../../frontend/js/features/settings/components/newsletter-section'
|
||||
|
||||
describe('<NewsletterSection />', function () {
|
||||
it('shows link to sessions', async function () {
|
||||
render(<NewsletterSection />)
|
||||
|
||||
const link = screen.getByRole('link', {
|
||||
name: 'Manage Your Newsletter Preferences',
|
||||
})
|
||||
|
||||
expect(link.getAttribute('href')).to.equal('/user/email-preferences')
|
||||
})
|
||||
})
|
@@ -0,0 +1,213 @@
|
||||
import { expect } from 'chai'
|
||||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import PasswordSection from '../../../../../frontend/js/features/settings/components/password-section'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<PasswordSection />', function () {
|
||||
beforeEach(function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: true,
|
||||
})
|
||||
window.metaAttributesCache.set(
|
||||
'ol-isExternalAuthenticationSystemUsed',
|
||||
false
|
||||
)
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('shows password managed externally message', async function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
isOverleaf: false,
|
||||
})
|
||||
window.metaAttributesCache.set(
|
||||
'ol-isExternalAuthenticationSystemUsed',
|
||||
true
|
||||
)
|
||||
render(<PasswordSection />)
|
||||
|
||||
screen.getByText('Password settings are managed externally')
|
||||
})
|
||||
|
||||
it('shows no existing password message', async function () {
|
||||
window.metaAttributesCache.set('ol-hasPassword', false)
|
||||
render(<PasswordSection />)
|
||||
|
||||
screen.getByText('Please use the password reset form to set your password')
|
||||
})
|
||||
|
||||
it('submits all inputs', async function () {
|
||||
const updateMock = fetchMock.post('/user/password/update', 200)
|
||||
render(<PasswordSection />)
|
||||
submitValidForm()
|
||||
|
||||
expect(updateMock.callHistory.called()).to.be.true
|
||||
expect(
|
||||
JSON.parse(updateMock.callHistory.calls().at(-1)?.options.body as string)
|
||||
).to.deep.equal({
|
||||
currentPassword: 'foobar',
|
||||
newPassword1: 'barbaz',
|
||||
newPassword2: 'barbaz',
|
||||
})
|
||||
})
|
||||
|
||||
it('disables button on invalid form', async function () {
|
||||
const updateMock = fetchMock.post('/user/password/update', 200)
|
||||
render(<PasswordSection />)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Change',
|
||||
})
|
||||
)
|
||||
expect(updateMock.callHistory.called()).to.be.false
|
||||
})
|
||||
|
||||
it('validates inputs', async function () {
|
||||
render(<PasswordSection />)
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: 'Change',
|
||||
}) as HTMLButtonElement
|
||||
expect(button.disabled).to.be.true
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Current password'), {
|
||||
target: { value: 'foobar' },
|
||||
})
|
||||
expect(button.disabled).to.be.true
|
||||
|
||||
fireEvent.change(screen.getByLabelText('New password'), {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
expect(button.disabled).to.be.true
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Confirm new password'), {
|
||||
target: { value: 'bar' },
|
||||
})
|
||||
screen.getByText('Doesn’t match')
|
||||
expect(button.disabled).to.be.true
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Confirm new password'), {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
expect(button.disabled).to.be.false
|
||||
})
|
||||
|
||||
it('sets browser validation attributes', async function () {
|
||||
window.metaAttributesCache.set('ol-passwordStrengthOptions', {
|
||||
length: {
|
||||
min: 3,
|
||||
},
|
||||
})
|
||||
render(<PasswordSection />)
|
||||
|
||||
const currentPasswordInput = screen.getByLabelText(
|
||||
'Current password'
|
||||
) as HTMLInputElement
|
||||
const newPassword1Input = screen.getByLabelText(
|
||||
'New password'
|
||||
) as HTMLInputElement
|
||||
const newPassword2Input = screen.getByLabelText(
|
||||
'Confirm new password'
|
||||
) as HTMLInputElement
|
||||
|
||||
expect(newPassword1Input.minLength).to.equal(3)
|
||||
|
||||
// not required before changes
|
||||
expect(currentPasswordInput.required).to.be.false
|
||||
expect(newPassword1Input.required).to.be.false
|
||||
expect(newPassword2Input.required).to.be.false
|
||||
|
||||
fireEvent.change(currentPasswordInput, {
|
||||
target: { value: 'foobar' },
|
||||
})
|
||||
fireEvent.change(newPassword1Input, {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
fireEvent.change(newPassword2Input, {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
expect(currentPasswordInput.required).to.be.true
|
||||
expect(newPassword1Input.required).to.be.true
|
||||
expect(newPassword2Input.required).to.be.true
|
||||
})
|
||||
|
||||
it('shows inflight state and success message', async function () {
|
||||
let finishUpdateCall: (value: any) => void = () => {}
|
||||
fetchMock.post(
|
||||
'/user/password/update',
|
||||
new Promise(resolve => (finishUpdateCall = resolve))
|
||||
)
|
||||
render(<PasswordSection />)
|
||||
submitValidForm()
|
||||
|
||||
await screen.findByRole('button', { name: 'Saving…' })
|
||||
|
||||
finishUpdateCall({
|
||||
status: 200,
|
||||
body: {
|
||||
message: {
|
||||
type: 'success',
|
||||
email: 'tim.alby@overleaf.com',
|
||||
text: 'Password changed',
|
||||
},
|
||||
},
|
||||
})
|
||||
await screen.findByRole('button', {
|
||||
name: 'Change',
|
||||
})
|
||||
screen.getByText('Password changed')
|
||||
})
|
||||
|
||||
it('shows server error', async function () {
|
||||
fetchMock.post('/user/password/update', 500)
|
||||
render(<PasswordSection />)
|
||||
submitValidForm()
|
||||
await screen.findByText('Something went wrong. Please try again.')
|
||||
})
|
||||
|
||||
it('shows server error message', async function () {
|
||||
fetchMock.post('/user/password/update', {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Your old password is wrong',
|
||||
},
|
||||
})
|
||||
render(<PasswordSection />)
|
||||
submitValidForm()
|
||||
|
||||
await screen.findByText('Your old password is wrong')
|
||||
})
|
||||
|
||||
it('shows message when user cannot use password log in', async function () {
|
||||
window.metaAttributesCache.set('ol-cannot-change-password', true)
|
||||
render(<PasswordSection />)
|
||||
await screen.findByRole('heading', { name: 'Change Password' })
|
||||
screen.getByText(
|
||||
'You can’t add or change your password because your group or organization uses',
|
||||
{ exact: false }
|
||||
)
|
||||
screen.getByRole('link', { name: 'single sign-on (SSO)' })
|
||||
})
|
||||
})
|
||||
|
||||
function submitValidForm() {
|
||||
fireEvent.change(screen.getByLabelText('Current password'), {
|
||||
target: { value: 'foobar' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('New password'), {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('Confirm new password'), {
|
||||
target: { value: 'barbaz' },
|
||||
})
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Change',
|
||||
})
|
||||
)
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { screen, render, waitFor } from '@testing-library/react'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import SettingsPageRoot from '../../../../../frontend/js/features/settings/components/root'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<SettingsPageRoot />', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-usersEmail', 'foo@bar.com')
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { isOverleaf: true })
|
||||
window.metaAttributesCache.set('ol-hasPassword', true)
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: true,
|
||||
isOverleaf: true,
|
||||
})
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
features: { github: true, dropbox: true, mendeley: true, zotero: true },
|
||||
refProviders: {
|
||||
mendeley: true,
|
||||
zotero: true,
|
||||
},
|
||||
})
|
||||
window.metaAttributesCache.set('ol-github', { enabled: true })
|
||||
window.metaAttributesCache.set('ol-dropbox', { registered: true })
|
||||
window.metaAttributesCache.set('ol-oauthProviders', {})
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
})
|
||||
|
||||
it('displays page for Overleaf', async function () {
|
||||
render(<SettingsPageRoot />)
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('Account Settings')
|
||||
})
|
||||
screen.getByText('Emails and Affiliations')
|
||||
screen.getByText('Update Account Info')
|
||||
screen.getByText('Change Password')
|
||||
screen.getByText('Integrations')
|
||||
screen.getByText('Overleaf Beta Program')
|
||||
screen.getByText('Sessions')
|
||||
screen.getByText('Newsletter')
|
||||
screen.getByRole('button', {
|
||||
name: 'Delete your account',
|
||||
})
|
||||
})
|
||||
|
||||
it('displays page for non-Overleaf', async function () {
|
||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||
hasAffiliationsFeature: false,
|
||||
isOverleaf: false,
|
||||
})
|
||||
render(<SettingsPageRoot />)
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('Account Settings')
|
||||
})
|
||||
expect(screen.queryByText('Emails and Affiliations')).to.not.exist
|
||||
screen.getByText('Update Account Info')
|
||||
screen.getByText('Change Password')
|
||||
screen.getByText('Integrations')
|
||||
expect(screen.queryByText('Overleaf Beta Program')).to.not.exist
|
||||
screen.getByText('Sessions')
|
||||
expect(screen.queryByText('Newsletter')).to.not.exist
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: 'Delete your account',
|
||||
})
|
||||
).to.not.exist
|
||||
})
|
||||
|
||||
it('sends tracking event on load', async function () {
|
||||
render(<SettingsPageRoot />)
|
||||
|
||||
sinon.assert.calledOnce(sendMBSpy)
|
||||
sinon.assert.calledWith(sendMBSpy, 'settings-view')
|
||||
})
|
||||
})
|
@@ -0,0 +1,40 @@
|
||||
import SecuritySection from '@/features/settings/components/security-section'
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
describe('<SecuritySection />', function () {
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('shows Group SSO rows in security section', async function () {
|
||||
window.metaAttributesCache.set('ol-memberOfSSOEnabledGroups', [
|
||||
{
|
||||
groupId: 'abc123abc123',
|
||||
linked: true,
|
||||
},
|
||||
{
|
||||
groupId: 'fff999fff999',
|
||||
linked: false,
|
||||
},
|
||||
])
|
||||
render(<SecuritySection />)
|
||||
|
||||
expect(screen.getAllByText('Single Sign-On (SSO)').length).to.equal(2)
|
||||
const link = screen.getByRole('button', {
|
||||
name: /Set up SSO/i,
|
||||
})
|
||||
expect(link).to.exist
|
||||
expect(link.getAttribute('href')).to.equal(
|
||||
'/subscription/fff999fff999/sso_enrollment'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not show the security section with no groups with SSO enabled', async function () {
|
||||
window.metaAttributesCache.set('ol-memberOfSSOEnabledGroups', [])
|
||||
render(<SecuritySection />)
|
||||
|
||||
expect(screen.queryByText('Security')).to.not.exist
|
||||
})
|
||||
})
|
@@ -0,0 +1,16 @@
|
||||
import { expect } from 'chai'
|
||||
import { screen, render } from '@testing-library/react'
|
||||
|
||||
import SessionsSection from '../../../../../frontend/js/features/settings/components/sessions-section'
|
||||
|
||||
describe('<SessionsSection />', function () {
|
||||
it('shows link to sessions', async function () {
|
||||
render(<SessionsSection />)
|
||||
|
||||
const link = screen.getByRole('link', {
|
||||
name: 'Manage Your Sessions',
|
||||
})
|
||||
|
||||
expect(link.getAttribute('href')).to.equal('/user/sessions')
|
||||
})
|
||||
})
|
@@ -0,0 +1,82 @@
|
||||
import { expect } from 'chai'
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import {
|
||||
SSOProvider,
|
||||
useSSOContext,
|
||||
} from '../../../../../frontend/js/features/settings/context/sso-context'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
const mockOauthProviders = {
|
||||
google: {
|
||||
descriptionKey: 'login_with_service',
|
||||
descriptionOptions: { service: 'Google' },
|
||||
name: 'Google',
|
||||
linkPath: '/auth/google',
|
||||
},
|
||||
orcid: {
|
||||
descriptionKey: 'oauth_orcid_description',
|
||||
descriptionOptions: {
|
||||
link: '/blog/434',
|
||||
appName: 'Overleaf',
|
||||
},
|
||||
name: 'ORCID',
|
||||
linkPath: '/auth/orcid',
|
||||
},
|
||||
}
|
||||
|
||||
describe('SSOContext', function () {
|
||||
const renderSSOContext = () =>
|
||||
renderHook(() => useSSOContext(), {
|
||||
wrapper: ({ children }) => <SSOProvider>{children}</SSOProvider>,
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-thirdPartyIds', {
|
||||
google: 'google-id',
|
||||
})
|
||||
window.metaAttributesCache.set('ol-oauthProviders', mockOauthProviders)
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('should initialise subscriptions with their linked status', function () {
|
||||
const { result } = renderSSOContext()
|
||||
expect(result.current.subscriptions).to.deep.equal({
|
||||
google: {
|
||||
providerId: 'google',
|
||||
provider: mockOauthProviders.google,
|
||||
linked: true,
|
||||
},
|
||||
orcid: {
|
||||
providerId: 'orcid',
|
||||
provider: mockOauthProviders.orcid,
|
||||
linked: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlink', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.post('express:/user/oauth-unlink', 200)
|
||||
})
|
||||
|
||||
it('should unlink an existing subscription', async function () {
|
||||
const { result, waitForNextUpdate } = renderSSOContext()
|
||||
result.current.unlink('google')
|
||||
await waitForNextUpdate()
|
||||
expect(result.current.subscriptions.google.linked).to.be.false
|
||||
})
|
||||
|
||||
it('when the provider is not linked, should do nothing', function () {
|
||||
const { result } = renderSSOContext()
|
||||
result.current.unlink('orcid')
|
||||
expect(fetchMock.callHistory.called()).to.be.false
|
||||
})
|
||||
|
||||
it('supports unmounting the component while the request is inflight', async function () {
|
||||
const { result, unmount } = renderSSOContext()
|
||||
result.current.unlink('google')
|
||||
expect(fetchMock.callHistory.called()).to.be.true
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,364 @@
|
||||
import { expect } from 'chai'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import { waitFor } from '@testing-library/react'
|
||||
|
||||
import {
|
||||
EmailContextType,
|
||||
UserEmailsProvider,
|
||||
useUserEmailsContext,
|
||||
} from '../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {
|
||||
confirmedUserData,
|
||||
professionalUserData,
|
||||
unconfirmedUserData,
|
||||
fakeUsersData,
|
||||
unconfirmedCommonsUserData,
|
||||
untrustedUserData,
|
||||
} from '../fixtures/test-user-email-data'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
|
||||
const renderUserEmailsContext = () =>
|
||||
renderHook(() => useUserEmailsContext(), {
|
||||
wrapper: ({ children }) => (
|
||||
<UserEmailsProvider>{children}</UserEmailsProvider>
|
||||
),
|
||||
})
|
||||
|
||||
describe('UserEmailContext', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
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.callHistory.flush(true)
|
||||
expect(fetchMock.callHistory.calls()).to.have.lengthOf(1)
|
||||
expect(result.current.state.data.byId).to.deep.equal({
|
||||
'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
|
||||
'baz@overleaf.com': unconfirmedUserData,
|
||||
'foo@overleaf.com': professionalUserData,
|
||||
'qux@overleaf.com': unconfirmedCommonsUserData,
|
||||
})
|
||||
expect(result.current.state.data.linkedInstitutionIds).to.have.lengthOf(0)
|
||||
|
||||
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.callHistory.flush()
|
||||
|
||||
await waitFor(() => {
|
||||
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.callHistory.flush(true)
|
||||
})
|
||||
|
||||
describe('getEmails()', function () {
|
||||
beforeEach(async function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('should set `isLoading === true`', function () {
|
||||
fetchMock.get(/\/user\/emails/, [
|
||||
{
|
||||
email: 'new@email.com',
|
||||
default: true,
|
||||
},
|
||||
])
|
||||
result.current.getEmails()
|
||||
expect(result.current.state.isLoading).to.be.true
|
||||
})
|
||||
|
||||
it('requests a new set of emails', async function () {
|
||||
const emailData = {
|
||||
email: 'new@email.com',
|
||||
default: true,
|
||||
}
|
||||
fetchMock.get(/\/user\/emails/, [emailData])
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(result.current.state.data.byId).to.deep.equal({
|
||||
'new@email.com': emailData,
|
||||
})
|
||||
})
|
||||
|
||||
it('should populate `linkedInstitutionIds`', async function () {
|
||||
fetchMock.get(/\/user\/emails/, [
|
||||
confirmedUserData,
|
||||
{ ...unconfirmedUserData, samlProviderId: 'saml_provider_1' },
|
||||
{ ...professionalUserData, samlProviderId: 'saml_provider_2' },
|
||||
])
|
||||
const { result } = renderUserEmailsContext()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
|
||||
'saml_provider_1',
|
||||
'saml_provider_2',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
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.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.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.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.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.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.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.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.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.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.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,123 @@
|
||||
import { UserEmailData } from '../../../../../types/user-email'
|
||||
import { Affiliation } from '../../../../../types/affiliation'
|
||||
|
||||
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 untrustedUserData = {
|
||||
...confirmedUserData,
|
||||
lastConfirmedAt: '2024-01-01T10:59:44.139Z',
|
||||
}
|
||||
|
||||
export const professionalUserData: 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',
|
||||
},
|
||||
confirmedAt: '2022-03-09T10:59:44.139Z',
|
||||
email: 'foo@overleaf.com',
|
||||
default: true,
|
||||
}
|
||||
|
||||
export const unconfirmedCommonsUserData: UserEmailData & {
|
||||
affiliation: Affiliation
|
||||
} = {
|
||||
affiliation: {
|
||||
cachedConfirmedAt: null,
|
||||
cachedEntitlement: null,
|
||||
cachedLastDayToReconfirm: null,
|
||||
cachedPastReconfirmDate: false,
|
||||
cachedReconfirmedAt: null,
|
||||
department: 'Art History',
|
||||
institution: {
|
||||
commonsAccount: true,
|
||||
writefullCommonsAccount: false,
|
||||
confirmed: true,
|
||||
id: 1,
|
||||
isUniversity: false,
|
||||
maxConfirmationMonths: null,
|
||||
name: 'Overleaf',
|
||||
ssoEnabled: false,
|
||||
ssoBeta: false,
|
||||
},
|
||||
inReconfirmNotificationPeriod: false,
|
||||
inferred: false,
|
||||
licence: 'free',
|
||||
pastReconfirmDate: false,
|
||||
portal: { slug: '', templates_count: 1 },
|
||||
role: 'Reader',
|
||||
},
|
||||
email: 'qux@overleaf.com',
|
||||
default: true,
|
||||
}
|
||||
|
||||
export const ssoUserData: UserEmailData = {
|
||||
affiliation: {
|
||||
cachedConfirmedAt: '2022-02-03T11:46:28.249Z',
|
||||
cachedEntitlement: null,
|
||||
cachedLastDayToReconfirm: null,
|
||||
cachedPastReconfirmDate: false,
|
||||
cachedReconfirmedAt: null,
|
||||
department: 'Art History',
|
||||
institution: {
|
||||
commonsAccount: true,
|
||||
writefullCommonsAccount: false,
|
||||
confirmed: true,
|
||||
id: 2,
|
||||
isUniversity: true,
|
||||
maxConfirmationMonths: 12,
|
||||
name: 'SSO University',
|
||||
ssoEnabled: true,
|
||||
ssoBeta: false,
|
||||
},
|
||||
inReconfirmNotificationPeriod: false,
|
||||
inferred: false,
|
||||
licence: 'pro_plus',
|
||||
pastReconfirmDate: false,
|
||||
portal: { slug: '', templates_count: 0 },
|
||||
role: 'Prof',
|
||||
},
|
||||
confirmedAt: '2022-02-03T11:46:28.249Z',
|
||||
email: 'sso-prof@sso-university.edu',
|
||||
samlProviderId: 'sso-prof-saml-id',
|
||||
default: false,
|
||||
}
|
||||
|
||||
export const fakeUsersData = [
|
||||
{ ...confirmedUserData },
|
||||
{ ...unconfirmedUserData },
|
||||
{ ...untrustedUserData },
|
||||
{ ...professionalUserData },
|
||||
{ ...unconfirmedCommonsUserData },
|
||||
]
|
Reference in New Issue
Block a user