From a1fd3d17b06880fb8bd599e7ae059bc633bd053e Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 13:12:05 -0600 Subject: [PATCH 1/3] 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/3] 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/3] 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,