From 96ded32afb562cafe2301d4b726f7f3d76754427 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 11:20:10 -0600 Subject: [PATCH 1/7] fix: added components tests to increase coverage to 80% --- src/components/Container.js | 40 -- src/components/Container.tsx | 7 +- .../ConnectInstitutionHeader-test.tsx | 108 +++ src/components/__tests__/Container-test.tsx | 262 +++++++ .../__tests__/DeleteMemberSurvey-test.tsx | 345 ++++++++++ .../__tests__/DetailReviewItem-test.tsx | 253 +++++++ .../__tests__/RenderConnectStep-test.jsx | 648 +++++++++++++++++- src/services/mockedData.ts | 27 + src/views/disclosure/Disclosure.js | 2 +- 9 files changed, 1629 insertions(+), 63 deletions(-) delete mode 100644 src/components/Container.js create mode 100644 src/components/__tests__/ConnectInstitutionHeader-test.tsx create mode 100644 src/components/__tests__/Container-test.tsx create mode 100644 src/components/__tests__/DeleteMemberSurvey-test.tsx create mode 100644 src/components/__tests__/DetailReviewItem-test.tsx 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 ( -
-
{props.children}
-
- ) -} -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/__tests__/ConnectInstitutionHeader-test.tsx b/src/components/__tests__/ConnectInstitutionHeader-test.tsx new file mode 100644 index 0000000000..95c0b0e271 --- /dev/null +++ b/src/components/__tests__/ConnectInstitutionHeader-test.tsx @@ -0,0 +1,108 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render } from 'src/utilities/testingLibrary' +import { ConnectInstitutionHeader } from 'src/components/ConnectInstitutionHeader' +import { COLOR_SCHEME } from 'src/const/Connect' +import { initialState } from 'src/services/mockedData' + +describe('ConnectInstitutionHeader', () => { + const createPreloadedState = (colorScheme: string) => ({ + ...initialState, + config: { + ...initialState.config, + color_scheme: colorScheme, + }, + }) + + describe('rendering', () => { + it('renders the header container with correct data-test attribute', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + }) + + it('renders SVG elements for the header graphics', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + }) + + describe('color scheme', () => { + it('renders with light mode color scheme', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders with dark mode color scheme', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + }) + + describe('institution logo', () => { + it('renders with institutionGuid provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const institutionGuid = 'INS-12345' + + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders without institutionGuid', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders with undefined institutionGuid', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders all elements together in light mode with institution', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const institutionGuid = 'INS-BANK-001' + + const { container } = render(, { + preloadedState, + }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + + it('renders all elements together in dark mode without institution', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + + const { container } = render(, { preloadedState }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/src/components/__tests__/Container-test.tsx b/src/components/__tests__/Container-test.tsx new file mode 100644 index 0000000000..3314dedb2d --- /dev/null +++ b/src/components/__tests__/Container-test.tsx @@ -0,0 +1,262 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { Container } from 'src/components/Container.tsx' +import { STEPS } from 'src/const/Connect' +import { initialState } from 'src/services/mockedData' + +describe('Container', () => { + const preloadedState = initialState + + describe('rendering', () => { + it('renders the container with correct data-test attribute', () => { + const { container } = render( + +
Test Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + + it('renders children content', () => { + render( + +
Test Content
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('child-content')).toBeInTheDocument() + expect(screen.getByText('Test Content')).toBeInTheDocument() + }) + + it('renders multiple children', () => { + render( + +
First Child
+
Second Child
+ Third Child +
, + { preloadedState }, + ) + + expect(screen.getByTestId('child-1')).toBeInTheDocument() + expect(screen.getByTestId('child-2')).toBeInTheDocument() + expect(screen.getByTestId('child-3')).toBeInTheDocument() + }) + + it('renders without children', () => { + const { container } = render(, { preloadedState }) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + + it('renders with null children', () => { + const { container } = render({null}, { preloadedState }) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + }) + + describe('step prop', () => { + it('renders without step prop', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders with SEARCH step and applies maxHeight', () => { + const { container } = render( + +
Search Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders with non-SEARCH step without maxHeight constraint', () => { + const { container } = render( + +
Connected Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders correctly with ENTER_CREDENTIALS step', () => { + render( + +
Enter Credentials
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('credentials-content')).toBeInTheDocument() + }) + + it('renders correctly with MFA step', () => { + render( + +
MFA Content
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('mfa-content')).toBeInTheDocument() + }) + }) + + describe('styling', () => { + it('applies consistent container styles', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ + minHeight: '100%', + display: 'flex', + justifyContent: 'center', + }) + }) + + it('has a content wrapper with proper constraints', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + const contentWrapper = containerDiv?.firstChild as HTMLElement + + expect(contentWrapper).toBeInTheDocument() + expect(contentWrapper).toHaveStyle({ + maxWidth: '400px', + minWidth: '270px', + width: '100%', + }) + }) + + it('applies background color from tokens', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ backgroundColor: expect.any(String) }) + }) + }) + + describe('integration', () => { + it('renders complete structure with SEARCH step', () => { + const { container } = render( + +
Search for institution
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) + expect(screen.getByTestId('search-content')).toBeInTheDocument() + expect(screen.getByText('Search for institution')).toBeInTheDocument() + }) + + it('renders complete structure with CONNECTED step', () => { + const { container } = render( + +
Successfully connected!
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(screen.getByTestId('success-message')).toBeInTheDocument() + expect(screen.getByText('Successfully connected!')).toBeInTheDocument() + }) + + it('renders nested component structure', () => { + render( + +
+

Title

+
+

Nested Content

+
+
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('outer')).toBeInTheDocument() + expect(screen.getByTestId('inner')).toBeInTheDocument() + expect(screen.getByText('Title')).toBeInTheDocument() + expect(screen.getByText('Nested Content')).toBeInTheDocument() + }) + + it('maintains structure with form elements', () => { + render( + +
+ + +
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('test-form')).toBeInTheDocument() + expect(screen.getByTestId('test-input')).toBeInTheDocument() + expect(screen.getByTestId('test-button')).toBeInTheDocument() + }) + + it('wraps components consistently regardless of content type', () => { + const { container } = render( + +
+ Text + test + +
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(screen.getByText('Text')).toBeInTheDocument() + expect(screen.getByAltText('test')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/__tests__/DeleteMemberSurvey-test.tsx b/src/components/__tests__/DeleteMemberSurvey-test.tsx new file mode 100644 index 0000000000..3e44e1c844 --- /dev/null +++ b/src/components/__tests__/DeleteMemberSurvey-test.tsx @@ -0,0 +1,345 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { DeleteMemberSurvey } from 'src/components/DeleteMemberSurvey' +import { initialState, CONNECTED_MEMBER, NON_CONNECTED_MEMBER } from 'src/services/mockedData' +import userEvent from '@testing-library/user-event' + +describe('DeleteMemberSurvey', () => { + const preloadedState = initialState + + const mockOnCancel = vi.fn() + const mockOnDeleteSuccess = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('rendering', () => { + it('renders the disconnect institution dialog', () => { + const { container } = render( + , + { preloadedState }, + ) + + const dialog = container.querySelector('[role="dialog"]') + expect(dialog).toBeInTheDocument() + }) + + it('renders the disconnect institution heading', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + }) + + it('renders the disclaimer with member name', () => { + render( + , + { preloadedState }, + ) + + const disclaimer = screen.getByTestId('disconnect-disclaimer') + expect(disclaimer).toBeInTheDocument() + expect(disclaimer.textContent).toContain('Chase Bank') + }) + + it('renders disconnect and cancel buttons', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + }) + + it('renders required field indicator', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Required')).toBeInTheDocument() + }) + }) + + describe('connected member reasons', () => { + it('renders correct reasons for connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() + expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() + expect(screen.getByText("I don't want to use this app")).toBeInTheDocument() + expect(screen.getByText('Other')).toBeInTheDocument() + }) + + it('does not render non-connected reasons for connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() + expect( + screen.queryByText('The account information is old or inaccurate'), + ).not.toBeInTheDocument() + expect(screen.queryByText("I don't want this account connected here")).not.toBeInTheDocument() + }) + }) + + describe('non-connected member reasons', () => { + it('renders correct reasons for non-connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() + expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() + expect(screen.getByText("I don't want this account connected here")).toBeInTheDocument() + expect(screen.getByText('Other')).toBeInTheDocument() + }) + + it('does not render connected-only reasons for non-connected member', () => { + render( + , + { preloadedState }, + ) + + expect( + screen.queryByText("I no longer use this account or it's not mine"), + ).not.toBeInTheDocument() + expect(screen.queryByText("I don't want to share my data")).not.toBeInTheDocument() + expect(screen.queryByText("I don't want to use this app")).not.toBeInTheDocument() + }) + }) + + describe('user interactions', () => { + it('calls onCancel when cancel button is clicked', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-cancel-button')) + + expect(mockOnCancel).toHaveBeenCalledTimes(1) + }) + + it('allows selecting a reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + expect(options[0]).toBeChecked() + }) + + it('allows changing selected reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + expect(options[0]).toBeChecked() + + await user.click(options[1]) + expect(options[1]).toBeChecked() + expect(options[0]).not.toBeChecked() + }) + }) + + describe('form validation', () => { + it('shows validation error when disconnect clicked without selecting reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + }) + + it('does not show validation error before first submit attempt', () => { + render( + , + { preloadedState }, + ) + + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + + it('validation error disappears after selecting a reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + await waitFor(() => { + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + }) + }) + + describe('delete member flow', () => { + it('initiates delete when disconnect clicked with valid selection', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + await user.click(screen.getByTestId('disconnect-button')) + + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders complete structure for connected member', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-disclaimer')).toBeInTheDocument() + expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) + expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + }) + + it('renders complete structure for non-connected member', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Wells Fargo') + expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) + }) + + it('handles complete user flow from selection to cancel', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + expect(options[0]).toBeChecked() + + await user.click(screen.getByTestId('disconnect-cancel-button')) + expect(mockOnCancel).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/components/__tests__/DetailReviewItem-test.tsx b/src/components/__tests__/DetailReviewItem-test.tsx new file mode 100644 index 0000000000..6ff3d4bed9 --- /dev/null +++ b/src/components/__tests__/DetailReviewItem-test.tsx @@ -0,0 +1,253 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { DetailReviewItem } from 'src/components/DetailReviewItem' +import { initialState } from 'src/services/mockedData' +import userEvent from '@testing-library/user-event' + +describe('DetailReviewItem', () => { + const preloadedState = initialState + + const defaultProps = { + label: 'Email', + value: 'user@example.com', + ariaButtonLabel: 'Edit email', + isEditable: false, + onEditClick: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('rendering', () => { + it('renders the label', () => { + render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + }) + + it('renders the value', () => { + render(, { preloadedState }) + + expect(screen.getByText('user@example.com')).toBeInTheDocument() + }) + + it('renders with correct data-test attributes for label', () => { + const { container } = render(, { preloadedState }) + + const labelElement = container.querySelector('[data-test="Email-row"]') + expect(labelElement).toBeInTheDocument() + }) + + it('renders with correct data-test attributes for value', () => { + const { container } = render(, { preloadedState }) + + const valueElement = container.querySelector('[data-test="user@example.com-row"]') + expect(valueElement).toBeInTheDocument() + }) + + it('renders edit button with correct aria-label', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeInTheDocument() + }) + + it('renders edit icon', () => { + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="Email-edit-button"]')).toBeInTheDocument() + }) + + it('sanitizes label with spaces for data-test attribute', () => { + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="Full-Name-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="Full-Name-edit-button"]')).toBeInTheDocument() + }) + }) + + describe('edit button functionality', () => { + it('calls onEditClick when edit button is clicked', async () => { + const user = userEvent.setup() + const mockOnEditClick = vi.fn() + + render(, { + preloadedState, + }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + await user.click(button) + + expect(mockOnEditClick).toHaveBeenCalledTimes(1) + }) + + it('enables edit button when isEditable is false', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeEnabled() + }) + + it('disables edit button when isEditable is true', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeDisabled() + }) + }) + + describe('different content types', () => { + it('renders with phone number', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Phone')).toBeInTheDocument() + expect(screen.getByText('555-123-4567')).toBeInTheDocument() + }) + + it('renders with address', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Address')).toBeInTheDocument() + expect(screen.getByText('123 Main St, City, ST 12345')).toBeInTheDocument() + }) + + it('renders with date', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Date of Birth')).toBeInTheDocument() + expect(screen.getByText('01/01/1990')).toBeInTheDocument() + }) + + it('renders with long text value', () => { + const longValue = + 'This is a very long value that might wrap to multiple lines depending on the container width' + + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Description')).toBeInTheDocument() + expect(screen.getByText(longValue)).toBeInTheDocument() + }) + }) + + describe('data-test attribute handling', () => { + it('handles special characters in label', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[data-test="First-&-Last-Name-row"]')).toBeInTheDocument() + expect( + container.querySelector('[data-test="First-&-Last-Name-edit-button"]'), + ).toBeInTheDocument() + }) + + it('handles special characters in value', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[data-test="user+test@example.com-row"]')).toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders complete structure with all elements', () => { + const { container } = render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + + expect(screen.getByText('user@example.com')).toBeInTheDocument() + + expect(screen.getByRole('button', { name: 'Edit email' })).toBeInTheDocument() + + expect(container.querySelector('[data-test="Email-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="user@example.com-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="Email-edit-button"]')).toBeInTheDocument() + }) + + it('handles complete user interaction flow', async () => { + const user = userEvent.setup() + const mockOnEditClick = vi.fn() + + render(, { + preloadedState, + }) + + expect(screen.getByText('Email')).toBeInTheDocument() + expect(screen.getByText('user@example.com')).toBeInTheDocument() + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeEnabled() + + await user.click(button) + + expect(mockOnEditClick).toHaveBeenCalledTimes(1) + }) + + it('renders correctly with multiple items scenario', () => { + const { rerender } = render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('Phone')).toBeInTheDocument() + expect(screen.getByText('555-1234')).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/__tests__/RenderConnectStep-test.jsx b/src/components/__tests__/RenderConnectStep-test.jsx index 6207b671f2..dbebc59c14 100644 --- a/src/components/__tests__/RenderConnectStep-test.jsx +++ b/src/components/__tests__/RenderConnectStep-test.jsx @@ -1,16 +1,19 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' import RenderConnectStep from 'src/components/RenderConnectStep' -import { STEPS } from 'src/const/Connect' +import { VERIFY_MODE, STEPS } from 'src/const/Connect' import { createRenderConnectStepInitialState } from 'src/utilities/test/createRenderConnectStepInitialState' +import { initialState } from 'src/services/mockedData' describe('RenderConnectStep', () => { const defaultProps = { availableAccountTypes: [], handleConsentGoBack: vi.fn(), handleCredentialsGoBack: vi.fn(), + handleOAuthGoBack: vi.fn(), navigationRef: React.createRef(), onManualAccountAdded: vi.fn(), + onSuccessfulAggregation: vi.fn(), onUpsertMember: vi.fn(), setConnectLocalState: vi.fn(), } @@ -23,30 +26,635 @@ describe('RenderConnectStep', () => { url: 'https://testbank.com', } - it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { - const initialState = createRenderConnectStepInitialState( - STEPS.DEMO_CONNECT_GUARD, - mockInstitution, - ) + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Step Rendering', () => { + it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { + const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + expect(screen.getByText('Demo mode active')).toBeInTheDocument() + expect( + screen.getByText(/Live institutions are not available in the demo environment/i), + ).toBeInTheDocument() + expect(screen.getByText('MX Bank')).toBeInTheDocument() + + const logo = screen.getByAltText('Logo for Test Bank') + expect(logo).toBeInTheDocument() + expect(logo).toHaveAttribute('src', mockInstitution.logo_url) + + const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') + expect(errorIcon).toBeInTheDocument() + + const button = screen.getByRole('button', { name: /return to institution selection/i }) + expect(button).toBeInTheDocument() + }) + + it('should render Search view for SEARCH step', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render Connecting view for CONNECTING step', () => { + const state = createRenderConnectStepInitialState(STEPS.CONNECTING, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render Disclosure view for DISCLOSURE step', () => { + const state = createRenderConnectStepInitialState(STEPS.DISCLOSURE) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render InstitutionStatusDetails for INSTITUTION_STATUS_DETAILS step', () => { + const state = createRenderConnectStepInitialState( + STEPS.INSTITUTION_STATUS_DETAILS, + mockInstitution, + ) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render DynamicDisclosure for CONSENT step', () => { + const state = createRenderConnectStepInitialState(STEPS.CONSENT, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render ManualAccountConnect for ADD_MANUAL_ACCOUNT step', () => { + const state = createRenderConnectStepInitialState(STEPS.ADD_MANUAL_ACCOUNT) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render MFAStep for MFA step', () => { + const state = createRenderConnectStepInitialState(STEPS.MFA, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render VerifyExistingMember for VERIFY_EXISTING_MEMBER step', () => { + const state = createRenderConnectStepInitialState(STEPS.VERIFY_EXISTING_MEMBER) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render VerifyError for VERIFY_ERROR step', () => { + const state = createRenderConnectStepInitialState(STEPS.VERIFY_ERROR) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it.skip('should render Connected for CONNECTED step', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member' } + const state = { + ...createRenderConnectStepInitialState(STEPS.CONNECTED, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.CONNECTED, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + // Just verify it renders without error - confetti testing is in Connected component tests + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render DeleteMemberSuccess for DELETE_MEMBER_SUCCESS step', () => { + const state = createRenderConnectStepInitialState( + STEPS.DELETE_MEMBER_SUCCESS, + mockInstitution, + ) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render OAuthError for OAUTH_ERROR step', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member' } + const state = { + ...createRenderConnectStepInitialState(STEPS.OAUTH_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.OAUTH_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should default to SEARCH step when location is empty', () => { + const state = { + ...initialState, + connect: { + ...initialState.connect, + location: [], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Styling', () => { + it('should apply maxHeight for SEARCH step', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv).toHaveStyle({ + maxHeight: 'calc(100% - 60px)', + }) + }) + + it('should not apply maxHeight for non-SEARCH steps', () => { + const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv.style.maxHeight).toBe('') + }) + + it('should apply correct container styles', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv).toHaveStyle({ + display: 'flex', + justifyContent: 'center', + minHeight: 'calc(100% - 60px)', + }) + }) + }) + + describe('Configuration-Dependent Rendering', () => { + it('should render in AGG_MODE by default', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) - const { container } = render(, { - preloadedState: initialState, + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render in VERIFY_MODE when configured', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.SEARCH), + config: { + ...initialState.config, + mode: VERIFY_MODE, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should apply widget profile settings', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.SEARCH), + profiles: { + ...initialState.profiles, + widgetProfile: { + ...initialState.profiles.widgetProfile, + enable_support_requests: false, + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Integration', () => { + it('should render complete component with all providers', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should handle step navigation', () => { + const state1 = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container, rerender } = render(, { + preloadedState: state1, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + + rerender() }) - expect(screen.getByText('Demo mode active')).toBeInTheDocument() - expect( - screen.getByText(/Live institutions are not available in the demo environment/i), - ).toBeInTheDocument() - expect(screen.getByText('MX Bank')).toBeInTheDocument() + it('should pass props correctly to views', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) - const logo = screen.getByAltText('Logo for Test Bank') - expect(logo).toBeInTheDocument() - expect(logo).toHaveAttribute('src', mockInstitution.logo_url) + const { container } = render(, { + preloadedState: state, + }) - const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') - expect(errorIcon).toBeInTheDocument() + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should handle missing optional props gracefully', () => { + const minimalProps = { + handleConsentGoBack: vi.fn(), + handleCredentialsGoBack: vi.fn(), + handleOAuthGoBack: vi.fn(), + navigationRef: React.createRef(), + setConnectLocalState: vi.fn(), + } + + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Error Handling', () => { + it('should handle invalid step gracefully', () => { + const state = { + ...initialState, + connect: { + ...initialState.connect, + location: [{ step: 'INVALID_STEP' }], + }, + } + + render(, { + preloadedState: state, + }) + expect(true).toBe(true) + }) + }) + + describe('ENTER_CREDENTIALS Step Variations', () => { + it.skip('should render OAuthStep when institution supports OAuth', () => { + const oauthInstitution = { ...mockInstitution, supports_oauth: true } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, oauthInstitution), + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + uses_oauth: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, oauthInstitution).connect, + selectedInstitution: oauthInstitution, + updateCredentials: false, + selectedInstructionalData: { + title: 'Log in at Test Bank', + description: 'Connect your account', + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render UpdateMemberForm when updateCredentials is true', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + updateCredentials: true, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) - const button = screen.getByRole('button', { name: /return to institution selection/i }) - expect(button).toBeInTheDocument() + it('should render CreateMemberForm when updateCredentials is false', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + updateCredentials: false, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it.skip('should render OAuthStep when current member is OAuth', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member', is_oauth: true } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + uses_oauth: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + updateCredentials: false, + selectedInstructionalData: { + title: 'Log in at Test Bank', + description: 'Connect your account', + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('MICRODEPOSITS Step', () => { + it('should render Microdeposits when enabled in verification mode', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS), + config: { + ...initialState.config, + mode: VERIFY_MODE, + }, + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + account_verification_is_enabled: true, + is_microdeposits_enabled: true, + }, + widgetProfile: { + ...initialState.profiles.widgetProfile, + show_microdeposits_in_connect: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS).connect, + currentMicrodepositGuid: 'MICRO-123', + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should not render Microdeposits when not enabled', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS), + config: { + ...initialState.config, + mode: 'aggregation', + }, + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + account_verification_is_enabled: false, + is_microdeposits_enabled: false, + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('ACTIONABLE_ERROR Step Variations', () => { + it('should render ActionableError when error code can be handled', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: { error_code: 'REQUEST_EXPIRED' }, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render LoginError when error code cannot be handled', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: { error_code: 'UNKNOWN_ERROR' }, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render LoginError when error code is null', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: null, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('ADDITIONAL_PRODUCT Step', () => { + it('should render AdditionalProductStep with valid product option', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ADDITIONAL_PRODUCT, mockInstitution), + config: { + ...initialState.config, + additional_product_option: 'account_verification', + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should throw error for invalid product option', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ADDITIONAL_PRODUCT), + config: { + ...initialState.config, + additional_product_option: 'invalid_option', + }, + } + + expect(() => { + render(, { + preloadedState: state, + }) + }).toThrow('invalid product offer') + }) }) }) diff --git a/src/services/mockedData.ts b/src/services/mockedData.ts index a5f2bc3a9b..31cd3d9a33 100644 --- a/src/services/mockedData.ts +++ b/src/services/mockedData.ts @@ -304,6 +304,33 @@ export const memberCredentialsData = { }, ], } + +export const CONNECTED_MEMBER = { + guid: 'MBR-123', + name: 'Chase Bank', + connection_status: 6, + aggregation_status: 1, + institution_guid: 'INS-123', + user_guid: 'USR-123', + is_being_aggregated: false, + is_manual: false, + is_managed_by_user: true, + is_oauth: false, +} + +export const NON_CONNECTED_MEMBER = { + guid: 'MBR-456', + name: 'Wells Fargo', + connection_status: 1, + aggregation_status: 0, + institution_guid: 'INS-456', + user_guid: 'USR-123', + is_being_aggregated: false, + is_manual: false, + is_managed_by_user: true, + is_oauth: false, +} + export const CONNECTED_MEMBERS = [ { aggregation_status: 1, diff --git a/src/views/disclosure/Disclosure.js b/src/views/disclosure/Disclosure.js index 2030ccf153..ce68617efe 100644 --- a/src/views/disclosure/Disclosure.js +++ b/src/views/disclosure/Disclosure.js @@ -29,6 +29,7 @@ import { goToUrlLink } from 'src/utilities/global' export const Disclosure = React.forwardRef((_, disclosureRef) => { const containerRef = useRef(null) useAnalyticsPath(...PageviewInfo.CONNECT_DISCLOSURE) + const size = useSelector(getSize) const isSmall = size === 'small' const tokens = useTokens() const styles = getStyles(tokens, isSmall) @@ -37,7 +38,6 @@ export const Disclosure = React.forwardRef((_, disclosureRef) => { // Redux const { isInAggMode, isInTaxMode, isInVerifyMode } = useSelector(selectCurrentMode) const connectConfig = useSelector(selectConnectConfig) - const size = useSelector(getSize) const showExternalLinkPopup = useSelector( (state) => state.profiles.clientProfile.show_external_link_popup, ) From bff1c584eb246af8eb6e31975d8d2331ebdaa8e8 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 13:27:47 -0600 Subject: [PATCH 2/7] moved test files by the files they are testing --- .../{__tests__ => }/ConnectInstitutionHeader-test.tsx | 0 src/components/{__tests__ => }/Container-test.tsx | 2 +- src/components/{__tests__ => }/DeleteMemberSurvey-test.tsx | 0 src/components/{__tests__ => }/DetailReviewItem-test.tsx | 0 src/components/{__tests__ => }/RenderConnectStep-test.jsx | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename src/components/{__tests__ => }/ConnectInstitutionHeader-test.tsx (100%) rename src/components/{__tests__ => }/Container-test.tsx (99%) rename src/components/{__tests__ => }/DeleteMemberSurvey-test.tsx (100%) rename src/components/{__tests__ => }/DetailReviewItem-test.tsx (100%) rename src/components/{__tests__ => }/RenderConnectStep-test.jsx (100%) diff --git a/src/components/__tests__/ConnectInstitutionHeader-test.tsx b/src/components/ConnectInstitutionHeader-test.tsx similarity index 100% rename from src/components/__tests__/ConnectInstitutionHeader-test.tsx rename to src/components/ConnectInstitutionHeader-test.tsx diff --git a/src/components/__tests__/Container-test.tsx b/src/components/Container-test.tsx similarity index 99% rename from src/components/__tests__/Container-test.tsx rename to src/components/Container-test.tsx index 3314dedb2d..aafcea129e 100644 --- a/src/components/__tests__/Container-test.tsx +++ b/src/components/Container-test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { describe, it, expect } from 'vitest' import { render, screen } from 'src/utilities/testingLibrary' -import { Container } from 'src/components/Container.tsx' +import { Container } from 'src/components/Container' import { STEPS } from 'src/const/Connect' import { initialState } from 'src/services/mockedData' diff --git a/src/components/__tests__/DeleteMemberSurvey-test.tsx b/src/components/DeleteMemberSurvey-test.tsx similarity index 100% rename from src/components/__tests__/DeleteMemberSurvey-test.tsx rename to src/components/DeleteMemberSurvey-test.tsx diff --git a/src/components/__tests__/DetailReviewItem-test.tsx b/src/components/DetailReviewItem-test.tsx similarity index 100% rename from src/components/__tests__/DetailReviewItem-test.tsx rename to src/components/DetailReviewItem-test.tsx diff --git a/src/components/__tests__/RenderConnectStep-test.jsx b/src/components/RenderConnectStep-test.jsx similarity index 100% rename from src/components/__tests__/RenderConnectStep-test.jsx rename to src/components/RenderConnectStep-test.jsx From 228dc514074b95ad87dcd2e7f2797b5f54415fae Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 15:09:37 -0600 Subject: [PATCH 3/7] simplified insitution header test --- .../ConnectInstitutionHeader-test.tsx | 104 +++++------------- src/components/ConnectInstitutionHeader.js | 14 ++- 2 files changed, 35 insertions(+), 83 deletions(-) diff --git a/src/components/ConnectInstitutionHeader-test.tsx b/src/components/ConnectInstitutionHeader-test.tsx index 95c0b0e271..e0bda198d5 100644 --- a/src/components/ConnectInstitutionHeader-test.tsx +++ b/src/components/ConnectInstitutionHeader-test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { describe, it, expect } from 'vitest' -import { render } from 'src/utilities/testingLibrary' +import { render, screen } from 'src/utilities/testingLibrary' import { ConnectInstitutionHeader } from 'src/components/ConnectInstitutionHeader' import { COLOR_SCHEME } from 'src/const/Connect' import { initialState } from 'src/services/mockedData' @@ -14,95 +14,43 @@ describe('ConnectInstitutionHeader', () => { }, }) - describe('rendering', () => { - it('renders the header container with correct data-test attribute', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const { container } = render(, { preloadedState }) + it('renders light backdrop when color scheme is LIGHT', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) - const header = container.querySelector('[data-test="disclosure-svg-header"]') - expect(header).toBeInTheDocument() - }) - - it('renders SVG elements for the header graphics', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const { container } = render(, { preloadedState }) - - const svgs = container.querySelectorAll('svg') - expect(svgs.length).toBeGreaterThan(0) - }) + expect(screen.getByTestId('backdrop-light')).toBeInTheDocument() + expect(screen.queryByTestId('backdrop-dark')).not.toBeInTheDocument() }) - describe('color scheme', () => { - it('renders with light mode color scheme', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const { container } = render(, { preloadedState }) - - expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() - }) + it('renders dark backdrop when color scheme is DARK', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + render(, { preloadedState }) - it('renders with dark mode color scheme', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) - const { container } = render(, { preloadedState }) - - expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() - }) + expect(screen.getByTestId('backdrop-dark')).toBeInTheDocument() + expect(screen.queryByTestId('backdrop-light')).not.toBeInTheDocument() }) - describe('institution logo', () => { - it('renders with institutionGuid provided', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const institutionGuid = 'INS-12345' - - const { container } = render(, { - preloadedState, - }) - - expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() - }) - - it('renders without institutionGuid', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const { container } = render(, { preloadedState }) - - expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() - }) - - it('renders with undefined institutionGuid', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const { container } = render(, { - preloadedState, - }) + it('renders HeaderDevice', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) - expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() - }) + expect(screen.getByTestId('device-container')).toBeInTheDocument() }) - describe('integration', () => { - it('renders all elements together in light mode with institution', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) - const institutionGuid = 'INS-BANK-001' - - const { container } = render(, { - preloadedState, - }) - - const header = container.querySelector('[data-test="disclosure-svg-header"]') - expect(header).toBeInTheDocument() - - const svgs = container.querySelectorAll('svg') - expect(svgs.length).toBeGreaterThan(0) + it('renders InstitutionLogo when institutionGuid is provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { + preloadedState, }) - it('renders all elements together in dark mode without institution', () => { - const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) - - const { container } = render(, { preloadedState }) + expect(screen.getByTestId('institution-logo-container')).toBeInTheDocument() + expect(screen.queryByTestId('default-institution-icon')).not.toBeInTheDocument() + }) - const header = container.querySelector('[data-test="disclosure-svg-header"]') - expect(header).toBeInTheDocument() + it('renders default institution icon when institutionGuid is not provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) - const svgs = container.querySelectorAll('svg') - expect(svgs.length).toBeGreaterThan(0) - }) + expect(screen.getByTestId('default-institution-icon')).toBeInTheDocument() }) }) diff --git a/src/components/ConnectInstitutionHeader.js b/src/components/ConnectInstitutionHeader.js index 6539ad5098..d03cec0588 100644 --- a/src/components/ConnectInstitutionHeader.js +++ b/src/components/ConnectInstitutionHeader.js @@ -22,16 +22,20 @@ export const ConnectInstitutionHeader = (props) => { return (
-
- {colorScheme === COLOR_SCHEME.LIGHT ? : } -
+
+ {colorScheme === COLOR_SCHEME.LIGHT ? ( + + ) : ( + + )} +
-
+
{props.institutionGuid ? ( ) : ( - + )}
From 12097d5fa1388d096c2cf1971b8f78d57b2f04e6 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 15:17:04 -0600 Subject: [PATCH 4/7] simplified container test --- src/components/Container-test.tsx | 269 +++--------------------------- 1 file changed, 21 insertions(+), 248 deletions(-) diff --git a/src/components/Container-test.tsx b/src/components/Container-test.tsx index aafcea129e..7e9821df16 100644 --- a/src/components/Container-test.tsx +++ b/src/components/Container-test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { describe, it, expect } from 'vitest' -import { render, screen } from 'src/utilities/testingLibrary' +import { render } from 'src/utilities/testingLibrary' import { Container } from 'src/components/Container' import { STEPS } from 'src/const/Connect' import { initialState } from 'src/services/mockedData' @@ -8,255 +8,28 @@ import { initialState } from 'src/services/mockedData' describe('Container', () => { const preloadedState = initialState - describe('rendering', () => { - it('renders the container with correct data-test attribute', () => { - const { container } = render( - -
Test Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - }) - - it('renders children content', () => { - render( - -
Test Content
-
, - { preloadedState }, - ) - - expect(screen.getByTestId('child-content')).toBeInTheDocument() - expect(screen.getByText('Test Content')).toBeInTheDocument() - }) - - it('renders multiple children', () => { - render( - -
First Child
-
Second Child
- Third Child -
, - { preloadedState }, - ) - - expect(screen.getByTestId('child-1')).toBeInTheDocument() - expect(screen.getByTestId('child-2')).toBeInTheDocument() - expect(screen.getByTestId('child-3')).toBeInTheDocument() - }) - - it('renders without children', () => { - const { container } = render(, { preloadedState }) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - }) - - it('renders with null children', () => { - const { container } = render({null}, { preloadedState }) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - }) - }) - - describe('step prop', () => { - it('renders without step prop', () => { - const { container } = render( - -
Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) - }) - - it('renders with SEARCH step and applies maxHeight', () => { - const { container } = render( - -
Search Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) - }) - - it('renders with non-SEARCH step without maxHeight constraint', () => { - const { container } = render( - -
Connected Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) - }) - - it('renders correctly with ENTER_CREDENTIALS step', () => { - render( - -
Enter Credentials
-
, - { preloadedState }, - ) - - expect(screen.getByTestId('credentials-content')).toBeInTheDocument() - }) - - it('renders correctly with MFA step', () => { - render( - -
MFA Content
-
, - { preloadedState }, - ) - - expect(screen.getByTestId('mfa-content')).toBeInTheDocument() - }) - }) - - describe('styling', () => { - it('applies consistent container styles', () => { - const { container } = render( - -
Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toHaveStyle({ - minHeight: '100%', - display: 'flex', - justifyContent: 'center', - }) - }) - - it('has a content wrapper with proper constraints', () => { - const { container } = render( - -
Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - const contentWrapper = containerDiv?.firstChild as HTMLElement - - expect(contentWrapper).toBeInTheDocument() - expect(contentWrapper).toHaveStyle({ - maxWidth: '400px', - minWidth: '270px', - width: '100%', - }) - }) - - it('applies background color from tokens', () => { - const { container } = render( - -
Content
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toHaveStyle({ backgroundColor: expect.any(String) }) - }) + it('renders', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) }) - describe('integration', () => { - it('renders complete structure with SEARCH step', () => { - const { container } = render( - -
Search for institution
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) - expect(screen.getByTestId('search-content')).toBeInTheDocument() - expect(screen.getByText('Search for institution')).toBeInTheDocument() - }) - - it('renders complete structure with CONNECTED step', () => { - const { container } = render( - -
Successfully connected!
-
, - { preloadedState }, - ) - - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(screen.getByTestId('success-message')).toBeInTheDocument() - expect(screen.getByText('Successfully connected!')).toBeInTheDocument() - }) - - it('renders nested component structure', () => { - render( - -
-

Title

-
-

Nested Content

-
-
-
, - { preloadedState }, - ) - - expect(screen.getByTestId('outer')).toBeInTheDocument() - expect(screen.getByTestId('inner')).toBeInTheDocument() - expect(screen.getByText('Title')).toBeInTheDocument() - expect(screen.getByText('Nested Content')).toBeInTheDocument() - }) - - it('maintains structure with form elements', () => { - render( - -
- - -
-
, - { preloadedState }, - ) - - expect(screen.getByTestId('test-form')).toBeInTheDocument() - expect(screen.getByTestId('test-input')).toBeInTheDocument() - expect(screen.getByTestId('test-button')).toBeInTheDocument() - }) - - it('wraps components consistently regardless of content type', () => { - const { container } = render( - -
- Text - test - -
-
, - { preloadedState }, - ) + it('applies maxHeight when step is SEARCH', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(screen.getByText('Text')).toBeInTheDocument() - expect(screen.getByAltText('test')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument() - }) + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) }) }) From 2e67c7d28c34d0199550312e8eb7e179925058d6 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 16:34:10 -0600 Subject: [PATCH 5/7] refactored delete member survey --- src/Connect.tsx | 32 +- src/components/DeleteMemberSurvey-test.tsx | 515 +++++++++------------ src/components/DeleteMemberSurvey.js | 22 +- 3 files changed, 236 insertions(+), 333 deletions(-) diff --git a/src/Connect.tsx b/src/Connect.tsx index f44c31faad..267e07bff1 100644 --- a/src/Connect.tsx +++ b/src/Connect.tsx @@ -328,25 +328,19 @@ export const Connect: React.FC = ({ return (
- {state.memberToDelete && ( - { - setState({ ...state, memberToDelete: null }) - }} - onDeleteSuccess={(deletedMember) => { - postMessageFunctions.onPostMessage('connect/memberDeleted', { - member_guid: deletedMember.guid, - }) - onMemberDeleted(deletedMember.guid) - - setState((prevState) => { - dispatch(connectActions.stepToDeleteMemberSuccess(deletedMember.guid)) - return { ...prevState, memberToDelete: null } - }) - }} - /> - )} + setState({ ...state, memberToDelete: null })} + onMemberDeleted={(memberGuid) => { + postMessageFunctions.onPostMessage('connect/memberDeleted', { + member_guid: memberGuid, + }) + onMemberDeleted(memberGuid) + dispatch(connectActions.stepToDeleteMemberSuccess(memberGuid)) + setState({ ...state, memberToDelete: null }) + }} + /> dispatch(handleGoBackWithSideEffects())} diff --git a/src/components/DeleteMemberSurvey-test.tsx b/src/components/DeleteMemberSurvey-test.tsx index 3e44e1c844..efab4fe9e4 100644 --- a/src/components/DeleteMemberSurvey-test.tsx +++ b/src/components/DeleteMemberSurvey-test.tsx @@ -1,345 +1,248 @@ import React from 'react' -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { DeleteMemberSurvey } from 'src/components/DeleteMemberSurvey' -import { initialState, CONNECTED_MEMBER, NON_CONNECTED_MEMBER } from 'src/services/mockedData' +import { initialState, CONNECTED_MEMBER } from 'src/services/mockedData' import userEvent from '@testing-library/user-event' +import { apiValue as mockApiValue } from 'src/const/apiProviderMock' describe('DeleteMemberSurvey', () => { const preloadedState = initialState - const mockOnCancel = vi.fn() - const mockOnDeleteSuccess = vi.fn() - - beforeEach(() => { - vi.clearAllMocks() + it('does not render when isOpen is false', () => { + const { container } = render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(container.firstChild).toBeNull() }) - describe('rendering', () => { - it('renders the disconnect institution dialog', () => { - const { container } = render( - , - { preloadedState }, - ) - - const dialog = container.querySelector('[role="dialog"]') - expect(dialog).toBeInTheDocument() - }) - - it('renders the disconnect institution heading', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - }) + 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('renders the disclaimer with member name', () => { - render( - , - { preloadedState }, - ) - - const disclaimer = screen.getByTestId('disconnect-disclaimer') - expect(disclaimer).toBeInTheDocument() - expect(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('renders disconnect and cancel buttons', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() - }) + it('shows connected member reasons', () => { + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() + expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() + expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() + }) - it('renders required field indicator', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('Required')).toBeInTheDocument() - }) + it('shows non-connected member reasons', () => { + const nonConnectedMember = { ...CONNECTED_MEMBER, connection_status: 1 } + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() + expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() + expect( + screen.queryByText("I no longer use this account or it's not mine"), + ).not.toBeInTheDocument() }) - describe('connected member reasons', () => { - it('renders correct reasons for connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() - expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() - expect(screen.getByText("I don't want to use this app")).toBeInTheDocument() - expect(screen.getByText('Other')).toBeInTheDocument() - }) + it('shows validation error when no reason selected', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) - it('does not render non-connected reasons for connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() - expect( - screen.queryByText('The account information is old or inaccurate'), - ).not.toBeInTheDocument() - expect(screen.queryByText("I don't want this account connected here")).not.toBeInTheDocument() - }) - }) + await user.click(screen.getByTestId('disconnect-button')) - describe('non-connected member reasons', () => { - it('renders correct reasons for non-connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() - expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() - expect(screen.getByText("I don't want this account connected here")).toBeInTheDocument() - expect(screen.getByText('Other')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() }) + }) - it('does not render connected-only reasons for non-connected member', () => { - render( - , - { preloadedState }, - ) - - expect( - screen.queryByText("I no longer use this account or it's not mine"), - ).not.toBeInTheDocument() - expect(screen.queryByText("I don't want to share my data")).not.toBeInTheDocument() - expect(screen.queryByText("I don't want to use this app")).not.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() }) - describe('user interactions', () => { - it('calls onCancel when cancel button is clicked', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - await user.click(screen.getByTestId('disconnect-cancel-button')) - - expect(mockOnCancel).toHaveBeenCalledTimes(1) - }) + it('clears validation error after selecting a reason', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) - it('allows selecting a reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - - expect(options[0]).toBeChecked() - }) + await user.click(screen.getByTestId('disconnect-button')) - it('allows changing selected reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - expect(options[0]).toBeChecked() - - await user.click(options[1]) - expect(options[1]).toBeChecked() - expect(options[0]).not.toBeChecked() + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() }) - }) - describe('form validation', () => { - it('shows validation error when disconnect clicked without selecting reason', async () => { - const user = userEvent.setup() - render( - , - { 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) - it('does not show validation error before first submit attempt', () => { - render( - , - { preloadedState }, - ) - - expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() - }) + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) - it('validation error disappears after selecting a reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - await user.click(screen.getByTestId('disconnect-button')) - - await waitFor(() => { - expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() - }) - const options = screen.getAllByRole('radio') - await user.click(options[0]) - - await waitFor(() => { - 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() }) }) - describe('delete member flow', () => { - it('initiates delete when disconnect clicked with valid selection', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) + 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() + }) - const options = screen.getAllByRole('radio') - await user.click(options[0]) + 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'))), + } - await user.click(screen.getByTestId('disconnect-button')) + render( + {}} + />, + { apiValue, preloadedState }, + ) - expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() - }) - }) + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + await user.click(screen.getByTestId('disconnect-button')) - describe('integration', () => { - it('renders complete structure for connected member', () => { - const { container } = render( - , - { preloadedState }, - ) - - expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-disclaimer')).toBeInTheDocument() - expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) - expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument() }) - it('renders complete structure for non-connected member', () => { - const { container } = render( - , - { preloadedState }, - ) - - expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Wells Fargo') - expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) - }) + await user.click(screen.getByTestId('disconnect-ok-button')) - it('handles complete user flow from selection to cancel', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - expect(options[0]).toBeChecked() - - await user.click(screen.getByTestId('disconnect-cancel-button')) - expect(mockOnCancel).toHaveBeenCalledTimes(1) - }) + expect(onClose).toHaveBeenCalled() }) }) diff --git a/src/components/DeleteMemberSurvey.js b/src/components/DeleteMemberSurvey.js index 9d8565541f..350a9c5114 100644 --- a/src/components/DeleteMemberSurvey.js +++ b/src/components/DeleteMemberSurvey.js @@ -19,7 +19,7 @@ import { PageviewInfo } from 'src/const/Analytics' import { ReadableStatuses } from 'src/const/Statuses' 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() @@ -59,12 +59,17 @@ export const DeleteMemberSurvey = (props) => { 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) => {