From 7e1fb7efc9a1fff122f946d2abe3a286a040c59c Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 11:48:49 -0600 Subject: [PATCH 1/5] 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) => - -
API Available
- - ) -} +import { describe, it, expect, vi } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { initialState } from 'src/services/mockedData' +import { ApiProvider, useApi } from 'src/context/ApiContext' +import { CreateMemberForm } from 'src/views/credentials/CreateMemberForm' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' 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') - }) + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + current_institution_guid: 'INS-123', + selectedInstitution: { + guid: 'INS-123', + code: 'mxbank', + name: 'MX Bank', + }, + institutions: [ + { + guid: 'INS-123', + code: 'mxbank', + name: 'MX Bank', + }, + ], + }, + } + + const defaultProps = { + onError: () => {}, + onSuccess: () => {}, + } + + it('provides API to child components', async () => { + const mockGetInstitutionCredentials = vi.fn().mockResolvedValue([ + { + guid: 'CRD-1', + label: 'Username', + field_name: 'username', + field_type: 'TEXT', + }, + ]) + + render( + + + , + { preloadedState }, + ) + + await waitFor(() => { + expect(mockGetInstitutionCredentials).toHaveBeenCalledWith('INS-123') + }) + + expect(screen.getByText('Username')).toBeInTheDocument() }) - 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') - }) + it('allows custom API values to be provided', async () => { + const customGetInstitutionCredentials = vi.fn().mockResolvedValue([ + { + guid: 'CRD-2', + label: 'Password', + field_name: 'password', + field_type: 'PASSWORD', + }, + ]) + + render( + + + , + { preloadedState }, + ) + + await waitFor(() => { + expect(customGetInstitutionCredentials).toHaveBeenCalledWith('INS-123') + }) + + expect(screen.getByText('Password')).toBeInTheDocument() }) - 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[]), + it('provides default API values when used outside provider', () => { + const TestComponent = () => { + const { api } = useApi() + return ( +
+
{typeof api.loadMembers === 'function' ? 'yes' : 'no'}
+
) + } - const TestComponentWithApi = () => { - const { api } = useApi() - const [members, setMembers] = React.useState([]) + render(, { preloadedState }) - 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') - }) + expect(screen.getByTestId('has-api')).toHaveTextContent('yes') }) }) diff --git a/src/context/ApiContext.tsx b/src/context/ApiContext.tsx index 0be06eccb8..540f8affc0 100644 --- a/src/context/ApiContext.tsx +++ b/src/context/ApiContext.tsx @@ -141,9 +141,6 @@ const ApiProvider = ({ apiValue, children }: ApiProviderTypes) => { const useApi = () => { const context = React.useContext(ApiContext) - if (context === undefined) { - throw new Error('useApi must be used within a ApiProvider') - } return { api: context } } diff --git a/src/context/WebSocketContext-test.tsx b/src/context/WebSocketContext-test.tsx index cb9bc2fefe..79c791d5fc 100644 --- a/src/context/WebSocketContext-test.tsx +++ b/src/context/WebSocketContext-test.tsx @@ -1,208 +1,74 @@ import React from 'react' -import { render as rtlRender, screen } from '@testing-library/react' -import { of, Subject } from 'rxjs' +import { renderHook } from '@testing-library/react' +import { of } from 'rxjs' import { WebSocketProvider, useWebSocket, WebSocketConnection } from 'src/context/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 return undefined when no WebSocket connection is provided', () => { + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => {children}, }) - 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') - }) + expect(result.current).toBeUndefined() }) - describe('useWebSocket hook', () => { - it('should return undefined when used without provider value', () => { - const TestComponent = () => { - const webSocket = useWebSocket() - return
{webSocket ? 'has value' : 'no value'}
- } - - rtlRender( - - - , - ) + it('should return the WebSocket connection when provided', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } - expect(screen.getByTestId('result')).toHaveTextContent('no value') + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - 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'}
- } + expect(result.current).toBe(mockConnection) + expect(result.current?.isConnected()).toBe(true) + }) - rtlRender( - - - , - ) + it('should allow accessing webSocketMessages$ observable', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({ event: 'test', payload: { id: 123 } }), + } - expect(screen.getByTestId('result')).toHaveTextContent('has value') + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - it('should allow accessing isConnected method', () => { - const mockConnection: WebSocketConnection = { - isConnected: vi.fn(() => true), - webSocketMessages$: of({}), - } + expect(result.current?.webSocketMessages$).toBeDefined() - const TestComponent = () => { - const webSocket = useWebSocket() - const connected = webSocket?.isConnected() - return
{connected ? 'connected' : 'disconnected'}
- } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('status')).toHaveTextContent('connected') - expect(mockConnection.isConnected).toHaveBeenCalled() + let receivedMessage: unknown + result.current?.webSocketMessages$.subscribe((msg) => { + receivedMessage = msg }) - 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') - }) + expect(receivedMessage).toEqual({ event: 'test', payload: { id: 123 } }) }) - describe('Integration tests', () => { - it('should allow multiple components to access the same WebSocket connection', () => { - const mockConnection: WebSocketConnection = { - isConnected: () => true, - webSocketMessages$: of({ type: 'test' }), - } + it('should provide the same connection to multiple consumers', () => { + const mockConnection: WebSocketConnection = { + isConnected: vi.fn(() => true), + webSocketMessages$: of({}), + } - 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') + const { result: result1 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - 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') + const { result: result2 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) + + expect(result1.current).toBe(mockConnection) + expect(result2.current).toBe(mockConnection) }) }) diff --git a/src/privacy/withProtection-test.tsx b/src/privacy/withProtection-test.tsx index a475e06461..9dd2963d46 100644 --- a/src/privacy/withProtection-test.tsx +++ b/src/privacy/withProtection-test.tsx @@ -5,45 +5,17 @@ import { maskInputFn, withProtection } from 'src/privacy/withProtection' import { render } from 'src/utilities/testingLibrary' describe('maskInputFn', () => { - it('should mask input text with asterisks by default', () => { + it('should mask input text 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', () => { @@ -87,144 +59,4 @@ describe('withProtection', () => { 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) => - - - - {__('This browser is not supported')} - - - { - // --TR: Full String: "We no longer support Internet Explorer. You can continue, or switch to a supported browser, like Edge, Chrome, or Firefox, for a better experience." - __( - 'We no longer support Internet Explorer. You can continue, or switch to a supported browser, like ', - ) - } - - {__('Edge')} - - {', '} - - {__('Chrome')} - - {', or '} - - {__('Firefox')} - - {', '} - {__(' for a better experience.')} - - - - {__( - 'Clicking the links to supported browsers will take you to an external website with a different privacy policy, security measures, and terms and conditions.', - )} - - - ) : null -} - -const getStyles = (tokens) => ({ - container: { - background: tokens.BackgroundColor.Modal, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: `0 ${tokens.Spacing.ContainerSidePadding}px`, - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - maxWidth: '352px', // Our max content width (does not include side margin) - minWidth: '270px', // Our min content width (does not include side margin) - margin: '0 auto', - }, - header: { - position: 'absolute', - display: 'flex', - justifyContent: 'flex-end', - width: '100%', - }, - closeButton: { - marginTop: tokens.Spacing.XSmall, - }, - title: { - textAlign: 'center', - marginBottom: tokens.Spacing.Tiny, - }, - paragraph: { - textAlign: 'center', - }, - continueButton: { - marginTop: tokens.Spacing.XLarge, - marginBottom: tokens.Spacing.Medium, - }, - icon: { - marginBottom: tokens.Spacing.Large, - marginTop: tokens.Spacing.Jumbo, - paddingTop: tokens.Spacing.Tiny, - }, -}) - -IEDeprecationDialog.propTypes = { - onAnalyticPageview: PropTypes.func.isRequired, -} From 1a3cef51a536906b46ef86a86e33c6f87d2cbd98 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Fri, 26 Jun 2026 13:11:57 -0600 Subject: [PATCH 5/5] removed unneeded tests --- src/const/Accounts-test.tsx | 162 ------------------------------- src/const/jobDetailCode-test.tsx | 51 ---------- 2 files changed, 213 deletions(-) delete mode 100644 src/const/Accounts-test.tsx delete mode 100644 src/const/jobDetailCode-test.tsx diff --git a/src/const/Accounts-test.tsx b/src/const/Accounts-test.tsx deleted file mode 100644 index 5f4d1ffeb9..0000000000 --- a/src/const/Accounts-test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { AccountTypeNames, ReadableAccountTypes } from 'src/const/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/jobDetailCode-test.tsx b/src/const/jobDetailCode-test.tsx deleted file mode 100644 index e1f51c6943..0000000000 --- a/src/const/jobDetailCode-test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { JOB_DETAIL_CODE } from 'src/const/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) - }) - }) -})