Skip to content

Commit

Permalink
feat(account-settings): Enable component override for <DeleteUser /> (#…
Browse files Browse the repository at this point in the history
…3123)

* Type updates

* add component override to DeleteUser

* Add default components to object

* Update unit tests

* Trigger CI

* Fix imports

* Remove `.only`

* Use nullish coaelescing

* Use override component directly
  • Loading branch information
wlee221 authored Dec 2, 2022
1 parent ec3c007 commit e85e3fa
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function ChangePassword({

/* Subcomponents */
const { CurrentPassword, NewPassword, ConfirmPassword, SubmitButton, Error } =
React.useMemo(() => ({ ...DEFAULTS, ...(components || {}) }), [components]);
React.useMemo(() => ({ ...DEFAULTS, ...(components ?? {}) }), [components]);

/* Event Handlers */
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import { deleteUser, translate, getLogger } from '@aws-amplify/ui';

import { useAuth } from '../../../internal';
import { Flex } from '../../../primitives';
import {
DefaultWarning,
DefaultError,
DefaultSubmitButton,
} from './defaultComponents';
import DEFAULTS from './defaults';
import { DeleteUserProps, DeleteUserState } from './types';

const logger = getLogger('Auth');

function DeleteUser({
components,
onSuccess,
onError,
handleDelete,
Expand All @@ -26,6 +23,12 @@ function DeleteUser({

const { user, isLoading } = useAuth();

// subcomponents
const { Error, SubmitButton, Warning } = React.useMemo(
() => ({ ...DEFAULTS, ...(components ?? {}) }),
[components]
);

const startConfirmation = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setState('CONFIRMATION');
Expand Down Expand Up @@ -67,7 +70,7 @@ function DeleteUser({
runDeleteUser();
}, [runDeleteUser]);

/** Return null if user isn't authenticated in the first place */
// Return null if user isn't authenticated in the first place
if (!user) {
logger.warn('<DeleteUser /> requires user to be authenticated.');
return null;
Expand All @@ -85,22 +88,26 @@ function DeleteUser({

return (
<Flex direction="column">
<DefaultSubmitButton
<SubmitButton
isDisabled={state === 'CONFIRMATION'}
onClick={startConfirmation}
>
{deleteAccountText}
</DefaultSubmitButton>
</SubmitButton>
{state === 'CONFIRMATION' || state === 'DELETING' ? (
<DefaultWarning
<Warning
onCancel={handleCancel}
isDisabled={state === 'DELETING'}
onConfirm={handleConfirmDelete}
/>
) : null}
{errorMessage ? <DefaultError>{errorMessage}</DefaultError> : null}
{errorMessage ? <Error>{errorMessage}</Error> : null}
</Flex>
);
}

DeleteUser.Error = DEFAULTS.Error;
DeleteUser.SubmitButton = DEFAULTS.SubmitButton;
DeleteUser.Warning = DEFAULTS.Warning;

export default DeleteUser;
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import {
render,
screen,
fireEvent,
waitFor,
act,
} from '@testing-library/react';

import * as UIModule from '@aws-amplify/ui';

import { Button, Flex, Heading, Text } from '../../../../primitives';
import { DeleteUserComponents } from '../types';
import DeleteUser from '../DeleteUser';

const user = {} as unknown as UIModule.AmplifyUser;
Expand All @@ -15,6 +23,29 @@ jest.mock('../../../../internal', () => ({

const deleteUserSpy = jest.spyOn(UIModule, 'deleteUser');

function CustomWarning({ onCancel, onConfirm, isDisabled }) {
return (
<Flex direction="column">
<Text variation="warning">Custom Warning Message</Text>
<Button onClick={onCancel}>Back</Button>
<Button variation="primary" onClick={onConfirm} isDisabled={isDisabled}>
Custom Confirm Button
</Button>
</Flex>
);
}

const components: DeleteUserComponents = {
SubmitButton: (props) => <Button {...props}>Custom Delete Button</Button>,
Warning: CustomWarning,
Error: ({ children }) => (
<>
<Heading>Custom Error Message</Heading>
<Text>{children}</Text>
</>
),
};

describe('ChangePassword', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -133,7 +164,88 @@ describe('ChangePassword', () => {

fireEvent.click(confirmDeleteButton);

// submit handling is async, wait for error to be displayed
await waitFor(() => expect(screen.findByText('Mock Error')).toBeDefined());
expect(await screen.findByText('Mock Error')).toBeDefined();
});

it('renders as expected with components overrides', async () => {
const { container } = render(<DeleteUser components={components} />);

const submitButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

expect(submitButton).toBeDefined();
expect(container).toMatchSnapshot();

fireEvent.click(submitButton);

expect(await screen.findByText('Custom Warning Message')).toBeDefined();
});

it('onSuccess is called with component overrides after successful user deletion', async () => {
deleteUserSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<DeleteUser components={components} onSuccess={onSuccess} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

// submit handling is async, wait for onSuccess to be called
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(onSuccess).toBeCalledTimes(1));
});

it('calls deleteUser with expected arguments and component overrides', async () => {
deleteUserSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<DeleteUser components={components} onSuccess={onSuccess} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

expect(deleteUserSpy).toBeCalledWith();
expect(deleteUserSpy).toBeCalledTimes(1);
});

it('error message is displayed with component overrides after unsuccessful submit', async () => {
deleteUserSpy.mockRejectedValue(new Error('Mock Error'));

render(<DeleteUser components={components} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

await screen.findByText('Mock Error');

expect(await screen.findByText('Custom Error Message')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,20 @@ exports[`ChangePassword renders as expected 1`] = `
</div>
</div>
`;

exports[`ChangePassword renders as expected with components overrides 1`] = `
<div>
<div
class="amplify-flex"
style="flex-direction: column;"
>
<button
class="amplify-button amplify-field-group__control"
data-fullwidth="false"
type="button"
>
Custom Delete Button
</button>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { translate } from '@aws-amplify/ui';
import React from 'react';
import { translate } from '@aws-amplify/ui';

import { Card, Flex, Text, Button, Alert } from '../../../primitives';
import { ErrorComponent, SubmitButtonComponent } from '../types';
import { DeleteUserWarningProps } from './types';

export const DefaultSubmitButton: SubmitButtonComponent = ({
children,
...rest
}) => {
return <Button {...rest}>{children}</Button>;
};
import { Button, Card, Flex, Text } from '../../../primitives';
import { DefaultError } from '../shared/Defaults';
import { DeleteUserComponents, WarningComponent } from './types';

export const DefaultWarning = ({
const DefaultWarning: WarningComponent = ({
onCancel,
onConfirm,
isDisabled,
}: DeleteUserWarningProps): JSX.Element => {
}) => {
// translations
// TODO: consolodiate translations to accountSettingsTextUtil
const warningText = translate(
Expand Down Expand Up @@ -46,10 +39,10 @@ export const DefaultWarning = ({
);
};

export const DefaultError: ErrorComponent = ({ children, ...rest }) => {
return (
<Alert variation="error" {...rest}>
{children}
</Alert>
);
const DEFAULTS: Required<DeleteUserComponents> = {
Error: DefaultError,
SubmitButton: Button,
Warning: DefaultWarning,
};

export default DEFAULTS;
25 changes: 21 additions & 4 deletions packages/react/src/components/AccountSettings/DeleteUser/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import React from 'react';

import { AmplifyUser } from '@aws-amplify/ui';

export interface DeleteUserWarningProps {
import { ErrorComponent, SubmitButtonComponent } from '../types';

export interface WarningProps {
/** called when end user cancels account deletion */
onCancel?: () => void;
onCancel: () => void;
/** called when user acknowledges account deletion */
onConfirm?: () => void;
onConfirm: () => void;
/** whether account deletion is in progress */
isDisabled?: boolean;
isDisabled: boolean;
}

export type WarningComponent<Props = {}> = React.ComponentType<
Props & WarningProps
>;

export type DeleteUserState =
| 'IDLE'
| 'CONFIRMATION'
| 'DELETING'
| 'DONE'
| 'ERROR';

export interface DeleteUserComponents {
Error?: ErrorComponent;
SubmitButton?: SubmitButtonComponent;
Warning?: WarningComponent;
}

export interface DeleteUserProps {
/** custom delete user service override */
handleDelete?: (user: AmplifyUser) => Promise<void> | void;
Expand All @@ -25,4 +39,7 @@ export interface DeleteUserProps {

/** callback for unsuccessful user deletion */
onError?: (error: Error) => void;

/** custom component overrides */
components?: DeleteUserComponents;
}

0 comments on commit e85e3fa

Please sign in to comment.