From a1fd3d17b06880fb8bd599e7ae059bc633bd053e Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 13:12:05 -0600 Subject: [PATCH 1/7] fix: add utilities and consent tests --- src/utilities/Accounts-test.js | 100 ++++ src/utilities/KeyPress-test.js | 62 +++ src/utilities/Polyfill-test.js | 113 ++++ src/utilities/ScrollToTop-test.js | 51 ++ src/views/consent/ConsentModal-test.tsx | 137 +++++ src/views/consent/DynamicDisclosure-test.tsx | 490 ++++++++++++++++++ .../__tests__/DynamicDisclosure-test.tsx | 36 -- 7 files changed, 953 insertions(+), 36 deletions(-) create mode 100644 src/utilities/Accounts-test.js create mode 100644 src/utilities/KeyPress-test.js create mode 100644 src/utilities/Polyfill-test.js create mode 100644 src/utilities/ScrollToTop-test.js create mode 100644 src/views/consent/ConsentModal-test.tsx create mode 100644 src/views/consent/DynamicDisclosure-test.tsx delete mode 100644 src/views/consent/__tests__/DynamicDisclosure-test.tsx diff --git a/src/utilities/Accounts-test.js b/src/utilities/Accounts-test.js new file mode 100644 index 0000000000..379e498849 --- /dev/null +++ b/src/utilities/Accounts-test.js @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest' +import { getSortedAccountsWithMembers } from 'src/utilities/Accounts.js' + +describe('getSortedAccountsWithMembers', () => { + it('returns empty array when no accounts match members', () => { + const accounts = [{ guid: 'ACC-1', member_guid: 'MEM-999', user_name: 'Account 1' }] + const members = [{ guid: 'MEM-1', name: 'Member 1' }] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toEqual([]) + }) + + it('filters accounts by member guid and adds member name', () => { + const accounts = [ + { guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Checking' }, + { guid: 'ACC-2', member_guid: 'MEM-2', user_name: 'Savings' }, + ] + const members = [ + { guid: 'MEM-1', name: 'Bank of America' }, + { guid: 'MEM-2', name: 'Chase' }, + ] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + guid: 'ACC-1', + member_guid: 'MEM-1', + user_name: 'Checking', + memberName: 'Bank of America', + }) + expect(result[1]).toEqual({ + guid: 'ACC-2', + member_guid: 'MEM-2', + user_name: 'Savings', + memberName: 'Chase', + }) + }) + + it('sorts accounts by user_name alphabetically', () => { + const accounts = [ + { guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Savings' }, + { guid: 'ACC-2', member_guid: 'MEM-1', user_name: 'Checking' }, + { guid: 'ACC-3', member_guid: 'MEM-1', user_name: 'Investment' }, + ] + const members = [{ guid: 'MEM-1', name: 'Bank' }] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result[0].user_name).toBe('Checking') + expect(result[1].user_name).toBe('Investment') + expect(result[2].user_name).toBe('Savings') + }) + + it('filters out accounts without matching member', () => { + const accounts = [ + { guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Account 1' }, + { guid: 'ACC-2', member_guid: 'MEM-999', user_name: 'Account 2' }, + { guid: 'ACC-3', member_guid: 'MEM-2', user_name: 'Account 3' }, + ] + const members = [ + { guid: 'MEM-1', name: 'Member 1' }, + { guid: 'MEM-2', name: 'Member 2' }, + ] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toHaveLength(2) + expect(result.find((a) => a.guid === 'ACC-2')).toBeUndefined() + }) + + it('handles empty members array', () => { + const accounts = [{ guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Account 1' }] + const members = [] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toEqual([]) + }) + + it('handles empty accounts array', () => { + const accounts = [] + const members = [{ guid: 'MEM-1', name: 'Member 1' }] + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toEqual([]) + }) + + it('handles member not found gracefully', () => { + const accounts = [{ guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Account 1' }] + const members = [{ guid: 'MEM-1' }] // No name property + + const result = getSortedAccountsWithMembers(accounts, members) + + expect(result).toHaveLength(1) + expect(result[0].memberName).toBeUndefined() + }) +}) diff --git a/src/utilities/KeyPress-test.js b/src/utilities/KeyPress-test.js new file mode 100644 index 0000000000..7dfff64dd4 --- /dev/null +++ b/src/utilities/KeyPress-test.js @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from 'vitest' +import { preventDefaultAndStopAllPropagation } from 'src/utilities/KeyPress.js' + +describe('preventDefaultAndStopAllPropagation', () => { + it('calls preventDefault on the event', () => { + const mockEvent = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + nativeEvent: { + stopImmediatePropagation: vi.fn(), + }, + } + + preventDefaultAndStopAllPropagation(mockEvent) + + expect(mockEvent.preventDefault).toHaveBeenCalled() + }) + + it('calls stopPropagation on the event', () => { + const mockEvent = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + nativeEvent: { + stopImmediatePropagation: vi.fn(), + }, + } + + preventDefaultAndStopAllPropagation(mockEvent) + + expect(mockEvent.stopPropagation).toHaveBeenCalled() + }) + + it('calls stopImmediatePropagation on the native event', () => { + const mockEvent = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + nativeEvent: { + stopImmediatePropagation: vi.fn(), + }, + } + + preventDefaultAndStopAllPropagation(mockEvent) + + expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalled() + }) + + it('calls all three propagation methods', () => { + const mockEvent = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + nativeEvent: { + stopImmediatePropagation: vi.fn(), + }, + } + + preventDefaultAndStopAllPropagation(mockEvent) + + expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1) + expect(mockEvent.stopPropagation).toHaveBeenCalledTimes(1) + expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/utilities/Polyfill-test.js b/src/utilities/Polyfill-test.js new file mode 100644 index 0000000000..709d3a3fc5 --- /dev/null +++ b/src/utilities/Polyfill-test.js @@ -0,0 +1,113 @@ +import { describe, expect, it, beforeEach, afterEach } from 'vitest' +import { fromEntriesPolyfill } from 'src/utilities/Polyfill.js' + +describe('fromEntriesPolyfill', () => { + let originalFromEntries + + beforeEach(() => { + originalFromEntries = Object.fromEntries + }) + + afterEach(() => { + Object.fromEntries = originalFromEntries + }) + + it('does not override Object.fromEntries if it exists', () => { + const existingImpl = Object.fromEntries + + fromEntriesPolyfill() + + expect(Object.fromEntries).toBe(existingImpl) + }) + + it('adds Object.fromEntries if it does not exist', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + expect(Object.fromEntries).toBeDefined() + expect(typeof Object.fromEntries).toBe('function') + }) + + it('creates object from entries array when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + const entries = [ + ['a', 1], + ['b', 2], + ['c', 3], + ] + const result = Object.fromEntries(entries) + + expect(result).toEqual({ a: 1, b: 2, c: 3 }) + }) + + it('handles Map entries when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + const map = new Map([ + ['key1', 'value1'], + ['key2', 'value2'], + ]) + const result = Object.fromEntries(map) + + expect(result).toEqual({ key1: 'value1', key2: 'value2' }) + }) + + it('throws error for non-iterable argument when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + expect(() => { + Object.fromEntries(null) + }).toThrow('Object.fromEntries() requires a single iterable argument') + }) + + it('throws error for undefined argument when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + expect(() => { + Object.fromEntries(undefined) + }).toThrow('Object.fromEntries() requires a single iterable argument') + }) + + it('handles empty entries array when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + const result = Object.fromEntries([]) + + expect(result).toEqual({}) + }) + + it('handles various value types when polyfilled', () => { + delete Object.fromEntries + + fromEntriesPolyfill() + + const entries = [ + ['string', 'value'], + ['number', 42], + ['boolean', true], + ['null', null], + ['object', { nested: 'object' }], + ] + const result = Object.fromEntries(entries) + + expect(result).toEqual({ + string: 'value', + number: 42, + boolean: true, + null: null, + object: { nested: 'object' }, + }) + }) +}) diff --git a/src/utilities/ScrollToTop-test.js b/src/utilities/ScrollToTop-test.js new file mode 100644 index 0000000000..ef6325e162 --- /dev/null +++ b/src/utilities/ScrollToTop-test.js @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from 'vitest' +import { scrollToTop } from 'src/utilities/ScrollToTop.js' + +describe('scrollToTop', () => { + it('calls scrollIntoView on the current ref element', () => { + const mockScrollIntoView = vi.fn() + const ref = { + current: { + scrollIntoView: mockScrollIntoView, + }, + } + + scrollToTop(ref) + + expect(mockScrollIntoView).toHaveBeenCalledWith(true) + }) + + it('returns undefined when ref.current is null', () => { + const ref = { + current: null, + } + + const result = scrollToTop(ref) + + expect(result).toBeUndefined() + }) + + it('returns undefined when ref.current is undefined', () => { + const ref = { + current: undefined, + } + + const result = scrollToTop(ref) + + expect(result).toBeUndefined() + }) + + it('returns the result of scrollIntoView', () => { + const mockReturnValue = 'scrolled' + const mockScrollIntoView = vi.fn().mockReturnValue(mockReturnValue) + const ref = { + current: { + scrollIntoView: mockScrollIntoView, + }, + } + + const result = scrollToTop(ref) + + expect(result).toBe(mockReturnValue) + }) +}) diff --git a/src/views/consent/ConsentModal-test.tsx b/src/views/consent/ConsentModal-test.tsx new file mode 100644 index 0000000000..b8059d0427 --- /dev/null +++ b/src/views/consent/ConsentModal-test.tsx @@ -0,0 +1,137 @@ +import React from 'react' +import { render, screen } from 'src/utilities/testingLibrary' +import { ConsentModal } from 'src/views/consent/ConsentModal.tsx' +import * as globalUtils from 'src/utilities/global' + +vi.mock('src/utilities/global', () => ({ + goToUrlLink: vi.fn(), +})) + +describe('ConsentModal', () => { + const mockSetDialogIsOpen = vi.fn() + const defaultProps = { + dialogIsOpen: true, + setDialogIsOpen: mockSetDialogIsOpen, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Rendering', () => { + it('should render the modal when dialogIsOpen is true', () => { + render() + + expect(screen.getByText('Who is MX Technologies?')).toBeInTheDocument() + }) + + it('should not render the modal when dialogIsOpen is false', () => { + render() + + expect(screen.queryByText('Who is MX Technologies?')).not.toBeInTheDocument() + }) + + it('should render all main content sections', () => { + render() + + expect( + screen.getByText( + /MX is a trusted financial data platform that securely connects your accounts/i, + ), + ).toBeInTheDocument() + expect(screen.getByText('MX promise:')).toBeInTheDocument() + }) + + it('should render secure section with lock emoji', () => { + render() + + expect(screen.getByText('Secure:')).toBeInTheDocument() + expect( + screen.getByText('Industry-standard encryption protects your data.'), + ).toBeInTheDocument() + }) + + it('should render control section with gear emoji', () => { + render() + + expect(screen.getByText('Control:')).toBeInTheDocument() + expect(screen.getByText('You can manage and revoke access anytime.')).toBeInTheDocument() + }) + + it('should render private section with shield emoji', () => { + render() + + expect(screen.getByText('Private:')).toBeInTheDocument() + expect( + screen.getByText('Your data is never sold or shared without consent.'), + ).toBeInTheDocument() + }) + + it('should render Close button', () => { + render() + + expect(screen.getByText('Close')).toBeInTheDocument() + }) + + it('should render Learn more button', () => { + render() + + expect(screen.getByText('Learn more')).toBeInTheDocument() + }) + }) + + describe('Interactions', () => { + it('should call setDialogIsOpen when dialog is closed via onClose', async () => { + const { user } = render() + + const dialog = screen.getByRole('dialog') + expect(dialog).toBeInTheDocument() + + const backdrop = document.querySelector('.MuiBackdrop-root') + if (backdrop) { + await user.click(backdrop as HTMLElement) + } + expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) + }) + + it('should call setDialogIsOpen when Close button is clicked', async () => { + const { user } = render() + + const closeButton = screen.getByText('Close') + await user.click(closeButton) + + expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) + }) + + it('should call goToUrlLink when Learn more button is clicked', async () => { + const { user } = render() + + const learnMoreButton = screen.getByText('Learn more') + await user.click(learnMoreButton) + + expect(globalUtils.goToUrlLink).toHaveBeenCalledWith('https://www.mx.com/company/') + }) + + it('should toggle state correctly when calling setDialogIsOpen function', () => { + render() + + const closeButton = screen.getByText('Close') + closeButton.click() + + expect(mockSetDialogIsOpen).toHaveBeenCalled() + + const toggleFunction = mockSetDialogIsOpen.mock.calls[0][0] + expect(toggleFunction(true)).toBe(false) + expect(toggleFunction(false)).toBe(true) + }) + }) + + describe('Styling', () => { + it('should apply dialog max width and min width styles', () => { + render() + + const dialog = screen.getByRole('dialog') + expect(dialog).toBeInTheDocument() + }) + }) +}) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx new file mode 100644 index 0000000000..4170265900 --- /dev/null +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -0,0 +1,490 @@ +import React from 'react' +import { screen, render, waitFor } from 'src/utilities/testingLibrary' +import { DynamicDisclosure } from 'src/views/consent/DynamicDisclosure' +import { initialState } from 'src/services/mockedData' +import { AGG_MODE, VERIFY_MODE } from 'src/const/Connect' +import { ActionTypes } from 'src/redux/actions/Connect' +import * as Animation from 'src/utilities/Animation' +import * as Intl from 'src/utilities/Intl' + +declare global { + interface Window { + app: { + options: { language: string } + } + } +} + +vi.mock('src/utilities/Animation', () => ({ + fadeOut: vi.fn(() => Promise.resolve()), +})) + +const dispatch = vi.fn() +vi.mock('react-redux', async (importActual) => { + const actual = await importActual() + return { + ...actual, + useDispatch: () => dispatch, + } +}) + +const onConsentClick = vi.fn() +const onGoBackClick = vi.fn() + +const dynamicDisclosureProps = { + onConsentClick, + onGoBackClick, +} + +const mockInstitution = { + guid: 'INS-123', + name: 'Test Bank', + logo_url: 'https://example.com/logo.png', +} + +describe('DynamicDisclosure', () => { + beforeEach(() => { + vi.clearAllMocks() + window.app = { options: { language: 'en-us' } } + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + configurable: true, + value: 1000, + }) + Object.defineProperty(document.documentElement, 'scrollTop', { + writable: true, + configurable: true, + value: 0, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: 800, + }) + }) + + describe('Rendering', () => { + it('loads the consent screen', async () => { + const ref = React.createRef() + render() + + expect(await screen.findByTestId('dynamic-disclosure-title')).toBeInTheDocument() + expect(await screen.findByTestId('dynamic-disclosure-p1')).toBeInTheDocument() + expect(await screen.findByText('I consent')).toBeInTheDocument() + expect(await screen.findByText('Account Information')).toBeInTheDocument() + const buttons = screen.getAllByRole('button') + expect(buttons).toHaveLength(5) + }) + + it('should render with app name when provided', () => { + const state = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + oauth_app_name: 'MyApp', + }, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('MyApp uses MX Technologies') + }) + + it('should render without app name when not provided', () => { + const state = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + oauth_app_name: null, + }, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('This app uses MX Technologies') + }) + + it('should render Share your data title', () => { + render() + + expect(screen.getByTestId('dynamic-disclosure-title')).toHaveTextContent('Share your data') + }) + + it('should render PrivateAndSecure component', () => { + render() + + expect(screen.getByText(/Private and secure/i)).toBeInTheDocument() + }) + }) + + describe('Mode-specific rendering', () => { + it('should render AGG mode content when mode is AGG_MODE', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + mode: AGG_MODE, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('manage your finances') + }) + + it('should render VERIFY mode content when mode is VERIFY_MODE', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + mode: VERIFY_MODE, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('move money') + }) + + it('should render combined mode content when both AGG and VERIFY', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + mode: AGG_MODE, + data_request: { + products: ['transactions', 'identity_verification'], + }, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('move money and manage your finances') + }) + + it('should render AGG mode when include_transactions is true', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + include_transactions: true, + }, + connect: { + ...initialState.connect, + selectedInstitution: mockInstitution, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + expect(container.textContent).toContain('manage your finances') + }) + }) + + describe('Modal interaction', () => { + it('loads the consent screen and clicks the info button to open modal', async () => { + const ref = React.createRef() + const { user } = render() + + await user.click(await screen.findByTestId('info-button')) + + expect(await screen.findByText('Who is MX Technologies?')).toBeInTheDocument() + }) + + it('should toggle modal when info button is clicked multiple times', async () => { + const { user } = render() + + const infoButton = screen.getByTestId('info-button') + await user.click(infoButton) + expect(screen.getByText('Who is MX Technologies?')).toBeInTheDocument() + + const closeButton = screen.getByText('Close') + await user.click(closeButton) + expect(screen.queryByText('Who is MX Technologies?')).not.toBeInTheDocument() + }) + }) + + describe('Consent button', () => { + it('should have consent button disabled initially when not scrolled to bottom', () => { + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + configurable: true, + value: 2000, + }) + Object.defineProperty(document.documentElement, 'scrollTop', { + writable: true, + configurable: true, + value: 0, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: 800, + }) + + render() + + const consentButton = screen.getByTestId('consent-button') + expect(consentButton).toBeDisabled() + }) + + it('should enable consent button when scrolled to bottom', async () => { + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + configurable: true, + value: 1000, + }) + Object.defineProperty(document.documentElement, 'scrollTop', { + writable: true, + configurable: true, + value: 200, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: 800, + }) + + render() + + window.dispatchEvent(new Event('scroll')) + const consentButton = await screen.findByTestId('consent-button') + expect(consentButton).not.toBeDisabled() + }) + + it('should dispatch USER_CONSENTED action when consent button is clicked', async () => { + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + configurable: true, + value: 1000, + }) + Object.defineProperty(document.documentElement, 'scrollTop', { + writable: true, + configurable: true, + value: 200, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: 800, + }) + + const { user } = render() + + window.dispatchEvent(new Event('scroll')) + + await waitFor(() => { + const consentButton = screen.getByTestId('consent-button') + expect(consentButton).not.toBeDisabled() + }) + + const consentButton = screen.getByTestId('consent-button') + await user.click(consentButton) + + expect(dispatch).toHaveBeenCalledWith({ type: ActionTypes.USER_CONSENTED }) + }) + }) + + describe('Translation toggle', () => { + it('should show translation link for Spanish locale', () => { + window.app = { options: { language: 'es' } } + vi.spyOn(Intl, 'getLocale').mockReturnValue('es') + + render() + + expect(screen.getByTestId('translation-button')).toBeInTheDocument() + }) + + it('should show translation link for French-Canadian locale', () => { + window.app = { options: { language: 'fr-ca' } } + vi.spyOn(Intl, 'getLocale').mockReturnValue('fr-ca') + + render() + + expect(screen.getByTestId('translation-button')).toBeInTheDocument() + }) + + it('should not show translation link for English locale', () => { + window.app = { options: { language: 'en-us' } } + vi.spyOn(Intl, 'getLocale').mockReturnValue('en') + + render() + + expect(screen.queryByTestId('translation-button')).not.toBeInTheDocument() + }) + + it('should toggle locale when translation link is clicked', async () => { + window.app = { options: { language: 'es' } } + const setLocaleSpy = vi.spyOn(Intl, 'setLocale') + vi.spyOn(Intl, 'getLocale').mockReturnValue('es') + + const { user } = render() + + const translationButton = screen.getByTestId('translation-button') + await user.click(translationButton) + + expect(setLocaleSpy).toHaveBeenCalledWith('en') + }) + }) + + describe('Imperative handle', () => { + it('should expose handleBackButton method', () => { + const ref = React.createRef<{ handleBackButton: () => void }>() + render() + + expect(ref.current).toBeDefined() + expect(ref.current?.handleBackButton).toBeDefined() + }) + + it('should call fadeOut and onGoBackClick when handleBackButton is called', async () => { + const ref = React.createRef<{ handleBackButton: () => void }>() + render() + + ref.current?.handleBackButton() + + await waitFor(() => { + expect(Animation.fadeOut).toHaveBeenCalled() + expect(onGoBackClick).toHaveBeenCalled() + }) + }) + + it('should expose showBackButton method', () => { + const ref = React.createRef<{ showBackButton: () => boolean }>() + render() + + expect(ref.current?.showBackButton).toBeDefined() + }) + + it('should return true for showBackButton when institution search is not disabled', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + disable_institution_search: false, + }, + } + + const ref = React.createRef<{ showBackButton: () => boolean }>() + render(, { preloadedState: state }) + + expect(ref.current?.showBackButton()).toBe(true) + }) + + it('should return false for showBackButton when institution search is disabled', () => { + const state = { + ...initialState, + config: { + ...initialState.config, + disable_institution_search: true, + }, + } + + const ref = React.createRef<{ showBackButton: () => boolean }>() + render(, { preloadedState: state }) + + expect(ref.current?.showBackButton()).toBe(false) + }) + + it('should restore locale when handleBackButton is called with non-English initial locale', async () => { + window.app = { options: { language: 'es' } } + const setLocaleSpy = vi.spyOn(Intl, 'setLocale') + vi.spyOn(Intl, 'getLocale').mockReturnValue('en') + + const ref = React.createRef<{ handleBackButton: () => void }>() + render() + + ref.current?.handleBackButton() + + await waitFor(() => { + expect(setLocaleSpy).toHaveBeenCalledWith('es') + }) + }) + + it('should restore locale when consent button is clicked with non-English initial locale', async () => { + window.app = { options: { language: 'es' } } + const setLocaleSpy = vi.spyOn(Intl, 'setLocale') + vi.spyOn(Intl, 'getLocale').mockReturnValue('en') + + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + configurable: true, + value: 1000, + }) + Object.defineProperty(document.documentElement, 'scrollTop', { + writable: true, + configurable: true, + value: 200, + }) + Object.defineProperty(document.documentElement, 'clientHeight', { + writable: true, + configurable: true, + value: 800, + }) + + const { user } = render() + + window.dispatchEvent(new Event('scroll')) + + await waitFor(() => { + const consentButton = screen.getByTestId('consent-button') + expect(consentButton).not.toBeDisabled() + }) + + const consentButton = screen.getByTestId('consent-button') + await user.click(consentButton) + + expect(setLocaleSpy).toHaveBeenCalledWith('es') + }) + }) + + describe('Cleanup', () => { + it('should remove scroll event listener on unmount', () => { + const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener') + + const { unmount } = render() + + unmount() + + expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)) + }) + }) +}) diff --git a/src/views/consent/__tests__/DynamicDisclosure-test.tsx b/src/views/consent/__tests__/DynamicDisclosure-test.tsx deleted file mode 100644 index 5ecba50faa..0000000000 --- a/src/views/consent/__tests__/DynamicDisclosure-test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' - -import { screen, render } from 'src/utilities/testingLibrary' - -import { DynamicDisclosure } from 'src/views/consent/DynamicDisclosure' - -const onConsentClick = vi.fn() -const onGoBackClick = vi.fn() - -const dynamicDisclosureProps = { - onConsentClick, - onGoBackClick, -} - -describe('dynamic disclosure', () => { - it('loads the consent screen', async () => { - const ref = React.createRef() - render() - - expect(await screen.findByTestId('dynamic-disclosure-title')).toBeInTheDocument() - expect(await screen.findByTestId('dynamic-disclosure-p1')).toBeInTheDocument() - expect(await screen.findByText('I consent')).toBeInTheDocument() - expect(await screen.findByText('Account Information')).toBeInTheDocument() - const buttons = screen.getAllByRole('button') - expect(buttons).toHaveLength(5) - }) - - it('loads the consent screen and clicks the info button to open modal', async () => { - const ref = React.createRef() - const { user } = render() - - await user.click(await screen.findByTestId('info-button')) - - expect(await screen.findByText('Who is MX Technologies?')).toBeInTheDocument() - }) -}) From fa4c4269c6c84c887b2d58f99af0d9a6576387cd Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 14:10:00 -0600 Subject: [PATCH 2/7] fixed imports --- src/utilities/Accounts-test.js | 2 +- src/utilities/KeyPress-test.js | 2 +- src/utilities/Polyfill-test.js | 2 +- src/utilities/ScrollToTop-test.js | 2 +- src/views/consent/ConsentModal-test.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utilities/Accounts-test.js b/src/utilities/Accounts-test.js index 379e498849..09f9e3e488 100644 --- a/src/utilities/Accounts-test.js +++ b/src/utilities/Accounts-test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { getSortedAccountsWithMembers } from 'src/utilities/Accounts.js' +import { getSortedAccountsWithMembers } from 'src/utilities/Accounts' describe('getSortedAccountsWithMembers', () => { it('returns empty array when no accounts match members', () => { diff --git a/src/utilities/KeyPress-test.js b/src/utilities/KeyPress-test.js index 7dfff64dd4..b48142d69c 100644 --- a/src/utilities/KeyPress-test.js +++ b/src/utilities/KeyPress-test.js @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { preventDefaultAndStopAllPropagation } from 'src/utilities/KeyPress.js' +import { preventDefaultAndStopAllPropagation } from 'src/utilities/KeyPress' describe('preventDefaultAndStopAllPropagation', () => { it('calls preventDefault on the event', () => { diff --git a/src/utilities/Polyfill-test.js b/src/utilities/Polyfill-test.js index 709d3a3fc5..8cd104659d 100644 --- a/src/utilities/Polyfill-test.js +++ b/src/utilities/Polyfill-test.js @@ -1,5 +1,5 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest' -import { fromEntriesPolyfill } from 'src/utilities/Polyfill.js' +import { fromEntriesPolyfill } from 'src/utilities/Polyfill' describe('fromEntriesPolyfill', () => { let originalFromEntries diff --git a/src/utilities/ScrollToTop-test.js b/src/utilities/ScrollToTop-test.js index ef6325e162..2084655e90 100644 --- a/src/utilities/ScrollToTop-test.js +++ b/src/utilities/ScrollToTop-test.js @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { scrollToTop } from 'src/utilities/ScrollToTop.js' +import { scrollToTop } from 'src/utilities/ScrollToTop' describe('scrollToTop', () => { it('calls scrollIntoView on the current ref element', () => { diff --git a/src/views/consent/ConsentModal-test.tsx b/src/views/consent/ConsentModal-test.tsx index b8059d0427..c09c50166f 100644 --- a/src/views/consent/ConsentModal-test.tsx +++ b/src/views/consent/ConsentModal-test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' -import { ConsentModal } from 'src/views/consent/ConsentModal.tsx' +import { ConsentModal } from 'src/views/consent/ConsentModal' import * as globalUtils from 'src/utilities/global' vi.mock('src/utilities/global', () => ({ From 6da1044524d59e23c562f7188b762b77a6b0b240 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Fri, 26 Jun 2026 14:57:34 -0600 Subject: [PATCH 3/7] simplified tests and removed fragile tests --- src/utilities/Accounts-test.js | 12 ++- src/utilities/KeyPress-test.js | 64 +++------------ src/utilities/Polyfill-test.js | 46 ++++------- src/utilities/ScrollToTop-test.js | 44 ++-------- src/views/consent/ConsentModal-test.tsx | 85 +++++--------------- src/views/consent/DynamicDisclosure-test.tsx | 17 ---- 6 files changed, 61 insertions(+), 207 deletions(-) diff --git a/src/utilities/Accounts-test.js b/src/utilities/Accounts-test.js index 09f9e3e488..fa0a00513c 100644 --- a/src/utilities/Accounts-test.js +++ b/src/utilities/Accounts-test.js @@ -24,13 +24,17 @@ describe('getSortedAccountsWithMembers', () => { const result = getSortedAccountsWithMembers(accounts, members) expect(result).toHaveLength(2) - expect(result[0]).toEqual({ + + const checking = result.find((a) => a.guid === 'ACC-1') + const savings = result.find((a) => a.guid === 'ACC-2') + + expect(checking).toEqual({ guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Checking', memberName: 'Bank of America', }) - expect(result[1]).toEqual({ + expect(savings).toEqual({ guid: 'ACC-2', member_guid: 'MEM-2', user_name: 'Savings', @@ -88,9 +92,9 @@ describe('getSortedAccountsWithMembers', () => { expect(result).toEqual([]) }) - it('handles member not found gracefully', () => { + it('leaves memberName undefined when the matched member has no name', () => { const accounts = [{ guid: 'ACC-1', member_guid: 'MEM-1', user_name: 'Account 1' }] - const members = [{ guid: 'MEM-1' }] // No name property + const members = [{ guid: 'MEM-1' }] const result = getSortedAccountsWithMembers(accounts, members) diff --git a/src/utilities/KeyPress-test.js b/src/utilities/KeyPress-test.js index b48142d69c..0fddbd8d6f 100644 --- a/src/utilities/KeyPress-test.js +++ b/src/utilities/KeyPress-test.js @@ -2,61 +2,21 @@ import { describe, expect, it, vi } from 'vitest' import { preventDefaultAndStopAllPropagation } from 'src/utilities/KeyPress' describe('preventDefaultAndStopAllPropagation', () => { - it('calls preventDefault on the event', () => { - const mockEvent = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - nativeEvent: { - stopImmediatePropagation: vi.fn(), - }, - } - - preventDefaultAndStopAllPropagation(mockEvent) - - expect(mockEvent.preventDefault).toHaveBeenCalled() - }) - - it('calls stopPropagation on the event', () => { - const mockEvent = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - nativeEvent: { - stopImmediatePropagation: vi.fn(), - }, - } - - preventDefaultAndStopAllPropagation(mockEvent) - - expect(mockEvent.stopPropagation).toHaveBeenCalled() - }) - - it('calls stopImmediatePropagation on the native event', () => { - const mockEvent = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - nativeEvent: { - stopImmediatePropagation: vi.fn(), - }, - } - - preventDefaultAndStopAllPropagation(mockEvent) - - expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalled() + const createMockEvent = () => ({ + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + nativeEvent: { + stopImmediatePropagation: vi.fn(), + }, }) - it('calls all three propagation methods', () => { - const mockEvent = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - nativeEvent: { - stopImmediatePropagation: vi.fn(), - }, - } + it('stops the event on both the synthetic and native event so global listeners do not fire', () => { + const event = createMockEvent() - preventDefaultAndStopAllPropagation(mockEvent) + preventDefaultAndStopAllPropagation(event) - expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1) - expect(mockEvent.stopPropagation).toHaveBeenCalledTimes(1) - expect(mockEvent.nativeEvent.stopImmediatePropagation).toHaveBeenCalledTimes(1) + expect(event.preventDefault).toHaveBeenCalledTimes(1) + expect(event.stopPropagation).toHaveBeenCalledTimes(1) + expect(event.nativeEvent.stopImmediatePropagation).toHaveBeenCalledTimes(1) }) }) diff --git a/src/utilities/Polyfill-test.js b/src/utilities/Polyfill-test.js index 8cd104659d..c56d8cf79d 100644 --- a/src/utilities/Polyfill-test.js +++ b/src/utilities/Polyfill-test.js @@ -4,6 +4,11 @@ import { fromEntriesPolyfill } from 'src/utilities/Polyfill' describe('fromEntriesPolyfill', () => { let originalFromEntries + const installPolyfill = () => { + delete Object.fromEntries + fromEntriesPolyfill() + } + beforeEach(() => { originalFromEntries = Object.fromEntries }) @@ -21,18 +26,14 @@ describe('fromEntriesPolyfill', () => { }) it('adds Object.fromEntries if it does not exist', () => { - delete Object.fromEntries - - fromEntriesPolyfill() + installPolyfill() expect(Object.fromEntries).toBeDefined() expect(typeof Object.fromEntries).toBe('function') }) it('creates object from entries array when polyfilled', () => { - delete Object.fromEntries - - fromEntriesPolyfill() + installPolyfill() const entries = [ ['a', 1], @@ -45,9 +46,7 @@ describe('fromEntriesPolyfill', () => { }) it('handles Map entries when polyfilled', () => { - delete Object.fromEntries - - fromEntriesPolyfill() + installPolyfill() const map = new Map([ ['key1', 'value1'], @@ -58,30 +57,17 @@ describe('fromEntriesPolyfill', () => { expect(result).toEqual({ key1: 'value1', key2: 'value2' }) }) - it('throws error for non-iterable argument when polyfilled', () => { - delete Object.fromEntries - - fromEntriesPolyfill() - - expect(() => { - Object.fromEntries(null) - }).toThrow('Object.fromEntries() requires a single iterable argument') - }) - - it('throws error for undefined argument when polyfilled', () => { - delete Object.fromEntries + it('throws for non-iterable arguments when polyfilled', () => { + installPolyfill() - fromEntriesPolyfill() + const expectedError = 'Object.fromEntries() requires a single iterable argument' - expect(() => { - Object.fromEntries(undefined) - }).toThrow('Object.fromEntries() requires a single iterable argument') + expect(() => Object.fromEntries(null)).toThrow(expectedError) + expect(() => Object.fromEntries(42)).toThrow(expectedError) }) it('handles empty entries array when polyfilled', () => { - delete Object.fromEntries - - fromEntriesPolyfill() + installPolyfill() const result = Object.fromEntries([]) @@ -89,9 +75,7 @@ describe('fromEntriesPolyfill', () => { }) it('handles various value types when polyfilled', () => { - delete Object.fromEntries - - fromEntriesPolyfill() + installPolyfill() const entries = [ ['string', 'value'], diff --git a/src/utilities/ScrollToTop-test.js b/src/utilities/ScrollToTop-test.js index 2084655e90..8260206035 100644 --- a/src/utilities/ScrollToTop-test.js +++ b/src/utilities/ScrollToTop-test.js @@ -2,50 +2,22 @@ import { describe, expect, it, vi } from 'vitest' import { scrollToTop } from 'src/utilities/ScrollToTop' describe('scrollToTop', () => { - it('calls scrollIntoView on the current ref element', () => { - const mockScrollIntoView = vi.fn() + it('scrolls the ref element into view and returns the result', () => { + const scrollIntoView = vi.fn().mockReturnValue('scrolled') const ref = { current: { - scrollIntoView: mockScrollIntoView, + scrollIntoView, }, } - scrollToTop(ref) - - expect(mockScrollIntoView).toHaveBeenCalledWith(true) - }) - - it('returns undefined when ref.current is null', () => { - const ref = { - current: null, - } - - const result = scrollToTop(ref) - - expect(result).toBeUndefined() - }) - - it('returns undefined when ref.current is undefined', () => { - const ref = { - current: undefined, - } - const result = scrollToTop(ref) - expect(result).toBeUndefined() + expect(scrollIntoView).toHaveBeenCalledWith(true) + expect(result).toBe('scrolled') }) - it('returns the result of scrollIntoView', () => { - const mockReturnValue = 'scrolled' - const mockScrollIntoView = vi.fn().mockReturnValue(mockReturnValue) - const ref = { - current: { - scrollIntoView: mockScrollIntoView, - }, - } - - const result = scrollToTop(ref) - - expect(result).toBe(mockReturnValue) + it('does nothing and returns undefined when ref.current is not set', () => { + expect(scrollToTop({ current: null })).toBeUndefined() + expect(scrollToTop({ current: undefined })).toBeUndefined() }) }) diff --git a/src/views/consent/ConsentModal-test.tsx b/src/views/consent/ConsentModal-test.tsx index c09c50166f..3f6459f9d5 100644 --- a/src/views/consent/ConsentModal-test.tsx +++ b/src/views/consent/ConsentModal-test.tsx @@ -19,21 +19,10 @@ describe('ConsentModal', () => { }) describe('Rendering', () => { - it('should render the modal when dialogIsOpen is true', () => { + it('should render the modal with its content when dialogIsOpen is true', () => { render() expect(screen.getByText('Who is MX Technologies?')).toBeInTheDocument() - }) - - it('should not render the modal when dialogIsOpen is false', () => { - render() - - expect(screen.queryByText('Who is MX Technologies?')).not.toBeInTheDocument() - }) - - it('should render all main content sections', () => { - render() - expect( screen.getByText( /MX is a trusted financial data platform that securely connects your accounts/i, @@ -42,96 +31,58 @@ describe('ConsentModal', () => { expect(screen.getByText('MX promise:')).toBeInTheDocument() }) - it('should render secure section with lock emoji', () => { + it('should render the secure, control, and private promise sections', () => { render() expect(screen.getByText('Secure:')).toBeInTheDocument() expect( screen.getByText('Industry-standard encryption protects your data.'), ).toBeInTheDocument() - }) - - it('should render control section with gear emoji', () => { - render() - expect(screen.getByText('Control:')).toBeInTheDocument() expect(screen.getByText('You can manage and revoke access anytime.')).toBeInTheDocument() - }) - - it('should render private section with shield emoji', () => { - render() - expect(screen.getByText('Private:')).toBeInTheDocument() expect( screen.getByText('Your data is never sold or shared without consent.'), ).toBeInTheDocument() }) - it('should render Close button', () => { - render() - - expect(screen.getByText('Close')).toBeInTheDocument() - }) - - it('should render Learn more button', () => { - render() + it('should not render the modal when dialogIsOpen is false', () => { + render() - expect(screen.getByText('Learn more')).toBeInTheDocument() + expect(screen.queryByText('Who is MX Technologies?')).not.toBeInTheDocument() }) }) describe('Interactions', () => { - it('should call setDialogIsOpen when dialog is closed via onClose', async () => { + it('should toggle dialogIsOpen when the Close button is clicked', async () => { const { user } = render() - const dialog = screen.getByRole('dialog') - expect(dialog).toBeInTheDocument() + await user.click(screen.getByText('Close')) - const backdrop = document.querySelector('.MuiBackdrop-root') - if (backdrop) { - await user.click(backdrop as HTMLElement) - } expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) + + // The updater flips the previous open state. + const toggle = mockSetDialogIsOpen.mock.calls[0][0] + expect(toggle(true)).toBe(false) + expect(toggle(false)).toBe(true) }) - it('should call setDialogIsOpen when Close button is clicked', async () => { + it('should toggle dialogIsOpen when the dialog is dismissed with Escape', async () => { const { user } = render() - const closeButton = screen.getByText('Close') - await user.click(closeButton) + expect(screen.getByRole('dialog')).toBeInTheDocument() + + await user.keyboard('{Escape}') expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) }) - it('should call goToUrlLink when Learn more button is clicked', async () => { + it('should open the MX company page when Learn more is clicked', async () => { const { user } = render() - const learnMoreButton = screen.getByText('Learn more') - await user.click(learnMoreButton) + await user.click(screen.getByText('Learn more')) expect(globalUtils.goToUrlLink).toHaveBeenCalledWith('https://www.mx.com/company/') }) - - it('should toggle state correctly when calling setDialogIsOpen function', () => { - render() - - const closeButton = screen.getByText('Close') - closeButton.click() - - expect(mockSetDialogIsOpen).toHaveBeenCalled() - - const toggleFunction = mockSetDialogIsOpen.mock.calls[0][0] - expect(toggleFunction(true)).toBe(false) - expect(toggleFunction(false)).toBe(true) - }) - }) - - describe('Styling', () => { - it('should apply dialog max width and min width styles', () => { - render() - - const dialog = screen.getByRole('dialog') - expect(dialog).toBeInTheDocument() - }) }) }) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx index 4170265900..82fa6641f1 100644 --- a/src/views/consent/DynamicDisclosure-test.tsx +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -72,8 +72,6 @@ describe('DynamicDisclosure', () => { expect(await screen.findByTestId('dynamic-disclosure-p1')).toBeInTheDocument() expect(await screen.findByText('I consent')).toBeInTheDocument() expect(await screen.findByText('Account Information')).toBeInTheDocument() - const buttons = screen.getAllByRole('button') - expect(buttons).toHaveLength(5) }) it('should render with app name when provided', () => { @@ -367,14 +365,6 @@ describe('DynamicDisclosure', () => { }) describe('Imperative handle', () => { - it('should expose handleBackButton method', () => { - const ref = React.createRef<{ handleBackButton: () => void }>() - render() - - expect(ref.current).toBeDefined() - expect(ref.current?.handleBackButton).toBeDefined() - }) - it('should call fadeOut and onGoBackClick when handleBackButton is called', async () => { const ref = React.createRef<{ handleBackButton: () => void }>() render() @@ -387,13 +377,6 @@ describe('DynamicDisclosure', () => { }) }) - it('should expose showBackButton method', () => { - const ref = React.createRef<{ showBackButton: () => boolean }>() - render() - - expect(ref.current?.showBackButton).toBeDefined() - }) - it('should return true for showBackButton when institution search is not disabled', () => { const state = { ...initialState, From e735c190a43ce759ceb70d1884822fb27caf10ae Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Mon, 29 Jun 2026 14:50:46 -0600 Subject: [PATCH 4/7] rendered a higher parent for more confidence --- src/views/consent/DynamicDisclosure-test.tsx | 246 +++++++------------ 1 file changed, 86 insertions(+), 160 deletions(-) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx index 82fa6641f1..2309d6fe02 100644 --- a/src/views/consent/DynamicDisclosure-test.tsx +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -1,10 +1,8 @@ import React from 'react' import { screen, render, waitFor } from 'src/utilities/testingLibrary' -import { DynamicDisclosure } from 'src/views/consent/DynamicDisclosure' +import RenderConnectStep from 'src/components/RenderConnectStep' import { initialState } from 'src/services/mockedData' -import { AGG_MODE, VERIFY_MODE } from 'src/const/Connect' -import { ActionTypes } from 'src/redux/actions/Connect' -import * as Animation from 'src/utilities/Animation' +import { AGG_MODE, VERIFY_MODE, STEPS } from 'src/const/Connect' import * as Intl from 'src/utilities/Intl' declare global { @@ -19,29 +17,55 @@ vi.mock('src/utilities/Animation', () => ({ fadeOut: vi.fn(() => Promise.resolve()), })) -const dispatch = vi.fn() -vi.mock('react-redux', async (importActual) => { - const actual = await importActual() - return { - ...actual, - useDispatch: () => dispatch, - } -}) - -const onConsentClick = vi.fn() -const onGoBackClick = vi.fn() - -const dynamicDisclosureProps = { - onConsentClick, - onGoBackClick, -} - const mockInstitution = { guid: 'INS-123', name: 'Test Bank', logo_url: 'https://example.com/logo.png', } +type DynamicDisclosureHandle = { + handleBackButton: () => void + showBackButton: () => boolean +} + +type StateOverrides = { + config?: Record + connect?: Record + profiles?: Record +} + +const renderConsentStep = (stateOverrides: StateOverrides = {}) => { + const navigationRef = React.createRef() + const handleConsentGoBack = vi.fn() + + const preloadedState = { + ...initialState, + config: { ...initialState.config, ...stateOverrides.config }, + profiles: { ...initialState.profiles, ...stateOverrides.profiles }, + connect: { + ...initialState.connect, + ...stateOverrides.connect, + location: [{ step: STEPS.CONSENT }], + selectedInstitution: mockInstitution, + }, + } + + const utils = render( + {}} + navigationRef={navigationRef} + onManualAccountAdded={() => {}} + onUpsertMember={() => {}} + setConnectLocalState={() => {}} + />, + { preloadedState }, + ) + + return { ...utils, navigationRef, handleConsentGoBack } +} + describe('DynamicDisclosure', () => { beforeEach(() => { vi.clearAllMocks() @@ -64,9 +88,8 @@ describe('DynamicDisclosure', () => { }) describe('Rendering', () => { - it('loads the consent screen', async () => { - const ref = React.createRef() - render() + it('renders the consent screen', async () => { + renderConsentStep() expect(await screen.findByTestId('dynamic-disclosure-title')).toBeInTheDocument() expect(await screen.findByTestId('dynamic-disclosure-p1')).toBeInTheDocument() @@ -75,59 +98,33 @@ describe('DynamicDisclosure', () => { }) it('should render with app name when provided', () => { - const state = { - ...initialState, + const { container } = renderConsentStep({ profiles: { - ...initialState.profiles, - client: { - ...initialState.profiles.client, - oauth_app_name: 'MyApp', - }, + client: { ...initialState.profiles.client, oauth_app_name: 'MyApp' }, }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, - }, - } - - const { container } = render(, { - preloadedState: state, }) expect(container.textContent).toContain('MyApp uses MX Technologies') }) it('should render without app name when not provided', () => { - const state = { - ...initialState, + const { container } = renderConsentStep({ profiles: { - ...initialState.profiles, - client: { - ...initialState.profiles.client, - oauth_app_name: null, - }, - }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, + client: { ...initialState.profiles.client, oauth_app_name: null }, }, - } - - const { container } = render(, { - preloadedState: state, }) expect(container.textContent).toContain('This app uses MX Technologies') }) it('should render Share your data title', () => { - render() + renderConsentStep() expect(screen.getByTestId('dynamic-disclosure-title')).toHaveTextContent('Share your data') }) it('should render PrivateAndSecure component', () => { - render() + renderConsentStep() expect(screen.getByText(/Private and secure/i)).toBeInTheDocument() }) @@ -135,84 +132,30 @@ describe('DynamicDisclosure', () => { describe('Mode-specific rendering', () => { it('should render AGG mode content when mode is AGG_MODE', () => { - const state = { - ...initialState, - config: { - ...initialState.config, - mode: AGG_MODE, - }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, - }, - } - - const { container } = render(, { - preloadedState: state, - }) + const { container } = renderConsentStep({ config: { mode: AGG_MODE } }) expect(container.textContent).toContain('manage your finances') }) it('should render VERIFY mode content when mode is VERIFY_MODE', () => { - const state = { - ...initialState, - config: { - ...initialState.config, - mode: VERIFY_MODE, - }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, - }, - } - - const { container } = render(, { - preloadedState: state, - }) + const { container } = renderConsentStep({ config: { mode: VERIFY_MODE } }) expect(container.textContent).toContain('move money') }) it('should render combined mode content when both AGG and VERIFY', () => { - const state = { - ...initialState, + const { container } = renderConsentStep({ config: { - ...initialState.config, mode: AGG_MODE, - data_request: { - products: ['transactions', 'identity_verification'], - }, - }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, + data_request: { products: ['transactions', 'identity_verification'] }, }, - } - - const { container } = render(, { - preloadedState: state, }) expect(container.textContent).toContain('move money and manage your finances') }) it('should render AGG mode when include_transactions is true', () => { - const state = { - ...initialState, - config: { - ...initialState.config, - include_transactions: true, - }, - connect: { - ...initialState.connect, - selectedInstitution: mockInstitution, - }, - } - - const { container } = render(, { - preloadedState: state, - }) + const { container } = renderConsentStep({ config: { include_transactions: true } }) expect(container.textContent).toContain('manage your finances') }) @@ -220,8 +163,7 @@ describe('DynamicDisclosure', () => { describe('Modal interaction', () => { it('loads the consent screen and clicks the info button to open modal', async () => { - const ref = React.createRef() - const { user } = render() + const { user } = renderConsentStep() await user.click(await screen.findByTestId('info-button')) @@ -229,7 +171,7 @@ describe('DynamicDisclosure', () => { }) it('should toggle modal when info button is clicked multiple times', async () => { - const { user } = render() + const { user } = renderConsentStep() const infoButton = screen.getByTestId('info-button') await user.click(infoButton) @@ -259,7 +201,7 @@ describe('DynamicDisclosure', () => { value: 800, }) - render() + renderConsentStep() const consentButton = screen.getByTestId('consent-button') expect(consentButton).toBeDisabled() @@ -282,14 +224,14 @@ describe('DynamicDisclosure', () => { value: 800, }) - render() + renderConsentStep() window.dispatchEvent(new Event('scroll')) const consentButton = await screen.findByTestId('consent-button') expect(consentButton).not.toBeDisabled() }) - it('should dispatch USER_CONSENTED action when consent button is clicked', async () => { + it('should advance to the enter credentials step when consent button is clicked', async () => { Object.defineProperty(document.documentElement, 'scrollHeight', { writable: true, configurable: true, @@ -306,7 +248,7 @@ describe('DynamicDisclosure', () => { value: 800, }) - const { user } = render() + const { user, store } = renderConsentStep() window.dispatchEvent(new Event('scroll')) @@ -318,7 +260,8 @@ describe('DynamicDisclosure', () => { const consentButton = screen.getByTestId('consent-button') await user.click(consentButton) - expect(dispatch).toHaveBeenCalledWith({ type: ActionTypes.USER_CONSENTED }) + const { location } = store.getState().connect + expect(location[location.length - 1].step).toBe(STEPS.ENTER_CREDENTIALS) }) }) @@ -327,7 +270,7 @@ describe('DynamicDisclosure', () => { window.app = { options: { language: 'es' } } vi.spyOn(Intl, 'getLocale').mockReturnValue('es') - render() + renderConsentStep() expect(screen.getByTestId('translation-button')).toBeInTheDocument() }) @@ -336,7 +279,7 @@ describe('DynamicDisclosure', () => { window.app = { options: { language: 'fr-ca' } } vi.spyOn(Intl, 'getLocale').mockReturnValue('fr-ca') - render() + renderConsentStep() expect(screen.getByTestId('translation-button')).toBeInTheDocument() }) @@ -345,7 +288,7 @@ describe('DynamicDisclosure', () => { window.app = { options: { language: 'en-us' } } vi.spyOn(Intl, 'getLocale').mockReturnValue('en') - render() + renderConsentStep() expect(screen.queryByTestId('translation-button')).not.toBeInTheDocument() }) @@ -355,7 +298,7 @@ describe('DynamicDisclosure', () => { const setLocaleSpy = vi.spyOn(Intl, 'setLocale') vi.spyOn(Intl, 'getLocale').mockReturnValue('es') - const { user } = render() + const { user } = renderConsentStep() const translationButton = screen.getByTestId('translation-button') await user.click(translationButton) @@ -365,46 +308,30 @@ describe('DynamicDisclosure', () => { }) describe('Imperative handle', () => { - it('should call fadeOut and onGoBackClick when handleBackButton is called', async () => { - const ref = React.createRef<{ handleBackButton: () => void }>() - render() + it('hands control back to the parent when the back button is triggered', async () => { + const { navigationRef, handleConsentGoBack } = renderConsentStep() - ref.current?.handleBackButton() + navigationRef.current?.handleBackButton() await waitFor(() => { - expect(Animation.fadeOut).toHaveBeenCalled() - expect(onGoBackClick).toHaveBeenCalled() + expect(handleConsentGoBack).toHaveBeenCalled() }) }) it('should return true for showBackButton when institution search is not disabled', () => { - const state = { - ...initialState, - config: { - ...initialState.config, - disable_institution_search: false, - }, - } - - const ref = React.createRef<{ showBackButton: () => boolean }>() - render(, { preloadedState: state }) + const { navigationRef } = renderConsentStep({ + config: { disable_institution_search: false }, + }) - expect(ref.current?.showBackButton()).toBe(true) + expect(navigationRef.current?.showBackButton()).toBe(true) }) it('should return false for showBackButton when institution search is disabled', () => { - const state = { - ...initialState, - config: { - ...initialState.config, - disable_institution_search: true, - }, - } - - const ref = React.createRef<{ showBackButton: () => boolean }>() - render(, { preloadedState: state }) + const { navigationRef } = renderConsentStep({ + config: { disable_institution_search: true }, + }) - expect(ref.current?.showBackButton()).toBe(false) + expect(navigationRef.current?.showBackButton()).toBe(false) }) it('should restore locale when handleBackButton is called with non-English initial locale', async () => { @@ -412,10 +339,9 @@ describe('DynamicDisclosure', () => { const setLocaleSpy = vi.spyOn(Intl, 'setLocale') vi.spyOn(Intl, 'getLocale').mockReturnValue('en') - const ref = React.createRef<{ handleBackButton: () => void }>() - render() + const { navigationRef } = renderConsentStep() - ref.current?.handleBackButton() + navigationRef.current?.handleBackButton() await waitFor(() => { expect(setLocaleSpy).toHaveBeenCalledWith('es') @@ -443,7 +369,7 @@ describe('DynamicDisclosure', () => { value: 800, }) - const { user } = render() + const { user } = renderConsentStep() window.dispatchEvent(new Event('scroll')) @@ -463,7 +389,7 @@ describe('DynamicDisclosure', () => { it('should remove scroll event listener on unmount', () => { const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener') - const { unmount } = render() + const { unmount } = renderConsentStep() unmount() From a2dccba0aad79d53f465edb9ec1367b545caa2a5 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Mon, 29 Jun 2026 16:08:57 -0600 Subject: [PATCH 5/7] fixed comments --- src/views/consent/ConsentModal-test.tsx | 63 ++++++++++++++------ src/views/consent/DynamicDisclosure-test.tsx | 4 -- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/views/consent/ConsentModal-test.tsx b/src/views/consent/ConsentModal-test.tsx index 3f6459f9d5..c7d0184a23 100644 --- a/src/views/consent/ConsentModal-test.tsx +++ b/src/views/consent/ConsentModal-test.tsx @@ -1,11 +1,38 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' +import RenderConnectStep from 'src/components/RenderConnectStep' import { ConsentModal } from 'src/views/consent/ConsentModal' -import * as globalUtils from 'src/utilities/global' - -vi.mock('src/utilities/global', () => ({ - goToUrlLink: vi.fn(), -})) +import { initialState } from 'src/services/mockedData' +import { STEPS } from 'src/const/Connect' + +const mockInstitution = { + guid: 'INS-123', + name: 'Test Bank', + logo_url: 'https://example.com/logo.png', +} + +const renderConsentStep = () => + render( + {}} + handleCredentialsGoBack={() => {}} + navigationRef={React.createRef()} + onManualAccountAdded={() => {}} + onUpsertMember={() => {}} + setConnectLocalState={() => {}} + />, + { + preloadedState: { + ...initialState, + connect: { + ...initialState.connect, + location: [{ step: STEPS.CONSENT }], + selectedInstitution: mockInstitution, + }, + }, + }, + ) describe('ConsentModal', () => { const mockSetDialogIsOpen = vi.fn() @@ -54,35 +81,37 @@ describe('ConsentModal', () => { }) describe('Interactions', () => { - it('should toggle dialogIsOpen when the Close button is clicked', async () => { - const { user } = render() + it('should close the modal when the Close button is clicked', async () => { + const { user } = renderConsentStep() - await user.click(screen.getByText('Close')) + await user.click(screen.getByTestId('info-button')) + expect(screen.getByText('Who is MX Technologies?')).toBeInTheDocument() - expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) + await user.click(screen.getByText('Close')) - // The updater flips the previous open state. - const toggle = mockSetDialogIsOpen.mock.calls[0][0] - expect(toggle(true)).toBe(false) - expect(toggle(false)).toBe(true) + expect(screen.queryByText('Who is MX Technologies?')).not.toBeInTheDocument() }) - it('should toggle dialogIsOpen when the dialog is dismissed with Escape', async () => { - const { user } = render() + it('should close the modal when dismissed with Escape', async () => { + const { user } = renderConsentStep() + await user.click(screen.getByTestId('info-button')) expect(screen.getByRole('dialog')).toBeInTheDocument() await user.keyboard('{Escape}') - expect(mockSetDialogIsOpen).toHaveBeenCalledWith(expect.any(Function)) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) it('should open the MX company page when Learn more is clicked', async () => { + const openSpy = vi.spyOn(window, 'open').mockReturnValue(null) const { user } = render() await user.click(screen.getByText('Learn more')) - expect(globalUtils.goToUrlLink).toHaveBeenCalledWith('https://www.mx.com/company/') + expect(openSpy).toHaveBeenCalledWith('https://www.mx.com/company/', '_blank') + + openSpy.mockRestore() }) }) }) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx index 2309d6fe02..21ceecfb50 100644 --- a/src/views/consent/DynamicDisclosure-test.tsx +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -13,10 +13,6 @@ declare global { } } -vi.mock('src/utilities/Animation', () => ({ - fadeOut: vi.fn(() => Promise.resolve()), -})) - const mockInstitution = { guid: 'INS-123', name: 'Test Bank', From b943e1bfd0de92adc3fe63fa2a9761b36fc4807f Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 30 Jun 2026 12:57:25 -0600 Subject: [PATCH 6/7] updated dynamic disclosure to render parent connect for more reliable tests --- src/views/consent/DynamicDisclosure-test.tsx | 98 +++++++++++++------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx index 21ceecfb50..8c826b242f 100644 --- a/src/views/consent/DynamicDisclosure-test.tsx +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -1,7 +1,12 @@ import React from 'react' -import { screen, render, waitFor } from 'src/utilities/testingLibrary' +import { act, createTestReduxStore, render, screen, waitFor } from 'src/utilities/testingLibrary' import RenderConnectStep from 'src/components/RenderConnectStep' -import { initialState } from 'src/services/mockedData' +import Connect from 'src/Connect' +import { ConnectedTokenProvider } from 'src/ConnectedTokenProvider' +import { PostMessageContext } from 'src/ConnectWidget' +import { ActionTypes } from 'src/redux/actions/Connect' +import { POST_MESSAGES } from 'src/const/postMessages' +import { initialState, institutionData } from 'src/services/mockedData' import { AGG_MODE, VERIFY_MODE, STEPS } from 'src/const/Connect' import * as Intl from 'src/utilities/Intl' @@ -62,6 +67,46 @@ const renderConsentStep = (stateOverrides: StateOverrides = {}) => { return { ...utils, navigationRef, handleConsentGoBack } } +const renderConsentWithNavigation = async (configOverrides: Partial = {}) => { + const store = createTestReduxStore() + const onPostMessage = vi.fn() + + const utils = render( + + + {}} + onSubmitConnectSuccessSurvey={() => {}} + profiles={initialState.profiles as unknown as ProfilesTypes} + userFeatures={{ items: [] }} + /> + + , + { store }, + ) + + await screen.findByTestId('navigation-header') + + act(() => { + store.dispatch({ + type: ActionTypes.SELECT_INSTITUTION_SUCCESS, + payload: { + institution: institutionData.institution, + institutionStatus: null, + consentIsEnabled: true, + additionalProductOption: null, + user: {}, + }, + }) + }) + + await screen.findByTestId('dynamic-disclosure-title') + + return { ...utils, store, onPostMessage } +} + describe('DynamicDisclosure', () => { beforeEach(() => { vi.clearAllMocks() @@ -87,10 +132,13 @@ describe('DynamicDisclosure', () => { it('renders the consent screen', async () => { renderConsentStep() - expect(await screen.findByTestId('dynamic-disclosure-title')).toBeInTheDocument() + expect(await screen.findByTestId('dynamic-disclosure-title')).toHaveTextContent( + 'Share your data', + ) expect(await screen.findByTestId('dynamic-disclosure-p1')).toBeInTheDocument() expect(await screen.findByText('I consent')).toBeInTheDocument() expect(await screen.findByText('Account Information')).toBeInTheDocument() + expect(screen.getByText(/Private and secure/i)).toBeInTheDocument() }) it('should render with app name when provided', () => { @@ -112,18 +160,6 @@ describe('DynamicDisclosure', () => { expect(container.textContent).toContain('This app uses MX Technologies') }) - - it('should render Share your data title', () => { - renderConsentStep() - - expect(screen.getByTestId('dynamic-disclosure-title')).toHaveTextContent('Share your data') - }) - - it('should render PrivateAndSecure component', () => { - renderConsentStep() - - expect(screen.getByText(/Private and secure/i)).toBeInTheDocument() - }) }) describe('Mode-specific rendering', () => { @@ -304,40 +340,38 @@ describe('DynamicDisclosure', () => { }) describe('Imperative handle', () => { - it('hands control back to the parent when the back button is triggered', async () => { - const { navigationRef, handleConsentGoBack } = renderConsentStep() + it('returns control to the parent when the global back button is clicked', async () => { + const { user, store, onPostMessage } = await renderConsentWithNavigation() - navigationRef.current?.handleBackButton() + await user.click(await screen.findByTestId('back-button')) await waitFor(() => { - expect(handleConsentGoBack).toHaveBeenCalled() + const { location } = store.getState().connect + expect(location[location.length - 1].step).toBe(STEPS.SEARCH) }) + expect(onPostMessage).toHaveBeenCalledWith(POST_MESSAGES.BACK_TO_SEARCH, {}) }) - it('should return true for showBackButton when institution search is not disabled', () => { - const { navigationRef } = renderConsentStep({ - config: { disable_institution_search: false }, - }) + it('shows the global back button when institution search is enabled', async () => { + await renderConsentWithNavigation({ disable_institution_search: false }) - expect(navigationRef.current?.showBackButton()).toBe(true) + expect(await screen.findByTestId('back-button')).toBeInTheDocument() }) - it('should return false for showBackButton when institution search is disabled', () => { - const { navigationRef } = renderConsentStep({ - config: { disable_institution_search: true }, - }) + it('hides the global back button when institution search is disabled', async () => { + await renderConsentWithNavigation({ disable_institution_search: true }) - expect(navigationRef.current?.showBackButton()).toBe(false) + expect(screen.queryByTestId('back-button')).not.toBeInTheDocument() }) - it('should restore locale when handleBackButton is called with non-English initial locale', async () => { + it('restores the initial locale when the back button is clicked', async () => { window.app = { options: { language: 'es' } } const setLocaleSpy = vi.spyOn(Intl, 'setLocale') vi.spyOn(Intl, 'getLocale').mockReturnValue('en') - const { navigationRef } = renderConsentStep() + const { user } = await renderConsentWithNavigation() - navigationRef.current?.handleBackButton() + await user.click(await screen.findByTestId('back-button')) await waitFor(() => { expect(setLocaleSpy).toHaveBeenCalledWith('es') From 677de3f6ed285384ab427910646dffe06bd3d322 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 30 Jun 2026 14:36:29 -0600 Subject: [PATCH 7/7] renamed test and checked for elements instead of state --- src/views/consent/DynamicDisclosure-test.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/views/consent/DynamicDisclosure-test.tsx b/src/views/consent/DynamicDisclosure-test.tsx index 8c826b242f..0bb2bb3f0a 100644 --- a/src/views/consent/DynamicDisclosure-test.tsx +++ b/src/views/consent/DynamicDisclosure-test.tsx @@ -280,7 +280,7 @@ describe('DynamicDisclosure', () => { value: 800, }) - const { user, store } = renderConsentStep() + const { user } = renderConsentStep() window.dispatchEvent(new Event('scroll')) @@ -292,8 +292,8 @@ describe('DynamicDisclosure', () => { const consentButton = screen.getByTestId('consent-button') await user.click(consentButton) - const { location } = store.getState().connect - expect(location[location.length - 1].step).toBe(STEPS.ENTER_CREDENTIALS) + expect(await screen.findByTestId('credentials-continue')).toBeInTheDocument() + expect(screen.queryByTestId('dynamic-disclosure-title')).not.toBeInTheDocument() }) }) @@ -340,15 +340,13 @@ describe('DynamicDisclosure', () => { }) describe('Imperative handle', () => { - it('returns control to the parent when the global back button is clicked', async () => { - const { user, store, onPostMessage } = await renderConsentWithNavigation() + it('navigates back to search when the global back button is clicked', async () => { + const { user, onPostMessage } = await renderConsentWithNavigation() await user.click(await screen.findByTestId('back-button')) - await waitFor(() => { - const { location } = store.getState().connect - expect(location[location.length - 1].step).toBe(STEPS.SEARCH) - }) + expect(await screen.findByTestId('search-header')).toBeInTheDocument() + expect(screen.queryByTestId('dynamic-disclosure-title')).not.toBeInTheDocument() expect(onPostMessage).toHaveBeenCalledWith(POST_MESSAGES.BACK_TO_SEARCH, {}) })