import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { format as formatDate, parse as parseDate } from 'date-fns'; import { act } from 'react-dom/test-utils'; import { generateTransaction, generateAccount, generateCategoryGroups, TestProvider } from 'loot-core/src/mocks'; import { initServer } from 'loot-core/src/platform/client/fetch'; import { addSplitTransaction, realizeTempTransactions, splitTransaction, updateTransaction } from 'loot-core/src/shared'; import { integerToCurrency } from 'loot-core/src/shared/util'; import { SelectedProviderWithItems } from 'loot-design/src/components'; import { SplitsExpandedProvider, TransactionTable } from './TransactionsTable'; const uuid = require('loot-core/src/platform/uuid'); const accounts = [generateAccount('Bank of America')]; const payees = [ { id: 'payed-to', name: 'Payed To' }, { id: 'guy', name: 'This guy on the side of the road' } ]; const categoryGroups = generateCategoryGroups([ { name: 'Investments and Savings', categories: [{ name: 'Savings' }] }, { name: 'Usual Expenses', categories: [{ name: 'Food' }, { name: 'General' }, { name: 'Home' }] }, { name: 'Projects', categories: [{ name: 'Big Projects' }, { name: 'Shed' }] } ]); const usualGroup = categoryGroups[1]; function generateTransactions(count, splitAtIndexes = [], showError = false) { const transactions = []; for (let i = 0; i < count; i++) { const isSplit = splitAtIndexes.includes(i); transactions.push.apply( transactions, generateTransaction( { account: accounts[0].id, category: i === 0 ? null : i === 1 ? usualGroup.categories[1].id : usualGroup.categories[0].id, amount: isSplit ? 50 : undefined, sort_order: i }, isSplit ? 30 : undefined, showError ) ); } return transactions; } class LiveTransactionTable extends React.Component { constructor(props) { super(props); this.state = { transactions: props.transactions }; } componentWillReceiveProps(nextProps) { if (this.state.transactions !== nextProps.transactions) { this.setState({ transactions: nextProps.transactions }); } } notifyChange = () => { const { onTransactionsChange } = this.props; onTransactionsChange && onTransactionsChange(this.state.transactions); }; onSplit = id => { let { state } = this; let { data, diff } = splitTransaction(state.transactions, id); this.setState({ transactions: data }, this.notifyChange); return diff.added[0].id; }; // onDelete = id => { // let { state } = this; // this.setState( // { // transactions: applyChanges( // deleteTransaction(state.transactions, id), // state.transactions // ) // }, // this.notifyChange // ); // }; onSave = transaction => { let { state } = this; let { data } = updateTransaction(state.transactions, transaction); this.setState({ transactions: data }, this.notifyChange); }; onAdd = newTransactions => { let { state } = this; newTransactions = realizeTempTransactions(newTransactions); this.setState( { transactions: [...newTransactions, ...state.transactions] }, this.notifyChange ); }; onAddSplit = id => { let { state } = this; let { data, diff } = addSplitTransaction(state.transactions, id); this.setState({ transactions: data }, this.notifyChange); return diff.added[0].id; }; onCreatePayee = name => 'id'; render() { const { state } = this; // It's important that these functions are they same instances // across renders. Doing so tests that the transaction table // implementation properly uses the right latest state even if the // hook dependencies haven't changed return ( state.transactions.map(t => t.id)} > {}} payees={payees} addNotification={n => console.log(n)} onSave={this.onSave} onSplit={this.onSplit} onAdd={this.onAdd} onAddSplit={this.onAddSplit} onCreatePayee={this.onCreatePayee} /> ); } } function initBasicServer() { initServer({ query: async query => { switch (query.table) { case 'payees': return { data: payees, dependencies: [] }; case 'accounts': return { data: accounts, dependencies: [] }; default: throw new Error(`queried unknown table: ${query.table}`); } } }); } beforeEach(() => { initBasicServer(); }); afterEach(() => { global.__resetWorld(); }); // Not good, see `Autocomplete.js` for details function waitForAutocomplete() { return new Promise(resolve => setTimeout(resolve, 0)); } const categories = categoryGroups.reduce( (all, group) => all.concat(group.categories), [] ); const keys = { ESC: { key: 'Esc', keyCode: 27, which: 27 }, ENTER: { key: 'Enter', keyCode: 13, which: 13 }, TAB: { key: 'Tab', keyCode: 9, which: 9 }, DOWN: { key: 'Down', keyCode: 40, which: 40 }, UP: { key: 'Up', keyCode: 38, which: 38 }, LEFT: { key: 'Left', keyCode: 37, which: 37 }, RIGHT: { key: 'Right', keyCode: 39, which: 39 } }; function prettyDate(date) { return formatDate(parseDate(date, 'yyyy-MM-dd', new Date()), 'MM/dd/yyyy'); } function keyWithShift(key) { return { ...key, shiftKey: true }; } function verifySortOrder(transactions) { transactions.forEach((transaction, idx) => { let lastTransaction = idx === 0 ? null : transactions[idx]; if (transaction.sort_order == null) { throw new Error("Transaction doesn't have sort_order"); } if ( lastTransaction && transaction.sort_order > lastTransaction.sort_order ) { throw new Error( 'Transaction sort order must always be decreasing. Found increasing sort order:\n ' + JSON.stringify(lastTransaction) + '\n ' + JSON.stringify(transaction) ); } }); } function renderTransactions(extraProps) { let transactions = generateTransactions(5, [6]); // Hardcoding the first value makes it easier for tests to do // various this transactions[0].amount = -2777; let defaultProps = { transactions, payees: payees, accounts: accounts, categoryGroups: categoryGroups, currentAccountId: accounts[0].id, showAccount: true, showCategory: true, isAdding: false, onTransactionsChange: t => { transactions = t; } }; let result = render( ); return { ...result, getTransactions: () => transactions, updateProps: props => render( , { container: result.container } ) }; } function queryNewField(container, name, subSelector = '', idx = 0) { const field = container.querySelectorAll( `[data-testid="new-transaction"] [data-testid="${name}"]` )[idx]; if (subSelector !== '') { return field.querySelector(subSelector); } return field; } function queryField(container, name, subSelector = '', idx) { const field = container.querySelectorAll( `[data-testid="transaction-table"] [data-testid="${name}"]` )[idx]; if (subSelector !== '') { return field.querySelector(subSelector); } return field; } function _editField(field, container) { // We only short-circuit this for inputs let input = field.querySelector(`input`); if (input) { expect(container.ownerDocument.activeElement).toBe(input); return input; } let element; let buttonQuery = 'button,div[data-testid=cell-button]'; if (field.querySelector(buttonQuery)) { let btn = field.querySelector(buttonQuery); fireEvent.click(btn); element = field.querySelector(':focus'); expect(element).toBeTruthy(); } else { fireEvent.click(field.querySelector('div')); element = field.querySelector('input'); expect(element).toBeTruthy(); expect(container.ownerDocument.activeElement).toBe(element); } return element; } function editNewField(container, name, rowIndex) { const field = queryNewField(container, name, '', rowIndex); return _editField(field, container); } function editField(container, name, rowIndex) { const field = queryField(container, name, '', rowIndex); return _editField(field, container); } function expectToBeEditingField(container, name, rowIndex, isNew) { let field; if (isNew) { field = queryNewField(container, name, '', rowIndex); } else { field = queryField(container, name, '', rowIndex); } const input = field.querySelector(':focus'); expect(input).toBeTruthy(); expect(container.ownerDocument.activeElement).toBe(input); return input; } describe('Transactions', () => { test('transactions table shows the correct data', () => { const { container, getTransactions } = renderTransactions(); getTransactions().forEach((transaction, idx) => { expect(queryField(container, 'date', 'div', idx).textContent).toBe( prettyDate(transaction.date) ); expect(queryField(container, 'account', 'div', idx).textContent).toBe( accounts.find(acct => acct.id === transaction.account).name ); expect(queryField(container, 'payee', 'div', idx).textContent).toBe( payees.find(p => p.id === transaction.payee).name ); expect(queryField(container, 'notes', 'div', idx).textContent).toBe( transaction.notes ); expect(queryField(container, 'category', 'div', idx).textContent).toBe( transaction.category ? categories.find(category => category.id === transaction.category) .name : 'Categorize' ); if (transaction.amount <= 0) { expect(queryField(container, 'debit', 'div', idx).textContent).toBe( integerToCurrency(-transaction.amount) ); expect(queryField(container, 'credit', 'div', idx).textContent).toBe( '' ); } else { expect(queryField(container, 'debit', 'div', idx).textContent).toBe(''); expect(queryField(container, 'credit', 'div', idx).textContent).toBe( integerToCurrency(transaction.amount) ); } }); }); test('keybindings enter/tab/alt should move around', () => { const { container } = renderTransactions(); // Enter/tab goes down/right let input = editField(container, 'notes', 2); fireEvent.keyDown(input, keys.ENTER); expectToBeEditingField(container, 'notes', 3); input = editField(container, 'payee', 2); fireEvent.keyDown(input, keys.TAB); expectToBeEditingField(container, 'notes', 2); // Shift+enter/tab goes up/left input = editField(container, 'notes', 2); fireEvent.keyDown(input, keyWithShift(keys.ENTER)); expectToBeEditingField(container, 'notes', 1); input = editField(container, 'payee', 2); fireEvent.keyDown(input, keyWithShift(keys.TAB)); expectToBeEditingField(container, 'account', 2); // Moving forward on the last cell moves to the next row input = editField(container, 'cleared', 2); fireEvent.keyDown(input, keys.TAB); expectToBeEditingField(container, 'select', 3); // Moving backward on the first cell moves to the previous row editField(container, 'date', 2); input = editField(container, 'select', 2); fireEvent.keyDown(input, keyWithShift(keys.TAB)); expectToBeEditingField(container, 'cleared', 1); // Blurring should close the input input = editField(container, 'credit', 1); fireEvent.blur(input); expect(container.querySelector('input')).toBe(null); // When reaching the bottom it shouldn't error input = editField(container, 'notes', 4); fireEvent.keyDown(input, keys.ENTER); // When reaching the top it shouldn't error input = editField(container, 'notes', 0); fireEvent.keyDown(input, keyWithShift(keys.ENTER)); }); test('keybinding escape resets the value', () => { const { container } = renderTransactions(); let input = editField(container, 'notes', 2); let oldValue = input.value; fireEvent.change(input, { target: { value: 'yo new value' } }); expect(input.value).toEqual('yo new value'); fireEvent.keyDown(input, keys.ESC); expect(input.value).toEqual(oldValue); input = editField(container, 'category', 2); oldValue = input.value; fireEvent.change(input, { target: { value: 'Gener' } }); expect(input.value).toEqual('Gener'); fireEvent.keyDown(input, keys.ESC); expect(input.value).toEqual(oldValue); }); test('text fields save when moved away from', () => { const { container, getTransactions } = renderTransactions(); function runWithMovementKeys(func) { // All of these keys move to a different field, and the value in // the previous input should be saved const ks = [ keys.TAB, keys.ENTER, keyWithShift(keys.TAB), keyWithShift(keys.ENTER) ]; ks.forEach((k, i) => func(k, i)); } runWithMovementKeys((key, idx) => { let input = editField(container, 'notes', 2); let oldValue = input.value; fireEvent.change(input, { target: { value: 'a happy little note' + idx } }); // It's not saved yet expect(getTransactions()[2].notes).toBe(oldValue); fireEvent.keyDown(input, keys.TAB); // Now it should be saved! expect(getTransactions()[2].notes).toBe('a happy little note' + idx); expect(queryField(container, 'notes', 'div', 2).textContent).toBe( 'a happy little note' + idx ); }); let input = editField(container, 'notes', 2); let oldValue = input.value; fireEvent.change(input, { target: { value: 'another happy note' } }); // It's not saved yet expect(getTransactions()[2].notes).toBe(oldValue); // Blur the input to make it stop editing fireEvent.blur(input); expect(getTransactions()[2].notes).toBe('another happy note'); }); test('dropdown automatically opens and can be filtered', () => { const { container } = renderTransactions(); let input = editField(container, 'category', 2); let tooltip = container.querySelector('[data-testid="tooltip"]'); expect(tooltip).toBeTruthy(); expect( [...tooltip.querySelectorAll('[data-testid*="category-item"]')].length ).toBe(9); fireEvent.change(input, { target: { value: 'Gener' } }); // Make sure the list is filtered, the right items exist, and the // first item is highlighted let items = tooltip.querySelectorAll('[data-testid*="category-item"]'); expect(items.length).toBe(2); expect(items[0].textContent).toBe('Usual Expenses'); expect(items[1].textContent).toBe('General'); expect(items[1].dataset['testid']).toBe('category-item-highlighted'); // It should also allow filtering on group names fireEvent.change(input, { target: { value: 'Usual' } }); items = tooltip.querySelectorAll('[data-testid*="category-item"]'); expect(items.length).toBe(4); expect(items[0].textContent).toBe('Usual Expenses'); expect(items[1].textContent).toBe('Food'); expect(items[2].textContent).toBe('General'); expect(items[3].textContent).toBe('Home'); expect(items[1].dataset['testid']).toBe('category-item-highlighted'); }); test('dropdown selects an item with keyboard', async () => { const { container, getTransactions } = renderTransactions(); let input = editField(container, 'category', 2); let tooltip = container.querySelector('[data-testid="tooltip"]'); // No item should be highlighted let highlighted = tooltip.querySelector( '[data-testid="category-item-highlighted"]' ); expect(highlighted).toBe(null); fireEvent.keyDown(input, keys.DOWN); fireEvent.keyDown(input, keys.DOWN); fireEvent.keyDown(input, keys.DOWN); fireEvent.keyDown(input, keys.DOWN); // The right item should be highlighted highlighted = tooltip.querySelector( '[data-testid="category-item-highlighted"]' ); expect(highlighted).toBeTruthy(); expect(highlighted.textContent).toBe('General'); expect(getTransactions()[2].category).toBe( categories.find(category => category.name === 'Food').id ); fireEvent.keyDown(input, keys.ENTER); await waitForAutocomplete(); // The transactions data should be updated with the right category expect(getTransactions()[2].category).toBe( categories.find(category => category.name === 'General').id ); // The category field should still be editing expectToBeEditingField(container, 'category', 2); // No dropdown should be open expect(container.querySelector('[data-testid="tooltip"]')).toBe(null); // Pressing enter should now move down fireEvent.keyDown(input, keys.ENTER); expectToBeEditingField(container, 'category', 3); }); test('dropdown selects an item when clicking', async () => { const { container, getTransactions } = renderTransactions(); let input = editField(container, 'category', 2); let tooltip = container.querySelector('[data-testid="tooltip"]'); // Make sure none of the items are highlighted let items = tooltip.querySelectorAll('[data-testid="category-item"]'); let highlighted = tooltip.querySelector( '[data-testid="category-item-highlighted"]' ); expect(highlighted).toBe(null); // Hover over an item fireEvent.mouseMove(items[2]); // Make sure the expected category is highlighted highlighted = tooltip.querySelector( '[data-testid="category-item-highlighted"]' ); expect(highlighted).toBeTruthy(); expect(highlighted.textContent).toBe('General'); // Click the item and check the before/after values expect(getTransactions()[2].category).toBe( categories.find(c => c.name === 'Food').id ); fireEvent.click(items[2]); await waitForAutocomplete(); expect(getTransactions()[2].category).toBe( categories.find(c => c.name === 'General').id ); // It should still be editing the category tooltip = container.querySelector('[data-testid="tooltip"]'); expect(tooltip).toBe(null); expectToBeEditingField(container, 'category', 2); }); test("dropdown hovers but doesn't change value", () => { const { container, getTransactions } = renderTransactions(); let input = editField(container, 'category', 2); let oldCategory = getTransactions()[2].category; let tooltip = container.querySelector('[data-testid="tooltip"]'); let items = tooltip.querySelectorAll('[data-testid="category-item"]'); // Hover over a few of the items to highlight them fireEvent.mouseMove(items[2]); fireEvent.mouseMove(items[3]); // Make sure one of them is highlighted let highlighted = tooltip.querySelector( '[data-testid="category-item-highlighted"]' ); expect(highlighted).toBeTruthy(); // Navigate away from the field with the keyboard fireEvent.keyDown(input, keys.TAB); // Make sure the category didn't update, and that the highlighted // field was different than the transactions' category let currentCategory = getTransactions()[2].category; expect(currentCategory).toBe(oldCategory); expect(highlighted.textContent).not.toBe( categories.find(c => c.id === currentCategory).name ); }); test('dropdown invalid value resets correctly', async () => { const { container, getTransactions } = renderTransactions(); // Invalid values should be rejected and nullified let input = editField(container, 'category', 2); fireEvent.change(input, { target: { value: 'aaabbbccc' } }); // For this first test case, make sure the tooltip is gone. We // don't need to check this in all the other cases let tooltipItems = container.querySelectorAll( '[data-testid="category-item-group"]' ); expect(tooltipItems.length).toBe(0); expect(getTransactions()[2].category).not.toBe(null); fireEvent.keyDown(input, keys.TAB); expect(getTransactions()[2].category).toBe(null); // Clear out the category value input = editField(container, 'category', 3); fireEvent.change(input, { target: { value: '' } }); // The category should be null when the value is cleared expect(getTransactions()[3].category).not.toBe(null); fireEvent.keyDown(input, keys.TAB); expect(getTransactions()[3].category).toBe(null); // Clear out the payee value input = editField(container, 'payee', 3); await new Promise(resolve => setTimeout(resolve, 10)); fireEvent.change(input, { target: { value: '' } }); // The payee should be empty when the value is cleared expect(getTransactions()[3].payee).not.toBe(''); fireEvent.keyDown(input, keys.TAB); expect(getTransactions()[3].payee).toBe(null); }); test('dropdown escape resets the value ', () => { const { container, getTransactions } = renderTransactions(); let input = editField(container, 'category', 2); let oldValue = input.value; fireEvent.change(input, { target: { value: 'aaabbbccc' } }); fireEvent.keyDown(input, keys.ESC); expect(input.value).toBe(oldValue); // The tooltip be closed let tooltip = container.querySelector('[data-testid="tooltip"]'); expect(tooltip).toBeNull(); }); test('adding a new transaction works', async () => { const { queryByTestId, container, getTransactions, updateProps } = renderTransactions(); expect(getTransactions().length).toBe(5); expect(queryByTestId('new-transaction')).toBe(null); updateProps({ isAdding: true }); expect(queryByTestId('new-transaction')).toBeTruthy(); let input = queryNewField(container, 'date', 'input'); // The date input should exist and have a default value expect(input).toBeTruthy(); expect(container.ownerDocument.activeElement).toBe(input); expect(input.value).not.toBe(''); input = editNewField(container, 'notes'); fireEvent.change(input, { target: { value: 'a transaction' } }); fireEvent.keyDown(input, keys.ENTER); input = editNewField(container, 'debit'); expect(input.value).toBe('0.00'); fireEvent.change(input, { target: { value: '100' } }); act(() => { fireEvent.keyDown(input, keys.ENTER); }); expect(getTransactions().length).toBe(6); expect(getTransactions()[0].amount).toBe(-10000); expect(getTransactions()[0].notes).toBe('a transaction'); // The date field should be re-focused to enter a new transaction expect(container.ownerDocument.activeElement).toBe( queryNewField(container, 'date', 'input') ); expect(queryNewField(container, 'debit').textContent).toBe('0.00'); }); test('adding a new split transaction works', async () => { const { container, getTransactions, updateProps } = renderTransactions(); updateProps({ isAdding: true }); let input = editNewField(container, 'debit'); fireEvent.change(input, { target: { value: '55.00' } }); fireEvent.blur(input); editNewField(container, 'category'); let splitButton = document.body.querySelector( '[data-testid="tooltip"] [data-testid="split-transaction-button"]' ); fireEvent.click(splitButton); await waitForAutocomplete(); await waitForAutocomplete(); await waitForAutocomplete(); fireEvent.click( container.querySelector('[data-testid="transaction-error"] button') ); input = editNewField(container, 'debit', 1); fireEvent.change(input, { target: { value: '45.00' } }); fireEvent.blur(input); expect( container.querySelector('[data-testid="transaction-error"]') ).toBeTruthy(); input = editNewField(container, 'debit', 2); fireEvent.change(input, { target: { value: '10.00' } }); fireEvent.blur(input); expect(container.querySelector('[data-testid="transaction-error"]')).toBe( null ); let addButton = container.querySelector('[data-testid="add-button"]'); expect(getTransactions().length).toBe(5); fireEvent.click(addButton); expect(getTransactions().length).toBe(8); expect(getTransactions()[0].is_parent).toBe(true); expect(getTransactions()[0].amount).toBe(-5500); expect(getTransactions()[1].is_child).toBe(true); expect(getTransactions()[1].amount).toBe(-4500); expect(getTransactions()[2].is_child).toBe(true); expect(getTransactions()[2].amount).toBe(-1000); expect(getTransactions().slice(0, 3)).toMatchSnapshot(); }); test('escape closes the new transaction rows', () => { const { container, updateProps } = renderTransactions({ onCloseAddTransaction: () => { updateProps({ isAdding: false }); } }); updateProps({ isAdding: true }); // While adding a transaction, pressing escape should close the // new transaction form let input = expectToBeEditingField(container, 'date', 0, true); fireEvent.keyDown(input, keys.TAB); input = expectToBeEditingField(container, 'account', 0, true); // The first escape closes the dropdown fireEvent.keyDown(input, keys.ESC); expect( container.querySelector('[data-testid="new-transaction"]') ).toBeTruthy(); // TOOD: Fix this // Now it should close the new transaction form // fireEvent.keyDown(input, keys.ESC); // expect( // container.querySelector('[data-testid="new-transaction"]') // ).toBeNull(); // The cancel button should also close the new transaction form updateProps({ isAdding: true }); let cancelButton = container.querySelectorAll( '[data-testid="new-transaction"] [data-testid="cancel-button"]' )[0]; fireEvent.click(cancelButton); expect(container.querySelector('[data-testid="new-transaction"]')).toBe( null ); }); test('transaction can be selected', () => { const { container } = renderTransactions(); editField(container, 'date', 2); const selectCell = queryField( container, 'select', '[data-testid=cell-button]', 2 ); fireEvent.click(selectCell); // The header is is selected as well as the single transaction expect(container.querySelectorAll('[data-testid=select] svg').length).toBe( 2 ); }); test('transaction can be split, updated, and deleted', async () => { const { container, getTransactions, updateProps } = renderTransactions(); let transactions = [...getTransactions()]; // Change the id to simulate a new transaction being added, and // work with that one. This makes sure that the transaction table // properly references new data. transactions[0] = { ...transactions[0], id: uuid.v4Sync() }; updateProps({ transactions }); function expectErrorToNotExist(transactions) { transactions.forEach((transaction, idx) => { expect(transaction.error).toBeFalsy(); }); } function expectErrorToExist(transactions) { transactions.forEach((transaction, idx) => { if (idx === 0) { expect(transaction.error).toBeTruthy(); } else { expect(transaction.error).toBeFalsy(); } }); } let input = editField(container, 'category', 0); let tooltip = container.querySelector('[data-testid="tooltip"]'); let splitButton = tooltip.querySelector( '[data-testid="split-transaction-button"]' ); // Make it clear that we are expected a negative transaction expect(getTransactions()[0].amount).toBe(-2777); expectErrorToNotExist([getTransactions()[0]]); // Make sure splitting a transaction works expect(getTransactions().length).toBe(5); fireEvent.click(splitButton); await waitForAutocomplete(); expect(getTransactions().length).toBe(6); expect(getTransactions()[0].is_parent).toBe(true); expect(getTransactions()[1].is_child).toBe(true); expect(getTransactions()[1].amount).toBe(0); expectErrorToExist(getTransactions().slice(0, 2)); let toolbars = container.querySelectorAll( '[data-testid="transaction-error"]' ); // Make sure the toolbar has appeared expect(toolbars.length).toBe(1); let toolbar = toolbars[0]; // Enter an amount for the new split transaction and make sure the // toolbar updates input = editField(container, 'debit', 1); fireEvent.change(input, { target: { value: '10.00' } }); fireEvent.keyDown(input, keys.TAB); expect(toolbar.innerHTML.includes('17.77')).toBeTruthy(); // Add another split transaction and make sure everything is // updated properly fireEvent.click(toolbar.querySelector('button')); expect(getTransactions().length).toBe(7); expect(getTransactions()[2].amount).toBe(0); expectErrorToExist(getTransactions().slice(0, 3)); // Change the amount to resolve the whole transaction. The toolbar // should disappear and no error should exist input = editField(container, 'debit', 2); fireEvent.change(input, { target: { value: '17.77' } }); fireEvent.keyDown(input, keys.TAB); expect( container.querySelectorAll('[data-testid="transaction-error"]').length ).toBe(0); expectErrorToNotExist(getTransactions().slice(0, 3)); // This snapshot makes sure the data is as we expect. It also // shows the sort order and makes sure that is correct expect(getTransactions().slice(0, 3)).toMatchSnapshot(); // Make sure deleting a split transaction updates the state again, // and deleting all split transactions turns it into a normal // transaction // // Deleting is disabled, unfortunately we can't delete in tests // yet because it doesn't do any batch editing // // const deleteCell = queryField(container, 'delete', '', 2); // fireEvent.click(deleteCell); // expect(getTransactions().length).toBe(6); // toolbar = container.querySelector('[data-testid="transaction-error"]'); // expect(toolbar).toBeTruthy(); // expect(toolbar.innerHTML.includes('17.77')).toBeTruthy(); // fireEvent.click(queryField(container, 'delete', '', 1)); // expect(getTransactions()[0].isParent).toBe(false); }); test('transaction with splits shows 0 in correct column', async () => { const { container, getTransactions } = renderTransactions(); let input = editField(container, 'category', 0); let tooltip = container.querySelector('[data-testid="tooltip"]'); let splitButton = tooltip.querySelector( '[data-testid="split-transaction-button"' ); // The first transaction should always be a negative amount expect(getTransactions()[0].amount).toBe(-2777); // Add two new split transactions expect(getTransactions().length).toBe(5); fireEvent.click(splitButton); await waitForAutocomplete(); fireEvent.click( container.querySelector('[data-testid="transaction-error"] button') ); expect(getTransactions().length).toBe(7); // The debit field should show the zeros expect(queryField(container, 'debit', '', 1).textContent).toBe('0.00'); expect(queryField(container, 'credit', '', 1).textContent).toBe(''); expect(queryField(container, 'debit', '', 2).textContent).toBe('0.00'); expect(queryField(container, 'credit', '', 2).textContent).toBe(''); // Change it to a credit transaction input = editField(container, 'credit', 0); fireEvent.change(input, { target: { value: '55.00' } }); fireEvent.keyDown(input, keys.TAB); // The zeros should now display in the credit column expect(queryField(container, 'debit', '', 1).textContent).toBe(''); expect(queryField(container, 'credit', '', 1).textContent).toBe('0.00'); expect(queryField(container, 'debit', '', 2).textContent).toBe(''); expect(queryField(container, 'credit', '', 2).textContent).toBe('0.00'); }); });