From e85e3fa2fe3056cca0915a50391dcfb1d3762b90 Mon Sep 17 00:00:00 2001 From: William Lee <43682783+wlee221@users.noreply.github.com> Date: Thu, 1 Dec 2022 16:35:24 -0800 Subject: [PATCH] feat(account-settings): Enable component override for (#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 --- .../ChangePassword/ChangePassword.tsx | 2 +- .../AccountSettings/DeleteUser/DeleteUser.tsx | 27 ++-- .../DeleteUser/__tests__/DeleteUser.test.tsx | 118 +++++++++++++++++- .../__snapshots__/DeleteUser.test.tsx.snap | 17 +++ .../{defaultComponents.tsx => defaults.tsx} | 31 ++--- .../AccountSettings/DeleteUser/types.ts | 25 +++- 6 files changed, 183 insertions(+), 37 deletions(-) rename packages/react/src/components/AccountSettings/DeleteUser/{defaultComponents.tsx => defaults.tsx} (63%) diff --git a/packages/react/src/components/AccountSettings/ChangePassword/ChangePassword.tsx b/packages/react/src/components/AccountSettings/ChangePassword/ChangePassword.tsx index fd9df6d516f..42bc539ec35 100644 --- a/packages/react/src/components/AccountSettings/ChangePassword/ChangePassword.tsx +++ b/packages/react/src/components/AccountSettings/ChangePassword/ChangePassword.tsx @@ -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) => { diff --git a/packages/react/src/components/AccountSettings/DeleteUser/DeleteUser.tsx b/packages/react/src/components/AccountSettings/DeleteUser/DeleteUser.tsx index 3abb140b344..d1c9526bdce 100644 --- a/packages/react/src/components/AccountSettings/DeleteUser/DeleteUser.tsx +++ b/packages/react/src/components/AccountSettings/DeleteUser/DeleteUser.tsx @@ -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, @@ -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) => { event.preventDefault(); setState('CONFIRMATION'); @@ -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(' requires user to be authenticated.'); return null; @@ -85,22 +88,26 @@ function DeleteUser({ return ( - {deleteAccountText} - + {state === 'CONFIRMATION' || state === 'DELETING' ? ( - ) : null} - {errorMessage ? {errorMessage} : null} + {errorMessage ? {errorMessage} : null} ); } +DeleteUser.Error = DEFAULTS.Error; +DeleteUser.SubmitButton = DEFAULTS.SubmitButton; +DeleteUser.Warning = DEFAULTS.Warning; + export default DeleteUser; diff --git a/packages/react/src/components/AccountSettings/DeleteUser/__tests__/DeleteUser.test.tsx b/packages/react/src/components/AccountSettings/DeleteUser/__tests__/DeleteUser.test.tsx index 7cf5f290d6f..295ffc6d9dc 100644 --- a/packages/react/src/components/AccountSettings/DeleteUser/__tests__/DeleteUser.test.tsx +++ b/packages/react/src/components/AccountSettings/DeleteUser/__tests__/DeleteUser.test.tsx @@ -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; @@ -15,6 +23,29 @@ jest.mock('../../../../internal', () => ({ const deleteUserSpy = jest.spyOn(UIModule, 'deleteUser'); +function CustomWarning({ onCancel, onConfirm, isDisabled }) { + return ( + + Custom Warning Message + + + + ); +} + +const components: DeleteUserComponents = { + SubmitButton: (props) => , + Warning: CustomWarning, + Error: ({ children }) => ( + <> + Custom Error Message + {children} + + ), +}; + describe('ChangePassword', () => { beforeEach(() => { jest.clearAllMocks(); @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); }); }); diff --git a/packages/react/src/components/AccountSettings/DeleteUser/__tests__/__snapshots__/DeleteUser.test.tsx.snap b/packages/react/src/components/AccountSettings/DeleteUser/__tests__/__snapshots__/DeleteUser.test.tsx.snap index f87ef48031d..b4fda13e703 100644 --- a/packages/react/src/components/AccountSettings/DeleteUser/__tests__/__snapshots__/DeleteUser.test.tsx.snap +++ b/packages/react/src/components/AccountSettings/DeleteUser/__tests__/__snapshots__/DeleteUser.test.tsx.snap @@ -16,3 +16,20 @@ exports[`ChangePassword renders as expected 1`] = ` `; + +exports[`ChangePassword renders as expected with components overrides 1`] = ` +
+
+ +
+
+`; diff --git a/packages/react/src/components/AccountSettings/DeleteUser/defaultComponents.tsx b/packages/react/src/components/AccountSettings/DeleteUser/defaults.tsx similarity index 63% rename from packages/react/src/components/AccountSettings/DeleteUser/defaultComponents.tsx rename to packages/react/src/components/AccountSettings/DeleteUser/defaults.tsx index f27ee32fd4b..070984c1f2e 100644 --- a/packages/react/src/components/AccountSettings/DeleteUser/defaultComponents.tsx +++ b/packages/react/src/components/AccountSettings/DeleteUser/defaults.tsx @@ -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 ; -}; +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( @@ -46,10 +39,10 @@ export const DefaultWarning = ({ ); }; -export const DefaultError: ErrorComponent = ({ children, ...rest }) => { - return ( - - {children} - - ); +const DEFAULTS: Required = { + Error: DefaultError, + SubmitButton: Button, + Warning: DefaultWarning, }; + +export default DEFAULTS; diff --git a/packages/react/src/components/AccountSettings/DeleteUser/types.ts b/packages/react/src/components/AccountSettings/DeleteUser/types.ts index 6962f39bba0..d1208c16b8b 100644 --- a/packages/react/src/components/AccountSettings/DeleteUser/types.ts +++ b/packages/react/src/components/AccountSettings/DeleteUser/types.ts @@ -1,14 +1,22 @@ +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 = React.ComponentType< + Props & WarningProps +>; + export type DeleteUserState = | 'IDLE' | 'CONFIRMATION' @@ -16,6 +24,12 @@ export type DeleteUserState = | 'DONE' | 'ERROR'; +export interface DeleteUserComponents { + Error?: ErrorComponent; + SubmitButton?: SubmitButtonComponent; + Warning?: WarningComponent; +} + export interface DeleteUserProps { /** custom delete user service override */ handleDelete?: (user: AmplifyUser) => Promise | void; @@ -25,4 +39,7 @@ export interface DeleteUserProps { /** callback for unsuccessful user deletion */ onError?: (error: Error) => void; + + /** custom component overrides */ + components?: DeleteUserComponents; }