Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,18 @@ const AccountProfilePage = (): ReactElement => {
try {
await deleteOwnAccount({ password: SHA256(passwordOrUsername) });
dispatchToastMessage({ type: 'success', message: t('User_has_been_deleted') });
setModal(null);
logout();
} catch (error: any) {
if (error.error === 'user-last-owner') {
const { shouldChangeOwner, shouldBeRemoved } = error.details;
return handleConfirmOwnerChange(passwordOrUsername, shouldChangeOwner, shouldBeRemoved);
}

if (error.errorType === 'error-invalid-password') {
throw error;
}

dispatchToastMessage({ type: 'error', message: error });
}
};
Expand Down
94 changes: 59 additions & 35 deletions apps/meteor/client/views/account/profile/ActionConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,87 @@
import { Box, PasswordInput, TextInput, FieldGroup, Field, FieldRow, FieldError } from '@rocket.chat/fuselage';
import { Box, PasswordInput, TextInput, FieldGroup, Field, FieldRow, FieldError, FieldLabel } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import type { ChangeEvent } from 'react';
import { useState, useCallback, useId } from 'react';
import { useId } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

type ActionConfirmModalProps = {
isPassword: boolean;
onConfirm: (input: string) => void;
onConfirm: (input: string) => Promise<void>;
onCancel: () => void;
};

// TODO: Use react-hook-form
const ActionConfirmModal = ({ isPassword, onConfirm, onCancel }: ActionConfirmModalProps) => {
const { t } = useTranslation();
const [inputText, setInputText] = useState('');
const [inputError, setInputError] = useState<string | undefined>();
const credentialFieldId = useId();
const credentialFieldError = `${credentialFieldId}-error`;

const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
e.target.value !== '' && setInputError(undefined);
setInputText(e.currentTarget.value);
},
[setInputText],
);
const {
control,
handleSubmit,
setError,
formState: { errors },
} = useForm({
defaultValues: { credential: '' },
mode: 'onBlur',
});

const handleSave = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (inputText === '') {
setInputError(t('Invalid_field'));
return;
const handleSave = async ({ credential }: { credential: string }) => {
try {
await onConfirm(credential);
} catch (error: any) {
if (error.errorType === 'error-invalid-password') {
setError('credential', { message: t('Invalid_password') });
}
onConfirm(inputText);
onCancel();
},
[inputText, onConfirm, onCancel, t],
);
}
};

const actionTextId = useId();
return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={handleSave} {...props} />}
onClose={onCancel}
onConfirm={handleSave}
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(handleSave)} {...props} />}
onCancel={onCancel}
variant='danger'
title={t('Delete_account?')}
confirmText={t('Delete_account')}
>
<Box mb={8} id={actionTextId}>
{isPassword ? t('Enter_your_password_to_delete_your_account') : t('Enter_your_username_to_delete_your_account')}
</Box>
<FieldGroup w='full'>
<Field>
<FieldLabel required htmlFor={credentialFieldId}>
{isPassword ? t('Enter_your_password_to_delete_your_account') : t('Enter_your_username_to_delete_your_account')}
</FieldLabel>
<FieldRow>
{isPassword && <PasswordInput value={inputText} onChange={handleChange} aria-labelledby={actionTextId} />}
{!isPassword && <TextInput value={inputText} onChange={handleChange} aria-labelledby={actionTextId} />}
<Controller
name='credential'
control={control}
rules={{ required: t('error-the-field-is-required', { field: isPassword ? t('Password') : t('Username') }) }}
render={({ field }) =>
isPassword ? (
<PasswordInput
{...field}
id={credentialFieldId}
error={errors.credential?.message}
aria-invalid={errors.credential ? 'true' : 'false'}
aria-describedby={errors.credential ? credentialFieldError : undefined}
aria-required='true'
/>
) : (
<TextInput
{...field}
id={credentialFieldId}
placeholder={t('Username')}
error={errors.credential?.message}
aria-invalid={errors.credential ? 'true' : 'false'}
aria-describedby={errors.credential ? credentialFieldError : undefined}
aria-required='true'
/>
)
}
/>
</FieldRow>
<FieldError>{inputError}</FieldError>
{errors.credential && (
<FieldError role='alert' id={credentialFieldError}>
{errors.credential.message}
</FieldError>
)}
</Field>
</FieldGroup>
</GenericModal>
Expand Down
32 changes: 9 additions & 23 deletions apps/meteor/tests/e2e/delete-account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,17 @@ test.describe('Delete Own Account', () => {
await page.goto('/account/profile');
await poAccountProfile.profileTitle.waitFor({ state: 'visible' });
await poAccountProfile.btnDeleteMyAccount.click();
await expect(poAccountProfile.deleteAccountDialog).toBeVisible();
});

await test.step('verify delete confirmation dialog appears', async () => {
await expect(poAccountProfile.deleteAccountDialogMessageWithPassword).toBeVisible();
await expect(poAccountProfile.inputDeleteAccountPassword).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountConfirm).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountCancel).toBeVisible();
await poAccountProfile.deleteAccountModal.waitForDisplay();
});

