From 7e1fb7efc9a1fff122f946d2abe3a286a040c59c Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 11:48:49 -0600 Subject: [PATCH 1/2] fix: add app, context, and privacy tests --- .../__tests__/IEDeprecationDialog-test.tsx | 300 ++++++++++++++ src/const/__tests__/Accounts-test.tsx | 162 ++++++++ src/const/__tests__/jobDetailCode-test.tsx | 51 +++ src/context/__tests__/ApiContext-test.tsx | 382 ++++++++++++++++++ .../__tests__/WebSocketContext-test.tsx | 208 ++++++++++ src/privacy/__tests__/withProtection-test.tsx | 230 +++++++++++ 6 files changed, 1333 insertions(+) create mode 100644 src/components/app/__tests__/IEDeprecationDialog-test.tsx create mode 100644 src/const/__tests__/Accounts-test.tsx create mode 100644 src/const/__tests__/jobDetailCode-test.tsx create mode 100644 src/context/__tests__/ApiContext-test.tsx create mode 100644 src/context/__tests__/WebSocketContext-test.tsx create mode 100644 src/privacy/__tests__/withProtection-test.tsx diff --git a/src/components/app/__tests__/IEDeprecationDialog-test.tsx b/src/components/app/__tests__/IEDeprecationDialog-test.tsx new file mode 100644 index 0000000000..bb03ec6687 --- /dev/null +++ b/src/components/app/__tests__/IEDeprecationDialog-test.tsx @@ -0,0 +1,300 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import userEvent from '@testing-library/user-event' +import { initialState } from 'src/services/mockedData' +import { IEDeprecationDialog } from '../IEDeprecationDialog' +import { PageviewInfo } from 'src/const/Analytics' +import { isIE } from 'src/utilities/Browser' +import type { RootState } from 'src/redux/Store' + +vi.mock('src/utilities/Browser') + +describe('IEDeprecationDialog', () => { + const mockOnAnalyticPageview = vi.fn() + + const defaultProps = { + onAnalyticPageview: mockOnAnalyticPageview, + } + + const preloadedState = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: true, + }, + }, + } as unknown as Partial + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Rendering', () => { + it('renders dialog when isIE is true and feature flag is enabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + }) + + it('does not render when isIE is false', () => { + vi.mocked(isIE).mockReturnValue(false) + + render(, { preloadedState }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('does not render when feature flag is disabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutFlag = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: false, + }, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutFlag, + }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('does not render when widgetProfile is null', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutProfile = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: null, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutProfile, + }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('renders all text content', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + expect(screen.getByText(/We no longer support Internet Explorer/i)).toBeInTheDocument() + expect(screen.getByText('Continue')).toBeInTheDocument() + expect(screen.getByText(/Clicking the links to supported browsers/i)).toBeInTheDocument() + }) + + it('renders browser links with correct hrefs', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + const edgeLink = screen.getByText('Edge').closest('a') + const chromeLink = screen.getByText('Chrome').closest('a') + const firefoxLink = screen.getByText('Firefox').closest('a') + + expect(edgeLink).toHaveAttribute('href', 'https://www.microsoft.com/edge') + expect(edgeLink).toHaveAttribute('target', '_blank') + expect(edgeLink).toHaveAttribute('rel', 'noreferrer noopener') + + expect(chromeLink).toHaveAttribute('href', 'https://www.google.com/chrome/') + expect(chromeLink).toHaveAttribute('target', '_blank') + expect(chromeLink).toHaveAttribute('rel', 'noreferrer noopener') + + expect(firefoxLink).toHaveAttribute('href', 'https://www.mozilla.org/firefox/') + expect(firefoxLink).toHaveAttribute('target', '_blank') + expect(firefoxLink).toHaveAttribute('rel', 'noreferrer noopener') + }) + + it('renders close button with correct aria-label', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + expect(closeButton).toBeInTheDocument() + }) + }) + + describe('User Interactions', () => { + it('hides dialog when close button is clicked', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + await user.click(closeButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('hides dialog when continue button is clicked', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + const continueButton = screen.getByRole('button', { name: /continue/i }) + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + await user.click(continueButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('keeps dialog hidden after being closed', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + const { rerender } = render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + await user.click(closeButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + + rerender() + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + }) + + describe('Analytics', () => { + it('tracks pageview when dialog is shown', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).toHaveBeenCalledWith(PageviewInfo.CONNECT_IE_11_DEPRECATION[1]) + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + + it('does not track pageview when not IE', () => { + vi.mocked(isIE).mockReturnValue(false) + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).not.toHaveBeenCalled() + }) + + it('does not track pageview when feature flag is disabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutFlag = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: false, + }, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutFlag, + }) + + expect(mockOnAnalyticPageview).not.toHaveBeenCalled() + }) + + it('does not track pageview after dialog is closed', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + await user.click(closeButton) + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + }) + + describe('Integration', () => { + it('renders complete dialog structure with all elements', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByRole('button', { name: /close modal/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /continue/i })).toBeInTheDocument() + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + expect(screen.getByText('Edge')).toBeInTheDocument() + expect(screen.getByText('Chrome')).toBeInTheDocument() + expect(screen.getByText('Firefox')).toBeInTheDocument() + }) + + it('handles full user interaction flow', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + expect(mockOnAnalyticPageview).toHaveBeenCalledWith(PageviewInfo.CONNECT_IE_11_DEPRECATION[1]) + + const continueButton = screen.getByRole('button', { name: /continue/i }) + await user.click(continueButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + + it('respects all conditional rendering flags', () => { + const testCases = [ + { isIE: false, flag: false, shouldRender: false }, + { isIE: false, flag: true, shouldRender: false }, + { isIE: true, flag: false, shouldRender: false }, + { isIE: true, flag: true, shouldRender: true }, + ] + + testCases.forEach(({ isIE: ieValue, flag, shouldRender }) => { + vi.mocked(isIE).mockReturnValue(ieValue) + + const testState = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: flag, + }, + }, + } as unknown as Partial + + const { unmount } = render(, { + preloadedState: testState, + }) + + if (shouldRender) { + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + } else { + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + } + + unmount() + vi.clearAllMocks() + }) + }) + }) +}) diff --git a/src/const/__tests__/Accounts-test.tsx b/src/const/__tests__/Accounts-test.tsx new file mode 100644 index 0000000000..86001eb95f --- /dev/null +++ b/src/const/__tests__/Accounts-test.tsx @@ -0,0 +1,162 @@ +import { AccountTypeNames, ReadableAccountTypes } from '../Accounts' + +describe('Accounts Constants', () => { + describe('ReadableAccountTypes', () => { + it('should have UNKNOWN as 0', () => { + expect(ReadableAccountTypes.UNKNOWN).toBe(0) + }) + + it('should have CHECKING as 1', () => { + expect(ReadableAccountTypes.CHECKING).toBe(1) + }) + + it('should have SAVINGS as 2', () => { + expect(ReadableAccountTypes.SAVINGS).toBe(2) + }) + + it('should have LOAN as 3', () => { + expect(ReadableAccountTypes.LOAN).toBe(3) + }) + + it('should have CREDIT_CARD as 4', () => { + expect(ReadableAccountTypes.CREDIT_CARD).toBe(4) + }) + + it('should have INVESTMENT as 5', () => { + expect(ReadableAccountTypes.INVESTMENT).toBe(5) + }) + + it('should have LINE_OF_CREDIT as 6', () => { + expect(ReadableAccountTypes.LINE_OF_CREDIT).toBe(6) + }) + + it('should have MORTGAGE as 7', () => { + expect(ReadableAccountTypes.MORTGAGE).toBe(7) + }) + + it('should have PROPERTY as 8', () => { + expect(ReadableAccountTypes.PROPERTY).toBe(8) + }) + + it('should have CASH as 9', () => { + expect(ReadableAccountTypes.CASH).toBe(9) + }) + + it('should have INSURANCE as 10', () => { + expect(ReadableAccountTypes.INSURANCE).toBe(10) + }) + + it('should have PREPAID as 11', () => { + expect(ReadableAccountTypes.PREPAID).toBe(11) + }) + + it('should have CHECKING_LINE_OF_CREDIT as 12', () => { + expect(ReadableAccountTypes.CHECKING_LINE_OF_CREDIT).toBe(12) + }) + + it('should have exactly 13 account types', () => { + expect(Object.keys(ReadableAccountTypes)).toHaveLength(13) + }) + + it('should have all numeric values', () => { + Object.values(ReadableAccountTypes).forEach((value) => { + expect(typeof value).toBe('number') + }) + }) + + it('should have unique values', () => { + const values = Object.values(ReadableAccountTypes) + const uniqueValues = new Set(values) + expect(uniqueValues.size).toBe(values.length) + }) + }) + + describe('AccountTypeNames', () => { + it('should have 13 account type names', () => { + expect(AccountTypeNames).toHaveLength(13) + }) + + it('should have "Other" at index 0 for UNKNOWN', () => { + expect(AccountTypeNames[ReadableAccountTypes.UNKNOWN]).toBe('Other') + }) + + it('should have "Checking" at index 1 for CHECKING', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') + }) + + it('should have "Savings" at index 2 for SAVINGS', () => { + expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') + }) + + it('should have "Loan" at index 3 for LOAN', () => { + expect(AccountTypeNames[ReadableAccountTypes.LOAN]).toBe('Loan') + }) + + it('should have "Credit Card" at index 4 for CREDIT_CARD', () => { + expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') + }) + + it('should have "Investment" at index 5 for INVESTMENT', () => { + expect(AccountTypeNames[ReadableAccountTypes.INVESTMENT]).toBe('Investment') + }) + + it('should have "Line of Credit" at index 6 for LINE_OF_CREDIT', () => { + expect(AccountTypeNames[ReadableAccountTypes.LINE_OF_CREDIT]).toBe('Line of Credit') + }) + + it('should have "Mortgage" at index 7 for MORTGAGE', () => { + expect(AccountTypeNames[ReadableAccountTypes.MORTGAGE]).toBe('Mortgage') + }) + + it('should have "Property" at index 8 for PROPERTY', () => { + expect(AccountTypeNames[ReadableAccountTypes.PROPERTY]).toBe('Property') + }) + + it('should have "Cash" at index 9 for CASH', () => { + expect(AccountTypeNames[ReadableAccountTypes.CASH]).toBe('Cash') + }) + + it('should have "Insurance" at index 10 for INSURANCE', () => { + expect(AccountTypeNames[ReadableAccountTypes.INSURANCE]).toBe('Insurance') + }) + + it('should have "Prepaid" at index 11 for PREPAID', () => { + expect(AccountTypeNames[ReadableAccountTypes.PREPAID]).toBe('Prepaid') + }) + + it('should have "Checking" at index 12 for CHECKING_LINE_OF_CREDIT', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT]).toBe('Checking') + }) + + it('should have all string values', () => { + AccountTypeNames.forEach((name) => { + expect(typeof name).toBe('string') + }) + }) + }) + + describe('Integration between ReadableAccountTypes and AccountTypeNames', () => { + it('should map all ReadableAccountTypes to valid AccountTypeNames', () => { + Object.entries(ReadableAccountTypes).forEach(([_key, value]) => { + expect(AccountTypeNames[value]).toBeDefined() + expect(typeof AccountTypeNames[value]).toBe('string') + }) + }) + + it('should have correct mapping for UNKNOWN type', () => { + const name = AccountTypeNames[ReadableAccountTypes.UNKNOWN] + expect(name).toBe('Other') + }) + + it('should have correct mapping for standard account types', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') + expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') + expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') + }) + + it('should handle CHECKING_LINE_OF_CREDIT as Checking', () => { + const name = AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT] + expect(name).toBe('Checking') + }) + }) +}) diff --git a/src/const/__tests__/jobDetailCode-test.tsx b/src/const/__tests__/jobDetailCode-test.tsx new file mode 100644 index 0000000000..017071d0b3 --- /dev/null +++ b/src/const/__tests__/jobDetailCode-test.tsx @@ -0,0 +1,51 @@ +import { JOB_DETAIL_CODE } from '../jobDetailCode' + +describe('JOB_DETAIL_CODE Constants', () => { + describe('Structure', () => { + it('should be an object', () => { + expect(typeof JOB_DETAIL_CODE).toBe('object') + expect(JOB_DETAIL_CODE).not.toBeNull() + }) + + it('should have exactly 1 property', () => { + expect(Object.keys(JOB_DETAIL_CODE)).toHaveLength(1) + }) + + it('should have all numeric values', () => { + Object.values(JOB_DETAIL_CODE).forEach((value) => { + expect(typeof value).toBe('number') + }) + }) + + it('should have unique values', () => { + const values = Object.values(JOB_DETAIL_CODE) + const uniqueValues = new Set(values) + expect(uniqueValues.size).toBe(values.length) + }) + }) + + describe('NO_VERIFIABLE_ACCOUNTS', () => { + it('should exist', () => { + expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBeDefined() + }) + + it('should equal 1000', () => { + expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe(1000) + }) + + it('should be a number', () => { + expect(typeof JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe('number') + }) + }) + + describe('Export', () => { + it('should export JOB_DETAIL_CODE as a named export', () => { + expect(JOB_DETAIL_CODE).toBeDefined() + }) + + it('should not be frozen or sealed', () => { + expect(Object.isFrozen(JOB_DETAIL_CODE)).toBe(false) + expect(Object.isSealed(JOB_DETAIL_CODE)).toBe(false) + }) + }) +}) diff --git a/src/context/__tests__/ApiContext-test.tsx b/src/context/__tests__/ApiContext-test.tsx new file mode 100644 index 0000000000..b2ca6ced5e --- /dev/null +++ b/src/context/__tests__/ApiContext-test.tsx @@ -0,0 +1,382 @@ +import React from 'react' +import { render as rtlRender, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ApiProvider, useApi, defaultApiValue, type ApiContextTypes } from '../ApiContext' + +const TestComponent: React.FC = () => { + const { api } = useApi() + return ( +
+ + +
API Available
+
+ ) +} + +describe('ApiContext', () => { + describe('ApiProvider', () => { + it('should render children', () => { + rtlRender( + +
Test Child
+
, + ) + + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should provide default API values', () => { + rtlRender( + + + , + ) + + expect(screen.getByTestId('api-available')).toBeInTheDocument() + }) + + it('should merge custom API values with defaults', async () => { + const user = userEvent.setup() + const customLoadMembers = vi.fn(() => Promise.resolve([])) + const customApiValue = { + loadMembers: customLoadMembers, + } + + const { getByText } = rtlRender( + + + , + ) + + await user.click(getByText('Load Members')) + + expect(customLoadMembers).toHaveBeenCalled() + }) + + it('should allow custom API values to override defaults', async () => { + const user = userEvent.setup() + const customLoadInstitution = vi.fn(() => + Promise.resolve({ guid: 'INS-123', name: 'Test Bank' } as InstitutionResponseType), + ) + + const { getByText } = rtlRender( + + + , + ) + + await user.click(getByText('Load Institution')) + + expect(customLoadInstitution).toHaveBeenCalledWith('INS-123') + }) + }) + + describe('useApi hook', () => { + it('should return api object when used within ApiProvider', () => { + const TestComponentCheckApi = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') + }) + + it('should return default API values even when used outside provider', () => { + const TestComponentCheckApi = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender() + + expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') + }) + + it('should have all default API methods available', () => { + const TestComponentCheckMethods = () => { + const { api } = useApi() + return ( +
+
+ {typeof api.addMember === 'function' ? 'yes' : 'no'} +
+
+ {typeof api.loadMembers === 'function' ? 'yes' : 'no'} +
+
+ {typeof api.loadInstitutions === 'function' ? 'yes' : 'no'} +
+
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('has-addMember')).toHaveTextContent('yes') + expect(screen.getByTestId('has-loadMembers')).toHaveTextContent('yes') + expect(screen.getByTestId('has-loadInstitutions')).toHaveTextContent('yes') + }) + }) + + describe('defaultApiValue', () => { + it('should have createAccount function', async () => { + expect(defaultApiValue.createAccount).toBeDefined() + const result = await defaultApiValue.createAccount!({} as AccountCreateType) + expect(result).toBeDefined() + }) + + it('should have addMember function', async () => { + expect(defaultApiValue.addMember).toBeDefined() + const result = await defaultApiValue.addMember({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have deleteMember function', async () => { + expect(defaultApiValue.deleteMember).toBeDefined() + await expect(defaultApiValue.deleteMember({} as MemberDeleteType)).resolves.toBeUndefined() + }) + + it('should have getMemberCredentials function', async () => { + expect(defaultApiValue.getMemberCredentials).toBeDefined() + const result = await defaultApiValue.getMemberCredentials('MEM-123') + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadMemberByGuid function', async () => { + expect(defaultApiValue.loadMemberByGuid).toBeDefined() + const result = await defaultApiValue.loadMemberByGuid!('MEM-123') + expect(result).toBeDefined() + }) + + it('should have loadMembers function', async () => { + expect(defaultApiValue.loadMembers).toBeDefined() + const result = await defaultApiValue.loadMembers() + expect(Array.isArray(result)).toBe(true) + }) + + it('should have updateMember function', async () => { + expect(defaultApiValue.updateMember).toBeDefined() + const result = await defaultApiValue.updateMember({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have getInstitutionCredentials function', async () => { + expect(defaultApiValue.getInstitutionCredentials).toBeDefined() + const result = await defaultApiValue.getInstitutionCredentials('INS-123') + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadDiscoveredInstitutions function', async () => { + expect(defaultApiValue.loadDiscoveredInstitutions).toBeDefined() + const result = await defaultApiValue.loadDiscoveredInstitutions!({ + iso_country_code: 'US', + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadInstitutionByCode function', async () => { + expect(defaultApiValue.loadInstitutionByCode).toBeDefined() + const result = await defaultApiValue.loadInstitutionByCode!('mxbank') + expect(result).toBeDefined() + }) + + it('should have loadInstitutions function', async () => { + expect(defaultApiValue.loadInstitutions).toBeDefined() + const result = await defaultApiValue.loadInstitutions({ + routing_number: '123456789', + account_verification_is_enabled: true, + account_identification_is_enabled: false, + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadInstitutionByGuid function', async () => { + expect(defaultApiValue.loadInstitutionByGuid).toBeDefined() + const result = await defaultApiValue.loadInstitutionByGuid('INS-123') + expect(result).toBeDefined() + }) + + it('should have loadPopularInstitutions function', async () => { + expect(defaultApiValue.loadPopularInstitutions).toBeDefined() + const result = await defaultApiValue.loadPopularInstitutions({}) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have createMicrodeposit function', async () => { + expect(defaultApiValue.createMicrodeposit).toBeDefined() + const result = await defaultApiValue.createMicrodeposit!({} as MicrodepositCreateType) + expect(result).toBeDefined() + }) + + it('should have loadMicrodepositByGuid function', async () => { + expect(defaultApiValue.loadMicrodepositByGuid).toBeDefined() + const result = await defaultApiValue.loadMicrodepositByGuid!('MICRO-123') + expect(result).toBeDefined() + }) + + it('should have refreshMicrodepositStatus function', async () => { + expect(defaultApiValue.refreshMicrodepositStatus).toBeDefined() + await expect(defaultApiValue.refreshMicrodepositStatus!('MICRO-123')).resolves.toBeUndefined() + }) + + it('should have updateMicrodeposit function', async () => { + expect(defaultApiValue.updateMicrodeposit).toBeDefined() + const result = await defaultApiValue.updateMicrodeposit!( + 'MICRO-123', + {} as MicrodepositUpdateType, + ) + expect(result).toBeDefined() + }) + + it('should have verifyMicrodeposit function', async () => { + expect(defaultApiValue.verifyMicrodeposit).toBeDefined() + const result = await defaultApiValue.verifyMicrodeposit!( + 'MICRO-123', + {} as MicroDepositVerifyType, + ) + expect(result).toBeDefined() + }) + + it('should have verifyRoutingNumber function', async () => { + expect(defaultApiValue.verifyRoutingNumber).toBeDefined() + const result = await defaultApiValue.verifyRoutingNumber!('123456789', true) + expect(result).toBeDefined() + }) + + it('should have updateMFA function', async () => { + expect(defaultApiValue.updateMFA).toBeDefined() + const result = await defaultApiValue.updateMFA({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have loadOAuthState function', async () => { + expect(defaultApiValue.loadOAuthState).toBeDefined() + const result = await defaultApiValue.loadOAuthState('OAUTH-123') + expect(result).toBeDefined() + }) + + it('should have loadOAuthStates function', async () => { + expect(defaultApiValue.loadOAuthStates).toBeDefined() + const result = await defaultApiValue.loadOAuthStates({ + outbound_member_guid: 'MEM-123', + auth_status: 'pending', + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have oAuthStart function', async () => { + expect(defaultApiValue.oAuthStart).toBeDefined() + await expect(defaultApiValue.oAuthStart!({ member: {} })).resolves.toBeUndefined() + }) + + it('should have createSupportTicket function', async () => { + expect(defaultApiValue.createSupportTicket).toBeDefined() + await expect( + defaultApiValue.createSupportTicket!({} as SupportTicketType), + ).resolves.toBeUndefined() + }) + + it('should have loadJob function', async () => { + expect(defaultApiValue.loadJob).toBeDefined() + const result = await defaultApiValue.loadJob('JOB-123') + expect(result).toBeDefined() + }) + + it('should have runJob function', async () => { + expect(defaultApiValue.runJob).toBeDefined() + const result = await defaultApiValue.runJob( + 'aggregate', + 'MEM-123', + {} as ClientConfigType, + true, + ) + expect(result).toBeDefined() + }) + + it('should have updateUserProfile function', async () => { + expect(defaultApiValue.updateUserProfile).toBeDefined() + const result = await defaultApiValue.updateUserProfile!({ + userProfile: {}, + too_small_modal_dismissed_at: '2024-01-01', + }) + expect(result).toBeDefined() + }) + }) + + describe('Integration tests', () => { + it('should allow calling API methods from components', async () => { + const user = userEvent.setup() + const mockLoadMembers = vi.fn(() => + Promise.resolve([ + { guid: 'MEM-1', name: 'Member 1' }, + { guid: 'MEM-2', name: 'Member 2' }, + ] as MemberResponseType[]), + ) + + const TestComponentWithApi = () => { + const { api } = useApi() + const [members, setMembers] = React.useState([]) + + const handleLoad = async () => { + const result = await api.loadMembers() + setMembers(result) + } + + return ( +
+ +
{members.length}
+
+ ) + } + + const { getByText, getByTestId } = rtlRender( + + + , + ) + + expect(getByTestId('member-count')).toHaveTextContent('0') + + await user.click(getByText('Load')) + + expect(mockLoadMembers).toHaveBeenCalled() + expect(getByTestId('member-count')).toHaveTextContent('2') + }) + + it('should allow multiple components to access the same API context', () => { + const Component1 = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + const Component2 = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender( + + + + , + ) + + expect(screen.getByTestId('comp1')).toHaveTextContent('Has API') + expect(screen.getByTestId('comp2')).toHaveTextContent('Has API') + }) + }) +}) diff --git a/src/context/__tests__/WebSocketContext-test.tsx b/src/context/__tests__/WebSocketContext-test.tsx new file mode 100644 index 0000000000..6c2d2e0a81 --- /dev/null +++ b/src/context/__tests__/WebSocketContext-test.tsx @@ -0,0 +1,208 @@ +import React from 'react' +import { render as rtlRender, screen } from '@testing-library/react' +import { of, Subject } from 'rxjs' +import { WebSocketProvider, useWebSocket, WebSocketConnection } from '../WebSocketContext' + +describe('WebSocketContext', () => { + describe('WebSocketProvider', () => { + it('should render children', () => { + rtlRender( + +
Test Child
+
, + ) + + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should provide undefined value when no value prop is passed', () => { + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket === undefined ? 'undefined' : 'defined'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('ws-value')).toHaveTextContent('undefined') + }) + + it('should provide WebSocket connection when value prop is passed', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + return ( +
+ {webSocket?.isConnected() ? 'connected' : 'disconnected'} +
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('ws-connected')).toHaveTextContent('connected') + }) + }) + + describe('useWebSocket hook', () => { + it('should return undefined when used without provider value', () => { + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket ? 'has value' : 'no value'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('result')).toHaveTextContent('no value') + }) + + it('should return WebSocket connection when provided', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({ type: 'message' }), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket ? 'has value' : 'no value'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('result')).toHaveTextContent('has value') + }) + + it('should allow accessing isConnected method', () => { + const mockConnection: WebSocketConnection = { + isConnected: vi.fn(() => true), + webSocketMessages$: of({}), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + const connected = webSocket?.isConnected() + return
{connected ? 'connected' : 'disconnected'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('status')).toHaveTextContent('connected') + expect(mockConnection.isConnected).toHaveBeenCalled() + }) + + it('should allow subscribing to webSocketMessages$', async () => { + const messageSubject = new Subject() + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: messageSubject.asObservable(), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + const [message, setMessage] = React.useState('') + + React.useEffect(() => { + if (webSocket) { + const subscription = webSocket.webSocketMessages$.subscribe((msg: { text: string }) => { + setMessage(msg.text) + }) + return () => subscription.unsubscribe() + } + return undefined + }, [webSocket]) + + return
{message || 'no message'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('message')).toHaveTextContent('no message') + + messageSubject.next({ text: 'Hello WebSocket' }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(screen.getByTestId('message')).toHaveTextContent('Hello WebSocket') + }) + }) + + describe('Integration tests', () => { + it('should allow multiple components to access the same WebSocket connection', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } + + const Component1 = () => { + const ws = useWebSocket() + return
{ws?.isConnected() ? 'connected' : 'disconnected'}
+ } + + const Component2 = () => { + const ws = useWebSocket() + return
{ws?.isConnected() ? 'connected' : 'disconnected'}
+ } + + rtlRender( + + + + , + ) + + expect(screen.getByTestId('comp1')).toHaveTextContent('connected') + expect(screen.getByTestId('comp2')).toHaveTextContent('connected') + }) + + it('should handle disconnected state', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({}), + } + + const TestComponent = () => { + const ws = useWebSocket() + return ( +
+ {ws?.isConnected() ? 'Connected' : 'Disconnected'} +
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('connection-status')).toHaveTextContent('Disconnected') + }) + }) +}) diff --git a/src/privacy/__tests__/withProtection-test.tsx b/src/privacy/__tests__/withProtection-test.tsx new file mode 100644 index 0000000000..9a72b5da27 --- /dev/null +++ b/src/privacy/__tests__/withProtection-test.tsx @@ -0,0 +1,230 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { maskInputFn, withProtection } from '../withProtection' +import { render } from '../../utilities/testingLibrary' + +describe('maskInputFn', () => { + it('should mask input text with asterisks by default', () => { + const result = maskInputFn('password123') + expect(result).toBe('***********') + }) + + it('should mask input text when no element is provided', () => { + const result = maskInputFn('secretText') + expect(result).toBe('**********') + }) + + it('should mask input text when element does not have unmask attribute', () => { + const element = document.createElement('input') + const result = maskInputFn('myPassword', element) + expect(result).toBe('**********') + }) + + it('should return original text when element has data-ph-unmask="true"', () => { + const element = document.createElement('input') + element.setAttribute('data-ph-unmask', 'true') + const result = maskInputFn('plainText123', element) + expect(result).toBe('plainText123') + }) + + it('should mask text when element has data-ph-unmask="false"', () => { + const element = document.createElement('input') + element.setAttribute('data-ph-unmask', 'false') + const result = maskInputFn('secretData', element) + expect(result).toBe('**********') + }) + + it('should mask empty string', () => { + const result = maskInputFn('') + expect(result).toBe('') + }) + + it('should mask single character', () => { + const result = maskInputFn('x') + expect(result).toBe('*') + }) +}) + +describe('withProtection', () => { + it('should wrap component with ph-no-capture class by default', () => { + const TestComponent = ({ 'data-test': dataTest }: { 'data-test': string }) => ( +
Sensitive Content
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeTruthy() + expect(screen.getByTestId('test-component')).toHaveTextContent('Sensitive Content') + }) + + it('should not wrap component when allowCapture is true', () => { + const TestComponent = ({ 'data-test': dataTest }: { 'data-test': string }) => ( +
Public Content
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeNull() + expect(screen.getByTestId('test-component')).toHaveTextContent('Public Content') + }) + + it('should add data-ph-unmask attribute when allowCapture is true', () => { + const TestComponent = React.forwardRef< + HTMLInputElement, + { 'data-test': string; 'data-ph-unmask'?: boolean } + >((props, ref) => ) + TestComponent.displayName = 'TestComponent' + + const ProtectedComponent = withProtection(TestComponent) + + render() + + const input = screen.getByTestId('test-input') + expect(input.getAttribute('data-ph-unmask')).toBe('true') + }) + + it('should not add data-ph-unmask attribute when allowCapture is false', () => { + const TestComponent = React.forwardRef< + HTMLInputElement, + { 'data-test': string; 'data-ph-unmask'?: boolean } + >((props, ref) => ) + TestComponent.displayName = 'TestComponent' + + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeTruthy() + + const input = screen.getByTestId('test-input') + expect(input.hasAttribute('data-ph-unmask')).toBe(false) + }) + + it('should pass through other props correctly', () => { + const TestComponent = ({ + 'data-test': dataTest, + className, + id, + }: { + 'data-test': string + className?: string + id?: string + }) => ( +
+ Content +
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render( + , + ) + + const element = screen.getByTestId('test-component') + expect(element).toHaveClass('custom-class') + expect(element).toHaveAttribute('id', 'custom-id') + }) + + it('should forward ref correctly', () => { + const TestComponent = React.forwardRef< + HTMLButtonElement, + { 'data-test': string; children: React.ReactNode } + >((props, ref) =>