mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-22 14:48:14 +00:00
Merge pull request #7225 from overleaf/ta-leave-modal
[DeleteAccount] Create Modal with Form GitOrigin-RevId: 611f08c7253f59d91c6937b79c80a386b9d21ccd
This commit is contained in:
parent
72f6d89e78
commit
d50271c1e9
14 changed files with 688 additions and 8 deletions
services/web
app/views/user
frontend
js/features/settings/components
stories/settings
locales
test
acceptance/src
frontend/features/settings/components
|
@ -221,7 +221,7 @@ block content
|
|||
.modal-header
|
||||
h3 #{translate("delete_account")}
|
||||
div.modal-body#delete-account-modal
|
||||
p !{translate("delete_account_warning_message_3")}
|
||||
p !{translate("delete_account_warning_message_3", {}, ['strong'])}
|
||||
if settings.createV1AccountOnLogin && settings.overleaf
|
||||
p
|
||||
strong
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LeaveModal from './leave/modal'
|
||||
|
||||
function LeaveSection() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsModalOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setIsModalOpen(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{t('need_to_leave')}{' '}
|
||||
<button className="btn btn-inline-link" onClick={handleOpen}>
|
||||
{t('delete_your_account')}
|
||||
</button>
|
||||
<LeaveModal isOpen={isModalOpen} handleClose={handleClose} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveSection
|
|
@ -0,0 +1,58 @@
|
|||
import { useState, Dispatch, SetStateAction } from 'react'
|
||||
import { Modal, Button } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import LeaveModalForm from './modal-form'
|
||||
|
||||
type LeaveModalContentProps = {
|
||||
handleHide: () => void
|
||||
inFlight: boolean
|
||||
setInFlight: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function LeaveModalContent({
|
||||
handleHide,
|
||||
inFlight,
|
||||
setInFlight,
|
||||
}: LeaveModalContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const [isFormValid, setIsFormValid] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('delete_account')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="delete_account_warning_message_3"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</p>
|
||||
<LeaveModalForm
|
||||
setInFlight={setInFlight}
|
||||
isFormValid={isFormValid}
|
||||
setIsFormValid={setIsFormValid}
|
||||
/>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button type="button" disabled={inFlight} onClick={handleHide}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
form="leave-form"
|
||||
type="submit"
|
||||
bsStyle="danger"
|
||||
disabled={inFlight || !isFormValid}
|
||||
>
|
||||
{inFlight ? <>{t('deleting')}…</> : t('delete')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveModalContent
|
|
@ -0,0 +1,46 @@
|
|||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import { FetchError } from '../../../../infrastructure/fetch-json'
|
||||
|
||||
type LeaveModalFormErrorProps = {
|
||||
error: FetchError
|
||||
}
|
||||
|
||||
function LeaveModalFormError({ error }: LeaveModalFormErrorProps) {
|
||||
const { t } = useTranslation()
|
||||
const isSaas = getMeta('ol-isSaas') as boolean
|
||||
|
||||
let errorMessage
|
||||
let errorTip = null
|
||||
if (error.response?.status === 403) {
|
||||
errorMessage = t('email_or_password_wrong_try_again')
|
||||
if (isSaas) {
|
||||
errorTip = (
|
||||
<Trans
|
||||
i18nKey="user_deletion_password_reset_tip"
|
||||
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
components={[<a href="/user/password/reset" />]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else if (error.data?.error === 'SubscriptionAdminDeletionError') {
|
||||
errorMessage = t('subscription_admins_cannot_be_deleted')
|
||||
} else {
|
||||
errorMessage = t('user_deletion_error')
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert bsStyle="danger">
|
||||
{errorMessage}
|
||||
{errorTip ? (
|
||||
<>
|
||||
<br />
|
||||
{errorTip}
|
||||
</>
|
||||
) : null}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveModalFormError
|
|
@ -0,0 +1,111 @@
|
|||
import { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
||||
import { Checkbox, ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { postJSON, FetchError } from '../../../../infrastructure/fetch-json'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import LeaveModalFormError from './modal-form-error'
|
||||
|
||||
type LeaveModalFormProps = {
|
||||
setInFlight: Dispatch<SetStateAction<boolean>>
|
||||
isFormValid: boolean
|
||||
setIsFormValid: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function LeaveModalForm({
|
||||
setInFlight,
|
||||
isFormValid,
|
||||
setIsFormValid,
|
||||
}: LeaveModalFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const userDefaultEmail = getMeta('ol-userDefaultEmail') as string
|
||||
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmation, setConfirmation] = useState(false)
|
||||
const [error, setError] = useState<FetchError | null>(null)
|
||||
|
||||
const handleEmailChange = event => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handlePasswordChange = event => {
|
||||
setPassword(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirmationChange = () => {
|
||||
setConfirmation(prev => !prev)
|
||||
}
|
||||
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault()
|
||||
if (!isFormValid) {
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setInFlight(true)
|
||||
postJSON('/user/delete', {
|
||||
body: {
|
||||
password,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
window.location.assign('/login')
|
||||
})
|
||||
.catch(setError)
|
||||
.finally(() => {
|
||||
setInFlight(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsFormValid(
|
||||
!!email &&
|
||||
email === userDefaultEmail &&
|
||||
password.length > 0 &&
|
||||
confirmation
|
||||
)
|
||||
}, [setIsFormValid, userDefaultEmail, email, password, confirmation])
|
||||
|
||||
return (
|
||||
<form id="leave-form" onSubmit={handleSubmit}>
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor="email-input">{t('email')}</ControlLabel>
|
||||
<FormControl
|
||||
id="email-input"
|
||||
type="text"
|
||||
placeholder={t('email')}
|
||||
required
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel htmlFor="password-input">{t('password')}</ControlLabel>
|
||||
<FormControl
|
||||
id="password-input"
|
||||
type="password"
|
||||
placeholder={t('password')}
|
||||
required
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Checkbox
|
||||
required
|
||||
checked={confirmation}
|
||||
onChange={handleConfirmationChange}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="delete_account_confirmation_label"
|
||||
components={[<i />]} // eslint-disable-line react/jsx-key
|
||||
values={{
|
||||
userDefaultEmail,
|
||||
}}
|
||||
/>
|
||||
</Checkbox>
|
||||
{error ? <LeaveModalFormError error={error} /> : null}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveModalForm
|
|
@ -0,0 +1,36 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import LeaveModalContent from './modal-content'
|
||||
|
||||
type LeaveModalProps = {
|
||||
isOpen: boolean
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
function LeaveModal({ isOpen, handleClose }: LeaveModalProps) {
|
||||
const [inFlight, setInFlight] = useState(false)
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
if (!inFlight) {
|
||||
handleClose()
|
||||
}
|
||||
}, [handleClose, inFlight])
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
animation
|
||||
show={isOpen}
|
||||
onHide={handleHide}
|
||||
id="leave-modal"
|
||||
backdrop="static"
|
||||
>
|
||||
<LeaveModalContent
|
||||
handleHide={handleHide}
|
||||
inFlight={inFlight}
|
||||
setInFlight={setInFlight}
|
||||
/>
|
||||
</AccessibleModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default LeaveModal
|
71
services/web/frontend/stories/settings/leave.stories.js
Normal file
71
services/web/frontend/stories/settings/leave.stories.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import useFetchMock from '../hooks/use-fetch-mock'
|
||||
import LeaveModal from '../../js/features/settings/components/leave/modal'
|
||||
import LeaveSection from '../../js/features/settings/components/leave-section'
|
||||
|
||||
const MOCK_DELAY = 1000
|
||||
window.metaAttributesCache = window.metaAttributesCache || new Map()
|
||||
|
||||
function defaultSetupMocks(fetchMock) {
|
||||
fetchMock.post(/\/user\/delete/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
}
|
||||
|
||||
export const Section = args => {
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'user@primary.com')
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <LeaveSection {...args} />
|
||||
}
|
||||
Section.component = LeaveSection
|
||||
Section.parameters = { controls: { include: [], hideNoControlsWarning: true } }
|
||||
|
||||
export const ModalSuccess = args => {
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'user@primary.com')
|
||||
useFetchMock(defaultSetupMocks)
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalAuthError = args => {
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'user@primary.com')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, 403)
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalServerError = args => {
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'user@primary.com')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, 500)
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export const ModalSubscriptionError = args => {
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'user@primary.com')
|
||||
useFetchMock(fetchMock => {
|
||||
fetchMock.post(/\/user\/delete/, {
|
||||
status: 422,
|
||||
body: {
|
||||
error: 'SubscriptionAdminDeletionError',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return <LeaveModal {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Leave',
|
||||
component: LeaveModal,
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleClose: { action: 'handleClose' },
|
||||
},
|
||||
}
|
|
@ -229,6 +229,7 @@
|
|||
"can_link_institution_email_acct_to_institution_acct": "You can now link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> institutional account.",
|
||||
"can_link_institution_email_acct_to_institution_acct_alt": "You can link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> institutional account.",
|
||||
"user_deletion_error": "Sorry, something went wrong deleting your account. Please try again in a minute.",
|
||||
"user_deletion_password_reset_tip": "If you cannot remember your password, or if you are using Single-Sign-On with another provider to sign in (such as Twitter or Google), please <0>reset your password</0> and try again.",
|
||||
"card_must_be_authenticated_by_3dsecure": "Your card must be authenticated with 3D Secure before continuing",
|
||||
"view_your_invoices": "View Your Invoices",
|
||||
"payment_provider_unreachable_error": "Sorry, there was an error talking to our payment provider. Please try again in a few moments.\nIf you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.",
|
||||
|
@ -631,7 +632,7 @@
|
|||
"log_in_with": "Log in with __provider__",
|
||||
"return_to_login_page": "Return to Login page",
|
||||
"login_failed": "Login failed",
|
||||
"delete_account_warning_message_3": "You are about to permanently <strong>delete all of your account data</strong>, including your projects and settings. Please type your account email address and password in the boxes below to proceed.",
|
||||
"delete_account_warning_message_3": "You are about to permanently <0>delete all of your account data</0>, including your projects and settings. Please type your account email address and password in the boxes below to proceed.",
|
||||
"delete_account_warning_message_2": "You are about to permanently <strong>delete all of your account data</strong>, including your projects and settings. Please type your account email address into the box below to proceed.",
|
||||
"your_sessions": "Your Sessions",
|
||||
"clear_sessions_description": "This is a list of other sessions (logins) which are active on your account, not including your current session. Click the \"Clear Sessions\" button below to log them out.",
|
||||
|
@ -917,6 +918,7 @@
|
|||
"delete_your_account": "Delete your account",
|
||||
"delete_account": "Delete Account",
|
||||
"delete_account_warning_message": "You are about to permanently <strong>delete all of your account data</strong>, including your projects and settings. Please type your account email address into the box below to proceed.",
|
||||
"delete_account_confirmation_label": "I understand this will delete all projects in my __appName__ account with email address <0>__userDefaultEmail__</0>",
|
||||
"deleting": "Deleting",
|
||||
"delete": "Delete",
|
||||
"sl_benefits_plans": "__appName__ is the world’s easiest to use LaTeX editor. Stay up to date with your collaborators, keep track of all changes to your work, and use our LaTeX environment from anywhere in the world.",
|
||||
|
@ -1182,7 +1184,7 @@
|
|||
"your_settings": "Your settings",
|
||||
"maintenance": "Maintenance",
|
||||
"to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again",
|
||||
"email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again",
|
||||
"email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again.",
|
||||
"rate_limit_hit_wait": "Rate limit hit. Please wait a while before retrying",
|
||||
"problem_changing_email_address": "There was a problem changing your email address. Please try again in a few moments. If the problem continues please contact us.",
|
||||
"single_version_easy_collab_blurb": "__appName__ makes sure that you’re always up to date with your collaborators and what they are doing. There is only a single master version of each document which everyone has access to. It’s impossible to make conflicting changes, and you don’t have to wait for your colleagues to send you the latest draft before you can keep working.",
|
||||
|
|
|
@ -54,7 +54,7 @@ describe('Captcha', function () {
|
|||
expect(response.statusCode).to.equal(401)
|
||||
expect(body).to.deep.equal({
|
||||
message: {
|
||||
text: 'Your email or password is incorrect. Please try again',
|
||||
text: 'Your email or password is incorrect. Please try again.',
|
||||
type: 'error',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -97,7 +97,9 @@ describe('Registration', function () {
|
|||
it('should produce the correct responses so far', function () {
|
||||
expect(results.length).to.equal(9)
|
||||
expect(results).to.deep.equal(
|
||||
Array(9).fill('Your email or password is incorrect. Please try again')
|
||||
Array(9).fill(
|
||||
'Your email or password is incorrect. Please try again.'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -115,7 +117,7 @@ describe('Registration', function () {
|
|||
expect(results.length).to.equal(15)
|
||||
expect(results).to.deep.equal(
|
||||
Array(10)
|
||||
.fill('Your email or password is incorrect. Please try again')
|
||||
.fill('Your email or password is incorrect. Please try again.')
|
||||
.concat(
|
||||
Array(5).fill(
|
||||
'This account has had too many login requests. Please wait 2 minutes before trying to log in again'
|
||||
|
@ -145,7 +147,7 @@ describe('Registration', function () {
|
|||
|
||||
it('should not rate limit their request', function () {
|
||||
expect(messages).to.deep.equal([
|
||||
'Your email or password is incorrect. Please try again',
|
||||
'Your email or password is incorrect. Please try again.',
|
||||
])
|
||||
})
|
||||
|
||||
|
@ -194,7 +196,7 @@ describe('Registration', function () {
|
|||
expect(results.length).to.equal(9)
|
||||
expect(results).to.deep.equal(
|
||||
Array(9).fill(
|
||||
'Your email or password is incorrect. Please try again'
|
||||
'Your email or password is incorrect. Please try again.'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
fireEvent,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
render,
|
||||
} from '@testing-library/react'
|
||||
|
||||
import LeaveSection from '../../../../../frontend/js/features/settings/components/leave-section'
|
||||
|
||||
describe('<LeaveSection />', function () {
|
||||
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,26 @@
|
|||
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'
|
||||
|
||||
describe('<LeaveModalContent />', function () {
|
||||
afterEach(function () {
|
||||
fetchMock.reset()
|
||||
})
|
||||
|
||||
it('disable delete button if form is not valid', function () {
|
||||
render(
|
||||
<LeaveModalContent
|
||||
handleHide={() => {}}
|
||||
inFlight={false}
|
||||
setInFlight={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
const deleteButton = screen.getByRole('button', {
|
||||
name: 'Delete',
|
||||
})
|
||||
expect(deleteButton.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
})
|
|
@ -0,0 +1,196 @@
|
|||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, screen, render, waitFor } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import LeaveModalForm from '../../../../../../frontend/js/features/settings/components/leave/modal-form'
|
||||
|
||||
describe('<LeaveModalForm />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'foo@bar.com')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
fetchMock.reset()
|
||||
})
|
||||
|
||||
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
|
||||
let setIsFormValid
|
||||
let deleteMock
|
||||
let locationStub
|
||||
const originalLocation = window.location
|
||||
|
||||
beforeEach(function () {
|
||||
setInFlight = sinon.stub()
|
||||
setIsFormValid = sinon.stub()
|
||||
deleteMock = fetchMock.post('/user/delete', 200)
|
||||
locationStub = sinon.stub()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
assign: locationStub,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.reset()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
})
|
||||
})
|
||||
|
||||
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.called()).to.be.true
|
||||
await waitFor(() => {
|
||||
sinon.assert.calledTwice(setInFlight)
|
||||
sinon.assert.calledWithMatch(setInFlight, false)
|
||||
sinon.assert.calledOnce(locationStub)
|
||||
sinon.assert.calledWithMatch(locationStub, '/login')
|
||||
})
|
||||
})
|
||||
|
||||
it('with invalid form', async function () {
|
||||
render(
|
||||
<LeaveModalForm
|
||||
setInFlight={setInFlight}
|
||||
isFormValid={false}
|
||||
setIsFormValid={setIsFormValid}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.submit(screen.getByLabelText('Email'))
|
||||
|
||||
expect(deleteMock.called()).to.be.false
|
||||
sinon.assert.notCalled(setInFlight)
|
||||
})
|
||||
})
|
||||
|
||||
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/)
|
||||
})
|
||||
expect(screen.queryByText(/If you cannot remember your password/)).to.not
|
||||
.exist
|
||||
})
|
||||
|
||||
it('handles credentials error without Saas tip', async function () {
|
||||
fetchMock.post('/user/delete', 403)
|
||||
window.metaAttributesCache.set('ol-isSaas', true)
|
||||
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,65 @@
|
|||
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'
|
||||
|
||||
describe('<LeaveModal />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set('ol-userDefaultEmail', 'foo@bar.com')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
fetchMock.reset()
|
||||
})
|
||||
|
||||
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/))
|
||||
}
|
Loading…
Add table
Reference in a new issue