await test.step('enter invalid password in the confirmation field and click delete account', async () => {
await poAccountProfile.inputDeleteAccountPassword.fill('invalid-password');
await expect(poAccountProfile.inputDeleteAccountPassword).toHaveValue('invalid-password');
await poAccountProfile.btnDeleteAccountConfirm.click();
await poAccountProfile.deleteAccountModal.inputPassword.fill('invalid-password');
await expect(poAccountProfile.deleteAccountModal.inputPassword).toHaveValue('invalid-password');
await poAccountProfile.deleteAccountModal.confirmDelete({ waitForDismissal: false });
});

await test.step('verify error message appears', async () => {
await poAccountProfile.toastMessage.waitForDisplay({ type: 'error', message: 'Invalid password [error-invalid-password]' });
await expect(poAccountProfile.deleteAccountModal.inputErrorMessage).toBeVisible();
});

await test.step('verify user is still on the profile page', async () => {
Expand All @@ -84,20 +77,13 @@ test.describe('Delete Own Account', () => {
await page.goto('/account/profile');
await poAccountProfile.profileTitle.waitFor({ state: 'visible' });
await poAccountProfile.btnDeleteMyAccount.click();
await expect(poAccountProfile.deleteAccountDialog).toBeVisible();
});

await test.step('verify delete confirmation dialog appears', async () => {
await expect(poAccountProfile.deleteAccountDialogMessageWithPassword).toBeVisible();
await expect(poAccountProfile.inputDeleteAccountPassword).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountConfirm).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountCancel).toBeVisible();
await poAccountProfile.deleteAccountModal.waitForDisplay();
});

await test.step('enter password in the confirmation field and click delete account', async () => {
await poAccountProfile.inputDeleteAccountPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await expect(poAccountProfile.inputDeleteAccountPassword).toHaveValue(DEFAULT_USER_CREDENTIALS.password);
await poAccountProfile.btnDeleteAccountConfirm.click();
await poAccountProfile.deleteAccountModal.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await expect(poAccountProfile.deleteAccountModal.inputPassword).toHaveValue(DEFAULT_USER_CREDENTIALS.password);
await poAccountProfile.deleteAccountModal.confirmDelete();
});

await test.step('verify user is redirected to login page', async () => {
Expand Down
32 changes: 8 additions & 24 deletions apps/meteor/tests/e2e/page-objects/account-profile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { Locator, Page } from '@playwright/test';

import { Account } from './account';
import { DeleteAccountModal } from './fragments';

export class AccountProfile extends Account {
readonly deleteAccountModal: DeleteAccountModal;

constructor(page: Page) {
super(page);
this.deleteAccountModal = new DeleteAccountModal(page);
}

get inputName(): Locator {
Expand Down Expand Up @@ -108,31 +112,11 @@ export class AccountProfile extends Account {
return this.page.getByRole('button', { name: 'Save changes', exact: true });
}

get btnDeleteMyAccount(): Locator {
return this.page.getByRole('button', { name: 'Delete my account' });
}

get deleteAccountDialog(): Locator {
return this.page.getByRole('dialog', { name: 'Delete account?' });
}

get deleteAccountDialogMessageWithPassword(): Locator {
return this.deleteAccountDialog.getByText('Enter your password to delete your account. This cannot be undone.');
}

get inputDeleteAccountPassword(): Locator {
return this.deleteAccountDialog.getByRole('textbox', { name: 'Enter your password to delete your account. This cannot be undone.' });
}

get btnDeleteAccountConfirm(): Locator {
return this.deleteAccountDialog.getByRole('button', { name: 'Delete Account' });
}

get btnDeleteAccountCancel(): Locator {
return this.deleteAccountDialog.getByRole('button', { name: 'Cancel' });
}

get profileTitle(): Locator {
return this.page.getByRole('heading', { name: 'Profile' });
}

get btnDeleteMyAccount(): Locator {
return this.page.getByRole('button', { name: 'Delete my account' });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Locator, Page } from 'playwright-core';

import { Modal } from './modal';

export class DeleteAccountModal extends Modal {
constructor(page: Page) {
super(page.getByRole('dialog', { name: 'Delete account?' }));
}

get btnDeleteAccount(): Locator {
return this.root.getByRole('button', { name: 'Delete Account' });
}

get btnCancel(): Locator {
return this.root.getByRole('button', { name: 'Cancel' });
}

async confirmDelete({ waitForDismissal = true } = {}): Promise<void> {
await this.btnDeleteAccount.click();
if (waitForDismissal) {
await this.waitForDismissal();
}
}

get deleteAccountDialogMessageWithPassword(): Locator {
return this.root.getByText('Enter your password to delete your account. This cannot be undone.');
}

get inputPassword(): Locator {
return this.root.getByRole('textbox', { name: 'Enter your password to delete your account. This cannot be undone.' });
}

get inputErrorMessage(): Locator {
return this.root.locator('[role="alert"]', { hasText: 'Invalid password' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './apps-modal';
export * from './confirm-delete-modal';
export * from './confirm-logout-modal';
export * from './create-new-modal';
export * from './delete-account-modal';
export * from './disable-room-encryption-modal';
export * from './edit-status-modal';
export * from './enable-room-encryption-modal';
Expand Down
Loading