- {colorScheme === COLOR_SCHEME.LIGHT ?
:
}
+
+ {colorScheme === COLOR_SCHEME.LIGHT ? (
+
+ ) : (
+
+ )}
-
+
{props.institutionGuid ? (
-
+
) : (
-
+
)}
diff --git a/src/components/Container-test.tsx b/src/components/Container-test.tsx
new file mode 100644
index 0000000000..7e9821df16
--- /dev/null
+++ b/src/components/Container-test.tsx
@@ -0,0 +1,35 @@
+import React from 'react'
+import { describe, it, expect } from 'vitest'
+import { render } from 'src/utilities/testingLibrary'
+import { Container } from 'src/components/Container'
+import { STEPS } from 'src/const/Connect'
+import { initialState } from 'src/services/mockedData'
+
+describe('Container', () => {
+ const preloadedState = initialState
+
+ it('renders', () => {
+ const { container } = render(
+
+ Content
+ ,
+ { preloadedState },
+ )
+
+ const containerDiv = container.querySelector('[data-test="container"]')
+ expect(containerDiv).toBeInTheDocument()
+ expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' })
+ })
+
+ it('applies maxHeight when step is SEARCH', () => {
+ const { container } = render(
+
+ Content
+ ,
+ { preloadedState },
+ )
+
+ const containerDiv = container.querySelector('[data-test="container"]')
+ expect(containerDiv).toHaveStyle({ maxHeight: '100%' })
+ })
+})
diff --git a/src/components/Container.js b/src/components/Container.js
deleted file mode 100644
index 51d48c7c1c..0000000000
--- a/src/components/Container.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-
-import { useTokens } from '@kyper/tokenprovider'
-
-import { STEPS } from 'src/const/Connect'
-/**
- * Our root container to handle our widgets min/max widths, positioning and padding for all views
- */
-export const Container = (props) => {
- const tokens = useTokens()
- const styles = getStyles(tokens, props.step)
-
- return (
-
- )
-}
-Container.propTypes = {
- step: PropTypes.string,
-}
-
-const getStyles = (tokens, step) => {
- return {
- container: {
- backgroundColor: tokens.BackgroundColor.Container,
- minHeight: '100%',
- maxHeight: step === STEPS.SEARCH ? '100%' : null,
- display: 'flex',
- justifyContent: 'center',
- },
- content: {
- maxWidth: '400px', // Our max content width (does not include side margin)
- minWidth: '270px', // Our min content width (does not include side margin)
- width: '100%', // We want this container to shrink and grow between our min-max
- margin: tokens.Spacing.Large,
- },
- }
-}
diff --git a/src/components/Container.tsx b/src/components/Container.tsx
index 7fa7c9f0ae..b44d8766a7 100644
--- a/src/components/Container.tsx
+++ b/src/components/Container.tsx
@@ -1,9 +1,11 @@
import React from 'react'
import { useTokens } from '@kyper/tokenprovider'
+import { STEPS } from 'src/const/Connect'
interface ContainerProps {
children?: React.ReactNode
+ step?: string
}
/**
@@ -11,7 +13,7 @@ interface ContainerProps {
*/
export const Container: React.FC
= (props) => {
const tokens = useTokens()
- const styles = getStyles(tokens)
+ const styles = getStyles(tokens, props.step)
return (
@@ -21,11 +23,12 @@ export const Container: React.FC
= (props) => {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const getStyles = (tokens: any) => {
+const getStyles = (tokens: any, step?: string) => {
return {
container: {
backgroundColor: tokens.BackgroundColor.Container,
minHeight: '100%',
+ maxHeight: step === STEPS.SEARCH ? '100%' : undefined,
display: 'flex',
justifyContent: 'center',
},
diff --git a/src/components/DeleteMemberSurvey-test.tsx b/src/components/DeleteMemberSurvey-test.tsx
new file mode 100644
index 0000000000..8126183264
--- /dev/null
+++ b/src/components/DeleteMemberSurvey-test.tsx
@@ -0,0 +1,250 @@
+import React from 'react'
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, waitFor } from 'src/utilities/testingLibrary'
+import { DeleteMemberSurvey, DELETE_REASONS } from 'src/components/DeleteMemberSurvey'
+import { initialState, CONNECTED_MEMBER } from 'src/services/mockedData'
+import userEvent from '@testing-library/user-event'
+import { apiValue as mockApiValue } from 'src/const/apiProviderMock'
+import { ReadableStatuses } from 'src/const/Statuses'
+
+describe('DeleteMemberSurvey', () => {
+ const preloadedState = initialState
+
+ it('does not render when isOpen is false', () => {
+ const { container } = render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders when isOpen is true', () => {
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ expect(screen.getByText('Disconnect institution')).toBeInTheDocument()
+ expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Chase Bank')
+ })
+
+ it('calls onClose when cancel button clicked', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+ render(
+ {}}
+ />,
+ { preloadedState },
+ )
+
+ await user.click(screen.getByTestId('disconnect-cancel-button'))
+
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('shows connected member reasons', () => {
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ expect(screen.getByText(DELETE_REASONS.NO_LONGER_USE_ACCOUNT)).toBeInTheDocument()
+ expect(screen.getByText(DELETE_REASONS.DONT_WANT_SHARE_DATA)).toBeInTheDocument()
+ expect(screen.queryByText(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT)).not.toBeInTheDocument()
+ })
+
+ it('shows non-connected member reasons', () => {
+ const nonConnectedMember = {
+ ...CONNECTED_MEMBER,
+ connection_status: ReadableStatuses.PREVENTED,
+ }
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ expect(screen.getByText(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT)).toBeInTheDocument()
+ expect(screen.getByText(DELETE_REASONS.ACCOUNT_INFORMATION_OLD)).toBeInTheDocument()
+ expect(screen.queryByText(DELETE_REASONS.NO_LONGER_USE_ACCOUNT)).not.toBeInTheDocument()
+ })
+
+ it('shows validation error when no reason selected', async () => {
+ const user = userEvent.setup()
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ await user.click(screen.getByTestId('disconnect-button'))
+
+ await waitFor(() => {
+ expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument()
+ })
+ })
+
+ it('allows selecting a reason', async () => {
+ const user = userEvent.setup()
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ const firstReason = screen.getAllByRole('radio')[0]
+ await user.click(firstReason)
+
+ expect(firstReason).toBeChecked()
+ })
+
+ it('clears validation error after selecting a reason', async () => {
+ const user = userEvent.setup()
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { preloadedState },
+ )
+
+ await user.click(screen.getByTestId('disconnect-button'))
+
+ await waitFor(() => {
+ expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument()
+ })
+
+ const firstReason = screen.getAllByRole('radio')[0]
+ await user.click(firstReason)
+
+ expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument()
+ })
+
+ it('successfully deletes member when reason selected', async () => {
+ const user = userEvent.setup()
+ const deleteMemberSpy = vi.fn(() => Promise.resolve())
+ const onClose = vi.fn()
+ const onMemberDeleted = vi.fn()
+ const apiValue = {
+ ...mockApiValue,
+ deleteMember: deleteMemberSpy,
+ }
+
+ render(
+ ,
+ { apiValue, preloadedState },
+ )
+
+ const firstReason = screen.getAllByRole('radio')[0]
+ await user.click(firstReason)
+ await user.click(screen.getByTestId('disconnect-button'))
+
+ await waitFor(() => {
+ expect(deleteMemberSpy).toHaveBeenCalledWith(CONNECTED_MEMBER)
+ })
+
+ await waitFor(() => {
+ expect(onMemberDeleted).toHaveBeenCalledWith(CONNECTED_MEMBER.guid)
+ expect(onClose).toHaveBeenCalled()
+ })
+ })
+
+ it('shows error message when delete fails', async () => {
+ const user = userEvent.setup()
+ const apiValue = {
+ ...mockApiValue,
+ deleteMember: vi.fn(() => Promise.reject(new Error('Delete failed'))),
+ }
+
+ render(
+ {}}
+ onMemberDeleted={() => {}}
+ />,
+ { apiValue, preloadedState },
+ )
+
+ const firstReason = screen.getAllByRole('radio')[0]
+ await user.click(firstReason)
+ await user.click(screen.getByTestId('disconnect-button'))
+
+ await waitFor(() => {
+ expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument()
+ })
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument()
+ expect(screen.getByTestId('disconnect-error-message')).toBeInTheDocument()
+ })
+
+ it('dismisses error dialog when ok clicked', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+ const apiValue = {
+ ...mockApiValue,
+ deleteMember: vi.fn(() => Promise.reject(new Error('Delete failed'))),
+ }
+
+ render(
+ {}}
+ />,
+ { apiValue, preloadedState },
+ )
+
+ const firstReason = screen.getAllByRole('radio')[0]
+ await user.click(firstReason)
+ await user.click(screen.getByTestId('disconnect-button'))
+
+ await waitFor(() => {
+ expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByTestId('disconnect-ok-button'))
+
+ expect(onClose).toHaveBeenCalled()
+ })
+})
diff --git a/src/components/DeleteMemberSurvey.js b/src/components/DeleteMemberSurvey.js
index 9d8565541f..6b76b5587f 100644
--- a/src/components/DeleteMemberSurvey.js
+++ b/src/components/DeleteMemberSurvey.js
@@ -18,8 +18,18 @@ import useAnalyticsPath from 'src/hooks/useAnalyticsPath'
import { PageviewInfo } from 'src/const/Analytics'
import { ReadableStatuses } from 'src/const/Statuses'
+export const DELETE_REASONS = {
+ NO_LONGER_USE_ACCOUNT: "I no longer use this account or it's not mine",
+ DONT_WANT_SHARE_DATA: "I don't want to share my data",
+ ACCOUNT_INFORMATION_OLD: 'The account information is old or inaccurate',
+ UNABLE_CONNECT_ACCOUNT: 'I am unable to connect this account here',
+ DONT_WANT_TO_USE_APP: "I don't want to use this app",
+ DONT_WANT_ACCOUNT_CONNECTED: "I don't want this account connected here",
+ OTHER_REASON: 'Other',
+}
+
export const DeleteMemberSurvey = (props) => {
- const { member, onCancel, onDeleteSuccess } = props
+ const { isOpen, member, onClose, onMemberDeleted } = props
const containerRef = useRef(null)
useAnalyticsPath(...PageviewInfo.CONNECT_DELETE_MEMBER_SURVEY)
const { api } = useApi()
@@ -32,39 +42,34 @@ export const DeleteMemberSurvey = (props) => {
const tokens = useTokens()
const styles = getStyles(tokens)
- const DELETE_REASONS = {
- NO_LONGER_USE_ACCOUNT: __("I no longer use this account or it's not mine"),
- DONT_WANT_SHARE_DATA: __("I don't want to share my data"),
- ACCOUNT_INFORMATION_OLD: __('The account information is old or inaccurate'),
- UNABLE_CONNECT_ACCOUNT: __('I am unable to connect this account here'),
- DONT_WANT_TO_USE_APP: __("I don't want to use this app"),
- DONT_WANT_ACCOUNT_CONNECTED: __("I don't want this account connected here"),
- OTHER_REASON: __('Other'),
- }
-
const CONNECTED_REASONS = [
- DELETE_REASONS.NO_LONGER_USE_ACCOUNT,
- DELETE_REASONS.DONT_WANT_SHARE_DATA,
- DELETE_REASONS.DONT_WANT_TO_USE_APP,
- DELETE_REASONS.OTHER_REASON,
+ __(DELETE_REASONS.NO_LONGER_USE_ACCOUNT),
+ __(DELETE_REASONS.DONT_WANT_SHARE_DATA),
+ __(DELETE_REASONS.DONT_WANT_TO_USE_APP),
+ __(DELETE_REASONS.OTHER_REASON),
]
const NON_CONECTED_REASONS = [
- DELETE_REASONS.UNABLE_CONNECT_ACCOUNT,
- DELETE_REASONS.ACCOUNT_INFORMATION_OLD,
- DELETE_REASONS.DONT_WANT_ACCOUNT_CONNECTED,
- DELETE_REASONS.OTHER_REASON,
+ __(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT),
+ __(DELETE_REASONS.ACCOUNT_INFORMATION_OLD),
+ __(DELETE_REASONS.DONT_WANT_ACCOUNT_CONNECTED),
+ __(DELETE_REASONS.OTHER_REASON),
]
useEffect(() => {
if (deleteMemberState.loading === false) return () => {}
const request$ = defer(() => api.deleteMember(member)).subscribe(
- () => onDeleteSuccess(member),
+ () => {
+ onMemberDeleted(member.guid)
+ onClose()
+ },
(err) => updateDeleteMemberState({ loading: false, error: err }),
)
return () => request$.unsubscribe()
- }, [deleteMemberState.loading])
+ }, [deleteMemberState.loading, api, member, onMemberDeleted, onClose])
+
+ if (!isOpen || !member) return null
let reasonList
@@ -109,7 +114,7 @@ export const DeleteMemberSurvey = (props) => {