From 4a4c7454cfce27ff7d096a27de5a7793851fea75 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 16 Oct 2024 08:46:57 -0400 Subject: [PATCH 1/2] Adding tests to transactions pages --- .../[[...financialAccount]].page.test.tsx | 160 ++++++++ .../AccountTransactions.test.tsx | 346 ++++++++++++++++++ .../AccountTransactions.tsx | 53 +-- .../AccountTransactionsMocks.ts | 66 ++++ .../FinancialAccounts.test.tsx | 73 ++-- .../FinancialAccountsMocks.ts | 41 +++ .../Header/Header.test.tsx | 174 +++++++++ .../FinancialAccountsReport/Header/Header.tsx | 2 +- 8 files changed, 840 insertions(+), 75 deletions(-) create mode 100644 pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx create mode 100644 src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx create mode 100644 src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionsMocks.ts create mode 100644 src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccountsMocks.ts create mode 100644 src/components/Reports/FinancialAccountsReport/Header/Header.test.tsx diff --git a/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx b/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx new file mode 100644 index 000000000..dd032cc24 --- /dev/null +++ b/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SnackbarProvider } from 'notistack'; +import { I18nextProvider } from 'react-i18next'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { + defaultFinancialAccount, + defaultFinancialAccountSummary, +} from 'src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummaryMock'; +import { FinancialAccountSummaryQuery } from 'src/components/Reports/FinancialAccountsReport/AccountSummary/financialAccountSummary.generated'; +import { FinancialAccountQuery } from 'src/components/Reports/FinancialAccountsReport/Context/FinancialAccount.generated'; +import { FinancialAccountsQuery } from 'src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccounts.generated'; +import { FinancialAccountsMock } from 'src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccountsMocks'; +import i18n from 'src/lib/i18n'; +import theme from 'src/theme'; +import FinancialAccountsPage from './[[...financialAccount]].page'; + +const accountListId = 'account-list-1'; +const financialAccountId = 'financialAccountId'; +const defaultRouter = { + query: { accountListId }, + isReady: true, +}; + +const Components = ({ router = defaultRouter }: { router?: object }) => ( + + + + + + + mocks={{ + FinancialAccountSummary: defaultFinancialAccountSummary, + FinancialAccount: defaultFinancialAccount, + ...FinancialAccountsMock, + }} + > + + + + + + + +); + +describe('Financial Accounts Page', () => { + it('should show initial financial accounts page', async () => { + const { findByText } = render(); + + expect(await findByText('Responsibility Centers')).toBeInTheDocument(); + + expect(await findByText('Test Account')).toBeInTheDocument(); + }); + + it('should show the summary page for a financial account', async () => { + const { findByText, findByRole, getByText, queryByText } = render( + , + ); + + expect(await findByText('Account 1')).toBeInTheDocument(); + + expect(queryByText('Responsibility Centers')).not.toBeInTheDocument(); + + expect( + await findByRole('heading', { name: 'Category' }), + ).toBeInTheDocument(); + + expect(getByText('Opening Balance')).toBeInTheDocument(); + }); + + it('should show the transactions page for a financial account', async () => { + const { findByText, findByRole, getByText, queryByText, queryByRole } = + render( + , + ); + + expect(await findByText('Account 1')).toBeInTheDocument(); + + expect( + await findByRole('button', { name: 'Export CSV' }), + ).toBeInTheDocument(); + + expect(getByText('Totals for Period')).toBeInTheDocument(); + + expect(queryByText('Responsibility Centers')).not.toBeInTheDocument(); + expect( + queryByRole('heading', { name: 'Category' }), + ).not.toBeInTheDocument(); + }); + + it('should open filters on load and set initial date Range filter', async () => { + const { findByRole } = render( + , + ); + + expect( + await findByRole('heading', { name: 'Filter (1)' }), + ).toBeInTheDocument(); + }); + + it('should open and close filters and menu', async () => { + const { findByRole, getByRole, queryByRole } = render( + , + ); + + // Filters + expect( + await findByRole('heading', { name: 'Filter (1)' }), + ).toBeInTheDocument(); + userEvent.click(getByRole('img', { name: 'Close' })); + expect( + queryByRole('heading', { name: 'Filter (1)' }), + ).not.toBeInTheDocument(); + + // Menu + userEvent.click(getByRole('img', { name: 'Toggle Menu Panel' })); + expect(getByRole('heading', { name: 'Reports' })).toBeInTheDocument(); + userEvent.click(getByRole('img', { name: 'Close' })); + expect(queryByRole('heading', { name: 'Reports' })).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx new file mode 100644 index 000000000..08ac0fb3e --- /dev/null +++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx @@ -0,0 +1,346 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { SnackbarProvider } from 'notistack'; +import { I18nextProvider } from 'react-i18next'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { + FinancialAccountTransactionFilters, + FinancialAccountsWrapper, +} from 'pages/accountLists/[accountListId]/reports/financialAccounts/Wrapper'; +import { Panel } from 'pages/accountLists/[accountListId]/reports/helpers'; +import { defaultFinancialAccount } from 'src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummaryMock'; +import i18n from 'src/lib/i18n'; +import theme from 'src/theme'; +import { FinancialAccountQuery } from '../Context/FinancialAccount.generated'; +import { + FinancialAccountContext, + FinancialAccountType, +} from '../Context/FinancialAccountsContext'; +import { + AccountTransactions, + defaultEndDate, + defaultStartDate, +} from './AccountTransactions'; +import { financialAccountEntriesMock } from './AccountTransactionsMocks'; +import { FinancialAccountEntriesQuery } from './financialAccountTransactions.generated'; + +const accountListId = 'account-list-1'; +const financialAccountId = 'financialAccountId'; +const defaultSearchTerm = ''; +const setActiveFilters = jest.fn(); +const setPanelOpen = jest.fn(); +const defaultActiveFilters = {}; +const defaultRouter = { + query: { accountListId }, + isReady: true, +}; +const mutationSpy = jest.fn(); + +interface ComponentsProps { + activeFilters?: FinancialAccountTransactionFilters; + searchTerm?: string; + router?: object; +} + +const Components = ({ + searchTerm = defaultSearchTerm, + activeFilters = defaultActiveFilters, + router = defaultRouter, +}: ComponentsProps) => ( + + + + + + + mocks={{ + FinancialAccount: defaultFinancialAccount, + FinancialAccountEntries: financialAccountEntriesMock, + }} + onCall={mutationSpy} + > + + + + + + + + + + + +); + +describe('Financial Account Transactions', () => { + describe('Resetting filters', () => { + it('should reset the activeFilters with the filters from the url filters', () => { + render( + , + ); + + expect(setActiveFilters).not.toHaveBeenCalledWith({ + dateRange: { + min: defaultStartDate, + max: defaultEndDate, + }, + }); + + expect(setActiveFilters).toHaveBeenCalledWith({ + categoryId: 'newCategoryId', + }); + expect(setPanelOpen).toHaveBeenCalledWith(Panel.Filters); + }); + + it('should set filters to default date range if no activeFilters or url filters', () => { + render(); + + expect(setActiveFilters).toHaveBeenCalledWith({ + dateRange: { + min: defaultStartDate, + max: defaultEndDate, + }, + }); + }); + }); + + it('should render data correctly', async () => { + const { findAllByText, getAllByText, getByText } = render(); + + expect(await findAllByText('category1Name')).toHaveLength(2); + expect(getAllByText('category1Code')).toHaveLength(2); + + // Header + expect(getByText('Closing Balance')).toBeInTheDocument(); + expect(getByText('UAH 280,414')).toBeInTheDocument(); + + // Row 1 + expect(getByText('8/9/2024')).toBeInTheDocument(); + expect(getByText('description3')).toBeInTheDocument(); + expect(getByText('code3')).toBeInTheDocument(); + expect(getByText('UAH 7,048')).toBeInTheDocument(); + + // Row 2 + expect(getByText('8/8/2024')).toBeInTheDocument(); + expect(getByText('description1')).toBeInTheDocument(); + expect(getByText('code1')).toBeInTheDocument(); + expect(getByText('UAH 15,008')).toBeInTheDocument(); + + // Row 3 + expect(getByText('8/7/2024')).toBeInTheDocument(); + expect(getByText('description2')).toBeInTheDocument(); + expect(getByText('code2')).toBeInTheDocument(); + expect(getByText('UAH 37')).toBeInTheDocument(); + + // Footer + expect(getByText('Opening Balance')).toBeInTheDocument(); + expect(getByText('UAH 202,240')).toBeInTheDocument(); + + // Totals + expect(getByText('Income:')).toBeInTheDocument(); + expect(getByText('UAH 307,519')).toBeInTheDocument(); + + expect(getByText('Expenses:')).toBeInTheDocument(); + expect(getByText('UAH 229,344')).toBeInTheDocument(); + + expect(getByText('Differences:')).toBeInTheDocument(); + expect(getByText('UAH 78,175')).toBeInTheDocument(); + }); + + it('should render closing and opening dates correctly when using filtered dates', async () => { + const { getByText, findByText } = render( + , + ); + + // Closing balance date + expect(await findByText('8/1/2024')).toBeInTheDocument(); + // Opening balance date + expect(getByText('9/30/2024')).toBeInTheDocument(); + }); + + describe('GraphQL query variables', () => { + it('should use date filters', async () => { + render( + , + ); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('FinancialAccountEntries', { + input: { + accountListId, + financialAccountId, + dateRange: '2024-08-01..2024-09-30', + categoryId: '', + wildcardSearch: '', + }, + }); + }); + }); + + it('should use date and category filters', async () => { + render( + , + ); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('FinancialAccountEntries', { + input: { + accountListId, + financialAccountId, + dateRange: '2024-08-01..2024-09-30', + categoryId: 'test123', + wildcardSearch: '', + }, + }); + }); + }); + + it('should use date, category and search filters', async () => { + render( + , + ); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('FinancialAccountEntries', { + input: { + accountListId, + financialAccountId, + dateRange: '2024-08-01..2024-09-30', + categoryId: 'test123', + wildcardSearch: 'searchTerm', + }, + }); + }); + }); + }); + + describe('Export CSV', () => { + it('should disable export CSV button when loading', async () => { + const { getByRole } = render(); + + expect(getByRole('button', { name: 'Export CSV' })).toBeDisabled(); + + await waitFor(() => { + expect(getByRole('button', { name: 'Export CSV' })).toBeEnabled(); + }); + }); + + it('should export CSV', async () => { + const { getByRole } = render(); + + await waitFor(() => { + expect(getByRole('button', { name: 'Export CSV' })).toBeEnabled(); + }); + + const link = document.createElement('a'); + + const createElementSpy = jest.spyOn(document, 'createElement'); + const appendChildSpy = jest.spyOn(document.body, 'appendChild'); + const removeChildSpy = jest.spyOn(document.body, 'removeChild'); + const clickSpy = jest.fn(); + const setAttributeSpy = jest.fn(); + + link.setAttribute = setAttributeSpy; + link.click = clickSpy; + + createElementSpy.mockReturnValue(link); + + fireEvent.click(getByRole('button', { name: 'Export CSV' })); + + const csvContentArray = [ + ['Date', 'Payee', 'Memo', 'Outflow', 'Inflow'], + ['8/9/2024', 'description1', 'category1Name', '', 'UAH 7,048'], + ['8/8/2024', 'description2', 'category1Name', 'UAH 15,008', ''], + ['8/7/2024', 'description3', 'category2Name', '', 'UAH 37'], + ]; + + const csvContent = + 'data:text/csv;charset=utf-8,' + + csvContentArray + .map((row) => + row + .map((field) => `"${String(field).replace(/"/g, '""')}"`) + .join(','), + ) + .join('\n'); + + await waitFor(() => { + expect(createElementSpy).toHaveBeenCalledWith('a'); + expect(appendChildSpy).toHaveBeenCalled(); + expect(setAttributeSpy).toHaveBeenCalledWith( + 'href', + encodeURI(csvContent), + ); + expect(clickSpy).toHaveBeenCalled(); + expect(removeChildSpy).toHaveBeenCalled(); + }); + + createElementSpy.mockRestore(); + appendChildSpy.mockRestore(); + removeChildSpy.mockRestore(); + }); + }); +}); diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx index 336f890a1..3a60253bf 100644 --- a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx +++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx @@ -38,8 +38,8 @@ const formatDateRange = (startDate?: DateTime, endDate?: DateTime) => { }; const defaultDateRange = formatDateRange(); -const defaultStartDate = defaultDateRange.split('..')[0]; -const defaultEndDate = defaultDateRange.split('..')[1]; +export const defaultStartDate = defaultDateRange.split('..')[0]; +export const defaultEndDate = defaultDateRange.split('..')[1]; export const AccountTransactions: React.FC = () => { const { query } = useRouter(); @@ -118,28 +118,33 @@ export const AccountTransactions: React.FC = () => { t('Outflow'), t('Inflow'), ]; - const convertDataToArray = data.financialAccountEntries.entries.map( - (entry) => [ - entry.entryDate - ? dateFormatShort(DateTime.fromISO(entry.entryDate), locale) - : 'No entry date', - entry.description ?? '', - entry.category?.name ?? entry.category?.code ?? '', - entry.type === FinancialAccountEntryTypeEnum.Debit - ? currencyFormat( - formatNumber(entry.amount), - entry.currency, - locale, - ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') - : '', - entry.type === FinancialAccountEntryTypeEnum.Credit - ? currencyFormat( - formatNumber(entry.amount), - entry.currency, - locale, - ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') - : '', - ], + const convertDataToArray = data.financialAccountEntries.entries.reduce( + (array, entry) => { + return [ + ...array, + [ + entry.entryDate + ? dateFormatShort(DateTime.fromISO(entry.entryDate), locale) + : 'No entry date', + entry.description ?? '', + entry.category?.name ?? entry.category?.code ?? '', + entry.type === FinancialAccountEntryTypeEnum.Debit + ? currencyFormat( + formatNumber(entry.amount), + entry.currency, + locale, + ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') + : '', + entry.type === FinancialAccountEntryTypeEnum.Credit + ? currencyFormat( + formatNumber(entry.amount), + entry.currency, + locale, + ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') + : '', + ], + ]; + }, [columnHeaders], ); diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionsMocks.ts b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionsMocks.ts new file mode 100644 index 000000000..261958348 --- /dev/null +++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactionsMocks.ts @@ -0,0 +1,66 @@ +import { FinancialAccountEntryTypeEnum } from './AccountTransactionTable/AccountTransactionTable'; +import { FinancialAccountEntriesQuery } from './financialAccountTransactions.generated'; + +export const financialAccountEntriesMock: FinancialAccountEntriesQuery = { + financialAccountEntries: { + entries: [ + { + __typename: 'FinancialAccountEntry', + id: 'entry1', + amount: '-7047.28', + currency: 'UAH', + code: 'code1', + description: 'description1', + entryDate: '2024-08-09', + type: FinancialAccountEntryTypeEnum.Credit, + category: { + __typename: 'FinancialAccountCategory', + id: 'category1', + code: 'category1Code', + name: 'category1Name', + }, + }, + { + __typename: 'FinancialAccountEntry', + id: 'entry2', + amount: '15008.0', + currency: 'UAH', + code: 'code2', + description: 'description2', + entryDate: '2024-08-08', + type: FinancialAccountEntryTypeEnum.Debit, + category: { + __typename: 'FinancialAccountCategory', + id: 'category1', + code: 'category1Code', + name: 'category1Name', + }, + }, + { + __typename: 'FinancialAccountEntry', + id: 'entry3', + amount: '-36.2', + currency: 'UAH', + code: 'code3', + description: 'description3', + entryDate: '2024-08-07', + type: FinancialAccountEntryTypeEnum.Credit, + category: { + __typename: 'FinancialAccountCategory', + id: 'category2', + code: 'category2Code', + name: 'category2Name', + }, + }, + ], + metaData: { + __typename: 'FinancialAccountMetaData', + credits: '-307518.87', + debits: '229344.0', + difference: '-78174.87', + currency: 'UAH', + closingBalance: '-280413.99', + openingBalance: '-202239.12', + }, + }, +}; diff --git a/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccounts.test.tsx b/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccounts.test.tsx index 9edb1681f..36dec7ada 100644 --- a/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccounts.test.tsx +++ b/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccounts.test.tsx @@ -11,10 +11,12 @@ import { FinancialAccountType, } from '../Context/FinancialAccountsContext'; import { FinancialAccounts } from './FinancialAccounts'; +import { FinancialAccountsQuery } from './FinancialAccounts.generated'; import { - FinancialAccountsDocument, - FinancialAccountsQuery, -} from './FinancialAccounts.generated'; + FinancialAccountsEmptyMock, + FinancialAccountsErrorMock, + FinancialAccountsMock, +} from './FinancialAccountsMocks'; jest.mock('next/router', () => ({ useRouter: () => { @@ -29,46 +31,6 @@ const accountListId = '111'; const onNavListToggle = jest.fn(); const mutationSpy = jest.fn(); -const mocks = { - FinancialAccounts: { - financialAccounts: { - nodes: [ - { - active: true, - balance: { - conversionDate: '2021-02-02', - convertedAmount: 3500, - convertedCurrency: 'CAD', - }, - code: '13212', - id: 'test-id-111', - name: 'Test Account', - organization: { - id: '111-2222-3333', - name: 'test org 01', - }, - updatedAt: '2021-02-02', - }, - ], - }, - }, -}; - -const errorMock = { - request: { - query: FinancialAccountsDocument, - }, - error: { name: 'error', message: 'Error loading data. Try again.' }, -}; - -const emptyMocks = { - FinancialAccounts: { - financialAccounts: { - nodes: [], - }, - }, -}; - interface ComponentsProps { mocks?: ApolloErgonoMockMap; designationAccounts?: string[]; @@ -91,7 +53,7 @@ const Components: React.FC = ({ } > {useErrorMockedProvider ? ( - + ) : ( @@ -112,7 +74,7 @@ describe('FinancialAccounts', () => { }); it('default', async () => { const { getByText, getByTestId, queryByTestId } = render( - , + , ); await waitFor(() => { @@ -127,7 +89,9 @@ describe('FinancialAccounts', () => { }); it('renders nav list icon and onclick triggers onNavListToggle', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + , + ); expect(getByTestId('ReportsFilterIcon')).toBeInTheDocument(); userEvent.click(getByTestId('ReportsFilterIcon')); @@ -135,7 +99,9 @@ describe('FinancialAccounts', () => { }); it('loading', async () => { - const { queryByTestId, getByText } = render(); + const { queryByTestId, getByText } = render( + , + ); expect(getByText('Responsibility Centers')).toBeInTheDocument(); expect(queryByTestId('LoadingFinancialAccounts')).toBeInTheDocument(); @@ -153,7 +119,9 @@ describe('FinancialAccounts', () => { }); it('empty', async () => { - const { queryByTestId } = render(); + const { queryByTestId } = render( + , + ); await waitFor(() => { expect(queryByTestId('LoadingFinancialAccounts')).not.toBeInTheDocument(); @@ -163,7 +131,12 @@ describe('FinancialAccounts', () => { }); it('filters report by designation account', async () => { - render(); + render( + , + ); await waitFor(() => expect(mutationSpy).toHaveGraphqlOperation('FinancialAccounts', { @@ -173,7 +146,7 @@ describe('FinancialAccounts', () => { }); it('does not filter report by designation account', async () => { - render(); + render(); await waitFor(() => expect(mutationSpy).toHaveGraphqlOperation('FinancialAccounts', { diff --git a/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccountsMocks.ts b/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccountsMocks.ts new file mode 100644 index 000000000..ca3105ab6 --- /dev/null +++ b/src/components/Reports/FinancialAccountsReport/FinancialAccounts/FinancialAccountsMocks.ts @@ -0,0 +1,41 @@ +import { FinancialAccountsDocument } from './FinancialAccounts.generated'; + +export const FinancialAccountsMock = { + FinancialAccounts: { + financialAccounts: { + nodes: [ + { + active: true, + balance: { + conversionDate: '2021-02-02', + convertedAmount: 3500, + convertedCurrency: 'CAD', + }, + code: '13212', + id: 'test-id-111', + name: 'Test Account', + organization: { + id: '111-2222-3333', + name: 'test org 01', + }, + updatedAt: '2021-02-02', + }, + ], + }, + }, +}; + +export const FinancialAccountsErrorMock = { + request: { + query: FinancialAccountsDocument, + }, + error: { name: 'error', message: 'Error loading data. Try again.' }, +}; + +export const FinancialAccountsEmptyMock = { + FinancialAccounts: { + financialAccounts: { + nodes: [], + }, + }, +}; diff --git a/src/components/Reports/FinancialAccountsReport/Header/Header.test.tsx b/src/components/Reports/FinancialAccountsReport/Header/Header.test.tsx new file mode 100644 index 000000000..911dd6860 --- /dev/null +++ b/src/components/Reports/FinancialAccountsReport/Header/Header.test.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { render, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SnackbarProvider } from 'notistack'; +import { I18nextProvider } from 'react-i18next'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { FinancialAccountsWrapper } from 'pages/accountLists/[accountListId]/reports/financialAccounts/Wrapper'; +import { defaultFinancialAccount } from 'src/components/Reports/FinancialAccountsReport/AccountSummary/AccountSummaryMock'; +import i18n from 'src/lib/i18n'; +import theme from 'src/theme'; +import { FinancialAccountQuery } from '../Context/FinancialAccount.generated'; +import { + FinancialAccountContext, + FinancialAccountType, +} from '../Context/FinancialAccountsContext'; +import { FinancialAccountHeader, FinancialAccountHeaderProps } from './Header'; + +const accountListId = 'account-list-1'; +const financialAccountId = 'financialAccountId'; +const handleNavListToggle = jest.fn(); +const defaultHasActiveFilters = false; +const handleFilterListToggle = jest.fn(); +const defaultSearchTerm = ''; +const setSearchTerm = jest.fn(); +const router = { + query: { accountListId }, + isReady: true, +}; + +interface ComponentsProps extends FinancialAccountHeaderProps { + hasActiveFilters?: boolean; + searchTerm?: string; +} +const Components = ({ + hasActiveFilters = defaultHasActiveFilters, + searchTerm = defaultSearchTerm, + onTransactionPage, + disableExportCSV, + handleExportCSV, +}: ComponentsProps) => ( + + + + + + + mocks={{ + FinancialAccount: defaultFinancialAccount, + }} + > + + + + + + + + + + + +); + +describe('Financial Account Header', () => { + describe('Summary view', () => { + it('should show initial view without transactions header', async () => { + const { findByText, getByRole, getByText, queryByRole } = render( + , + ); + + expect(await findByText('Account 1')).toBeInTheDocument(); + expect(getByRole('link', { name: 'Summary' })).toBeInTheDocument(); + expect(getByRole('link', { name: 'Transactions' })).toBeInTheDocument(); + expect(getByText('$1,000')).toBeInTheDocument(); + + expect(getByText('accountCode - Organization 1')).toBeInTheDocument(); + + expect( + queryByRole('button', { + name: 'Export CSV', + }), + ).not.toBeInTheDocument(); + + expect( + getByRole('img', { name: 'Toggle Menu Panel' }), + ).toBeInTheDocument(); + }); + + it('should have correct links for Summary and Transaction', async () => { + const { findByRole, getByRole } = render(); + + expect(await findByRole('link', { name: 'Summary' })).toHaveAttribute( + 'href', + '/accountLists/account-list-1/reports/financialAccounts/financialAccountId', + ); + expect(getByRole('link', { name: 'Transactions' })).toHaveAttribute( + 'href', + '/accountLists/account-list-1/reports/financialAccounts/financialAccountId/entries', + ); + }); + }); + + describe('Transactions view', () => { + const handleExportCSV = jest.fn(); + + it('should show transactions header', async () => { + const { findByText, findByRole, getByTestId, getByRole } = render( + , + ); + + expect(await findByText('Account 1')).toBeInTheDocument(); + + expect( + await findByRole('button', { name: 'Export CSV' }), + ).toBeInTheDocument(); + + expect(getByTestId('SearchIcon')).toBeInTheDocument(); + + expect( + getByRole('img', { name: 'Toggle Filter Panel' }), + ).toBeInTheDocument(); + }); + + it('should export CSV on click of the button', async () => { + const { findByRole } = render( + , + ); + + const exportButton = await findByRole('button', { name: 'Export CSV' }); + + userEvent.click(exportButton); + + expect(handleExportCSV).toHaveBeenCalled(); + }); + + it('should update search on type', async () => { + const { getByTestId } = render(); + + const view = getByTestId('FinancialAccountHeader'); + + const searchInput = within(view).getByRole('textbox'); + + userEvent.type(searchInput, 'test'); + + expect(setSearchTerm).toHaveBeenCalledWith('test'); + }); + }); +}); diff --git a/src/components/Reports/FinancialAccountsReport/Header/Header.tsx b/src/components/Reports/FinancialAccountsReport/Header/Header.tsx index 42cf1394c..a5a1bd3cb 100644 --- a/src/components/Reports/FinancialAccountsReport/Header/Header.tsx +++ b/src/components/Reports/FinancialAccountsReport/Header/Header.tsx @@ -69,7 +69,7 @@ const HeaderActions = styled(Box)(({ theme }) => ({ width: 'calc(100% - 150px)', })); -interface FinancialAccountHeaderProps { +export interface FinancialAccountHeaderProps { onTransactionPage?: boolean; disableExportCSV?: boolean; handleExportCSV?: () => void; From a8d4ac003be2813ad9453df49986188141be6ff1 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 16 Oct 2024 09:12:00 -0400 Subject: [PATCH 2/2] Edits to the export to ensure the actual amounts are shown without rounding or the currency sign --- .../[[...financialAccount]].page.test.tsx | 41 ++++------------ .../AccountTransactions.test.tsx | 26 +++++----- .../AccountTransactions.tsx | 49 +++++++++++-------- 3 files changed, 52 insertions(+), 64 deletions(-) diff --git a/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx b/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx index dd032cc24..d93066e79 100644 --- a/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx +++ b/pages/accountLists/[accountListId]/reports/financialAccounts/[[...financialAccount]].page.test.tsx @@ -26,6 +26,13 @@ const defaultRouter = { query: { accountListId }, isReady: true, }; +const entriesRouter = { + query: { + accountListId, + financialAccount: [financialAccountId, 'entries'], + }, + isReady: true, +}; const Components = ({ router = defaultRouter }: { router?: object }) => ( @@ -85,17 +92,7 @@ describe('Financial Accounts Page', () => { it('should show the transactions page for a financial account', async () => { const { findByText, findByRole, getByText, queryByText, queryByRole } = - render( - , - ); + render(); expect(await findByText('Account 1')).toBeInTheDocument(); @@ -112,17 +109,7 @@ describe('Financial Accounts Page', () => { }); it('should open filters on load and set initial date Range filter', async () => { - const { findByRole } = render( - , - ); + const { findByRole } = render(); expect( await findByRole('heading', { name: 'Filter (1)' }), @@ -131,15 +118,7 @@ describe('Financial Accounts Page', () => { it('should open and close filters and menu', async () => { const { findByRole, getByRole, queryByRole } = render( - , + , ); // Filters diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx index 08ac0fb3e..0b2936960 100644 --- a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx +++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.test.tsx @@ -3,6 +3,7 @@ import { ThemeProvider } from '@mui/material/styles'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { fireEvent, render, waitFor } from '@testing-library/react'; +import { Settings } from 'luxon'; import { SnackbarProvider } from 'notistack'; import { I18nextProvider } from 'react-i18next'; import TestRouter from '__tests__/util/TestRouter'; @@ -20,11 +21,7 @@ import { FinancialAccountContext, FinancialAccountType, } from '../Context/FinancialAccountsContext'; -import { - AccountTransactions, - defaultEndDate, - defaultStartDate, -} from './AccountTransactions'; +import { AccountTransactions } from './AccountTransactions'; import { financialAccountEntriesMock } from './AccountTransactionsMocks'; import { FinancialAccountEntriesQuery } from './financialAccountTransactions.generated'; @@ -96,6 +93,9 @@ const Components = ({ ); describe('Financial Account Transactions', () => { + beforeEach(() => { + Settings.now = () => new Date(2024, 7, 31).valueOf(); + }); describe('Resetting filters', () => { it('should reset the activeFilters with the filters from the url filters', () => { render( @@ -113,8 +113,8 @@ describe('Financial Account Transactions', () => { expect(setActiveFilters).not.toHaveBeenCalledWith({ dateRange: { - min: defaultStartDate, - max: defaultEndDate, + min: '2024-08-01', + max: '2024-08-31', }, }); @@ -129,8 +129,8 @@ describe('Financial Account Transactions', () => { expect(setActiveFilters).toHaveBeenCalledWith({ dateRange: { - min: defaultStartDate, - max: defaultEndDate, + min: '2024-08-01', + max: '2024-08-31', }, }); }); @@ -143,6 +143,7 @@ describe('Financial Account Transactions', () => { expect(getAllByText('category1Code')).toHaveLength(2); // Header + expect(getByText('8/31/2024')).toBeInTheDocument(); expect(getByText('Closing Balance')).toBeInTheDocument(); expect(getByText('UAH 280,414')).toBeInTheDocument(); @@ -165,6 +166,7 @@ describe('Financial Account Transactions', () => { expect(getByText('UAH 37')).toBeInTheDocument(); // Footer + expect(getByText('8/1/2024')).toBeInTheDocument(); expect(getByText('Opening Balance')).toBeInTheDocument(); expect(getByText('UAH 202,240')).toBeInTheDocument(); @@ -312,9 +314,9 @@ describe('Financial Account Transactions', () => { const csvContentArray = [ ['Date', 'Payee', 'Memo', 'Outflow', 'Inflow'], - ['8/9/2024', 'description1', 'category1Name', '', 'UAH 7,048'], - ['8/8/2024', 'description2', 'category1Name', 'UAH 15,008', ''], - ['8/7/2024', 'description3', 'category2Name', '', 'UAH 37'], + ['8/9/2024', 'description1', 'category1Name', '', '7047.28'], + ['8/8/2024', 'description2', 'category1Name', '15008', ''], + ['8/7/2024', 'description3', 'category2Name', '', '36.2'], ]; const csvContent = diff --git a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx index 3a60253bf..b6538c4f0 100644 --- a/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx +++ b/src/components/Reports/FinancialAccountsReport/AccountTransactions/AccountTransactions.tsx @@ -9,8 +9,7 @@ import { headerHeight } from 'src/components/Shared/Header/ListHeader'; import { useDebouncedValue } from 'src/hooks/useDebounce'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { useLocale } from 'src/hooks/useLocale'; -import { currencyFormat, dateFormatShort } from 'src/lib/intlFormat'; -import { formatNumber } from '../AccountSummary/AccountSummaryHelper'; +import { dateFormatShort } from 'src/lib/intlFormat'; import { FinancialAccountContext, FinancialAccountType, @@ -27,19 +26,31 @@ const Container = styled(Box)(() => ({ overflowY: 'auto', })); -const formatDateRange = (startDate?: DateTime, endDate?: DateTime) => { - if (!startDate) { - startDate = DateTime.local().minus({ months: 1 }).plus({ days: 1 }); +/** + * Converts the "amount" string to a number to remove ".0" + * If the value is 0 or isExpense is true, it returns the value as is. + * Otherwise, it removes the '-' character if present, or prepends it if absent. + */ +const formatAmountForExport = ( + amount?: string | null, + isExpense?: boolean, +): number => { + if (!amount) { + return 0; } - if (!endDate) { - endDate = DateTime.local(); + + if (amount === '0' || isExpense) { + return Number(amount); } - return `${startDate.toISODate()}..${endDate.toISODate()}`; + return -Number(amount); }; -const defaultDateRange = formatDateRange(); -export const defaultStartDate = defaultDateRange.split('..')[0]; -export const defaultEndDate = defaultDateRange.split('..')[1]; +const formatDateRange = (startDate?: DateTime, endDate?: DateTime) => { + const minDate = + startDate ?? DateTime.local().minus({ months: 1 }).plus({ days: 1 }); + const maxDate = endDate ?? DateTime.local(); + return `${minDate.toISODate()}..${maxDate.toISODate()}`; +}; export const AccountTransactions: React.FC = () => { const { query } = useRouter(); @@ -69,6 +80,10 @@ export const AccountTransactions: React.FC = () => { }; }, [query?.filters]); + const defaultDateRange = useMemo(() => formatDateRange(), []); + const defaultStartDate = defaultDateRange.split('..')[0]; + const defaultEndDate = defaultDateRange.split('..')[1]; + useEffect(() => { if (!hasActiveFilters && !query?.filters) { setActiveFilters({ @@ -129,18 +144,10 @@ export const AccountTransactions: React.FC = () => { entry.description ?? '', entry.category?.name ?? entry.category?.code ?? '', entry.type === FinancialAccountEntryTypeEnum.Debit - ? currencyFormat( - formatNumber(entry.amount), - entry.currency, - locale, - ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') + ? `${formatAmountForExport(entry.amount, true)}` : '', entry.type === FinancialAccountEntryTypeEnum.Credit - ? currencyFormat( - formatNumber(entry.amount), - entry.currency, - locale, - ).replace(/[\xA0\u2000-\u200B\uFEFF]/g, ' ') + ? `${formatAmountForExport(entry.amount)}` : '', ], ];