9c0df36e16
* style: enforce sorting of imports * style: alphabetize imports * style: merge duplicated imports
1005 lines
32 KiB
JavaScript
1005 lines
32 KiB
JavaScript
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 (
|
|
<TestProvider>
|
|
<SelectedProviderWithItems
|
|
name="transactions"
|
|
items={state.transactions}
|
|
fetchAllIds={() => state.transactions.map(t => t.id)}
|
|
>
|
|
<SplitsExpandedProvider>
|
|
<TransactionTable
|
|
{...this.props}
|
|
transactions={state.transactions}
|
|
loadMoreTransactions={() => {}}
|
|
payees={payees}
|
|
addNotification={n => console.log(n)}
|
|
onSave={this.onSave}
|
|
onSplit={this.onSplit}
|
|
onAdd={this.onAdd}
|
|
onAddSplit={this.onAddSplit}
|
|
onCreatePayee={this.onCreatePayee}
|
|
/>
|
|
</SplitsExpandedProvider>
|
|
</SelectedProviderWithItems>
|
|
</TestProvider>
|
|
);
|
|
}
|
|
}
|
|
|
|
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(
|
|
<LiveTransactionTable {...defaultProps} {...extraProps} />
|
|
);
|
|
return {
|
|
...result,
|
|
getTransactions: () => transactions,
|
|
updateProps: props =>
|
|
render(
|
|
<LiveTransactionTable {...defaultProps} {...extraProps} {...props} />,
|
|
{ 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');
|
|
});
|
|
});
|