/*
* Copyright (c) 2012 - 2015 Ngewi Fet <ngewif@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gnucash.android.test.ui;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.support.test.espresso.Espresso;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.DatabaseSchema;
import org.gnucash.android.db.adapter.AccountsDbAdapter;
import org.gnucash.android.db.adapter.CommoditiesDbAdapter;
import org.gnucash.android.db.adapter.DatabaseAdapter;
import org.gnucash.android.db.adapter.SplitsDbAdapter;
import org.gnucash.android.db.adapter.TransactionsDbAdapter;
import org.gnucash.android.model.Account;
import org.gnucash.android.model.Commodity;
import org.gnucash.android.model.Money;
import org.gnucash.android.model.Split;
import org.gnucash.android.model.Transaction;
import org.gnucash.android.model.TransactionType;
import org.gnucash.android.receivers.TransactionRecorder;
import org.gnucash.android.test.ui.util.DisableAnimationsRule;
import org.gnucash.android.ui.common.UxArgument;
import org.gnucash.android.ui.settings.PreferenceActivity;
import org.gnucash.android.ui.transaction.TransactionFormFragment;
import org.gnucash.android.ui.transaction.TransactionsActivity;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.clearText;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
@RunWith(AndroidJUnit4.class)
public class TransactionsActivityTest {
private static final String TRANSACTION_AMOUNT = "9.99";
private static final String TRANSACTION_NAME = "Pizza";
private static final String TRANSACTIONS_ACCOUNT_UID = "transactions-account";
private static final String TRANSACTIONS_ACCOUNT_NAME = "Transactions Account";
private static final String TRANSFER_ACCOUNT_NAME = "Transfer account";
private static final String TRANSFER_ACCOUNT_UID = "transfer_account";
public static final String CURRENCY_CODE = "USD";
public static Commodity COMMODITY = Commodity.DEFAULT_COMMODITY;
private Transaction mTransaction;
private long mTransactionTimeMillis;
private static AccountsDbAdapter mAccountsDbAdapter;
private static TransactionsDbAdapter mTransactionsDbAdapter;
private static SplitsDbAdapter mSplitsDbAdapter;
private TransactionsActivity mTransactionsActivity;
@ClassRule
public static DisableAnimationsRule disableAnimationsRule = new DisableAnimationsRule();
@Rule
public ActivityTestRule<TransactionsActivity> mActivityRule =
new ActivityTestRule<>(TransactionsActivity.class, true, false);
private Account mBaseAccount;
private Account mTransferAccount;
public TransactionsActivityTest() {
mBaseAccount = new Account(TRANSACTIONS_ACCOUNT_NAME, COMMODITY);
mBaseAccount.setUID(TRANSACTIONS_ACCOUNT_UID);
mTransferAccount = new Account(TRANSFER_ACCOUNT_NAME, COMMODITY);
mTransferAccount.setUID(TRANSFER_ACCOUNT_UID);
mTransactionTimeMillis = System.currentTimeMillis();
mTransaction = new Transaction(TRANSACTION_NAME);
mTransaction.setCommodity(COMMODITY);
mTransaction.setNote("What up?");
mTransaction.setTime(mTransactionTimeMillis);
Split split = new Split(new Money(TRANSACTION_AMOUNT, CURRENCY_CODE), TRANSACTIONS_ACCOUNT_UID);
split.setType(TransactionType.DEBIT);
mTransaction.addSplit(split);
mTransaction.addSplit(split.createPair(TRANSFER_ACCOUNT_UID));
mBaseAccount.addTransaction(mTransaction);
}
@BeforeClass
public static void prepareTestCase(){
Context context = GnuCashApplication.getAppContext();
AccountsActivityTest.preventFirstRunDialogs(context);
mSplitsDbAdapter = SplitsDbAdapter.getInstance();
mTransactionsDbAdapter = TransactionsDbAdapter.getInstance();
mAccountsDbAdapter = AccountsDbAdapter.getInstance();
COMMODITY = CommoditiesDbAdapter.getInstance().getCommodity(CURRENCY_CODE);
// PreferenceActivity.getActiveBookSharedPreferences(context)
// .edit().putBoolean(context.getString(R.string.key_use_compact_list), false)
// .apply();
}
@Before
public void setUp() throws Exception {
mAccountsDbAdapter.deleteAllRecords();
mAccountsDbAdapter.addRecord(mBaseAccount, DatabaseAdapter.UpdateMethod.insert);
mAccountsDbAdapter.addRecord(mTransferAccount, DatabaseAdapter.UpdateMethod.insert);
mTransactionsDbAdapter.addRecord(mTransaction, DatabaseAdapter.UpdateMethod.insert);
assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(3); //including ROOT account
assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, TRANSACTIONS_ACCOUNT_UID);
mTransactionsActivity = mActivityRule.launchActivity(intent);
//refreshTransactionsList();
}
private void validateTransactionListDisplayed(){
onView(withId(R.id.transaction_recycler_view)).check(matches(isDisplayed()));
}
private int getTransactionCount(){
return mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID).size();
}
private void validateTimeInput(long timeMillis){
String expectedValue = TransactionFormFragment.DATE_FORMATTER.format(new Date(timeMillis));
onView(withId(R.id.input_date)).check(matches(withText(expectedValue)));
expectedValue = TransactionFormFragment.TIME_FORMATTER.format(new Date(timeMillis));
onView(withId(R.id.input_time)).check(matches(withText(expectedValue)));
}
@Test
public void testAddTransactionShouldRequireAmount(){
validateTransactionListDisplayed();
int beforeCount = mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID);
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.input_transaction_name))
.check(matches(isDisplayed()))
.perform(typeText("Lunch"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.menu_save))
.check(matches(isDisplayed()))
.perform(click());
onView(withText(R.string.title_add_transaction)).check(matches(isDisplayed()));
sleep(1000);
assertToastDisplayed(R.string.toast_transanction_amount_required);
int afterCount = mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID);
assertThat(afterCount).isEqualTo(beforeCount);
}
/**
* Sleep the thread for a specified period
* @param millis Duration to sleep in milliseconds
*/
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Checks that a specific toast message is displayed
* @param toastString String that should be displayed
*/
private void assertToastDisplayed(int toastString) {
onView(withText(toastString))
.inRoot(withDecorView(not(mTransactionsActivity.getWindow().getDecorView())))
.check(matches(isDisplayed()));
}
private void validateEditTransactionFields(Transaction transaction){
onView(withId(R.id.input_transaction_name)).check(matches(withText(transaction.getDescription())));
Money balance = transaction.getBalance(TRANSACTIONS_ACCOUNT_UID);
NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault());
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(2);
onView(withId(R.id.input_transaction_amount)).check(matches(withText(formatter.format(balance.asDouble()))));
onView(withId(R.id.input_date)).check(matches(withText(TransactionFormFragment.DATE_FORMATTER.format(transaction.getTimeMillis()))));
onView(withId(R.id.input_time)).check(matches(withText(TransactionFormFragment.TIME_FORMATTER.format(transaction.getTimeMillis()))));
onView(withId(R.id.input_description)).check(matches(withText(transaction.getNote())));
validateTimeInput(transaction.getTimeMillis());
}
//TODO: Add test for only one account but with double-entry enabled
@Test
public void testAddTransaction(){
setDoubleEntryEnabled(true);
setDefaultTransactionType(TransactionType.DEBIT);
validateTransactionListDisplayed();
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.input_transaction_name)).perform(typeText("Lunch"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.input_transaction_amount)).perform(typeText("899"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.input_transaction_type))
.check(matches(allOf(isDisplayed(), withText(R.string.label_receive))))
.perform(click())
.check(matches(withText(R.string.label_spend)));
String expectedValue = NumberFormat.getInstance().format(-899);
onView(withId(R.id.input_transaction_amount)).check(matches(withText(expectedValue)));
int transactionsCount = getTransactionCount();
onView(withId(R.id.menu_save)).perform(click());
validateTransactionListDisplayed();
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
assertThat(transactions).hasSize(2);
Transaction transaction = transactions.get(0);
assertThat(transaction.getSplits()).hasSize(2);
assertThat(getTransactionCount()).isEqualTo(transactionsCount + 1);
}
@Test
public void testAddMultiCurrencyTransaction(){
Commodity euro = Commodity.getInstance("EUR");
Account euroAccount = new Account("Euro Konto", euro);
mAccountsDbAdapter.addRecord(euroAccount);
int transactionCount = mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID);
setDoubleEntryEnabled(true);
setDefaultTransactionType(TransactionType.DEBIT);
validateTransactionListDisplayed();
onView(withId(R.id.fab_create_transaction)).perform(click());
String transactionName = "Multicurrency lunch";
onView(withId(R.id.input_transaction_name)).perform(typeText(transactionName));
onView(withId(R.id.input_transaction_amount)).perform(typeText("10"));
Espresso.pressBack(); //close calculator keyboard
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(euroAccount.getFullName()))
.check(matches(isDisplayed()))
.perform(click());
onView(withId(R.id.menu_save)).perform(click());
onView(withText(R.string.msg_provide_exchange_rate)).check(matches(isDisplayed()));
onView(withId(R.id.radio_converted_amount)).perform(click());
onView(withId(R.id.input_converted_amount)).perform(typeText("5"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.btn_save)).perform(click());
onView(withId(R.id.menu_save)).perform(click());
List<Transaction> allTransactions = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
assertThat(allTransactions).hasSize(transactionCount+1);
Transaction multiTrans = allTransactions.get(0);
assertThat(multiTrans.getSplits()).hasSize(2);
assertThat(multiTrans.getSplits()).extracting("mAccountUID")
.contains(TRANSACTIONS_ACCOUNT_UID)
.contains(euroAccount.getUID());
Split euroSplit = multiTrans.getSplits(euroAccount.getUID()).get(0);
Money expectedQty = new Money("5", euro.getCurrencyCode());
Money expectedValue = new Money(BigDecimal.TEN, COMMODITY);
assertThat(euroSplit.getQuantity()).isEqualTo(expectedQty);
assertThat(euroSplit.getValue()).isEqualTo(expectedValue);
Split usdSplit = multiTrans.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0);
assertThat(usdSplit.getQuantity()).isEqualTo(expectedValue);
assertThat(usdSplit.getValue()).isEqualTo(expectedValue);
}
@Test
public void testEditTransaction(){
validateTransactionListDisplayed();
onView(withId(R.id.edit_transaction)).perform(click());
validateEditTransactionFields(mTransaction);
String trnName = "Pasta";
onView(withId(R.id.input_transaction_name)).perform(clearText(), typeText(trnName));
onView(withId(R.id.menu_save)).perform(click());
Transaction editedTransaction = mTransactionsDbAdapter.getRecord(mTransaction.getUID());
assertThat(editedTransaction.getDescription()).isEqualTo(trnName);
assertThat(editedTransaction.getSplits()).hasSize(2);
Split split = mTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0);
Split editedSplit = editedTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0);
assertThat(split.isEquivalentTo(editedSplit)).isTrue();
split = mTransaction.getSplits(TRANSFER_ACCOUNT_UID).get(0);
editedSplit = editedTransaction.getSplits(TRANSFER_ACCOUNT_UID).get(0);
assertThat(split.isEquivalentTo(editedSplit)).isTrue();
}
/**
* Tests that transactions splits are automatically balanced and an imbalance account will be created
* This test case assumes that single entry is used
*/
//TODO: move this to the unit tests
public void testAutoBalanceTransactions(){
setDoubleEntryEnabled(false);
mTransactionsDbAdapter.deleteAllRecords();
assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(0);
String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE));
assertThat(imbalanceAcctUID).isNull();
validateTransactionListDisplayed();
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.fragment_transaction_form)).check(matches(isDisplayed()));
onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance"));
onView(withId(R.id.input_transaction_amount)).perform(typeText("499"));
//no double entry so no split editor
//TODO: check that the split drawable is not displayed
onView(withId(R.id.menu_save)).perform(click());
assertThat(mTransactionsDbAdapter.getRecordsCount()).isEqualTo(1);
Transaction transaction = mTransactionsDbAdapter.getAllTransactions().get(0);
assertThat(transaction.getSplits()).hasSize(2);
imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE));
assertThat(imbalanceAcctUID).isNotNull();
assertThat(imbalanceAcctUID).isNotEmpty();
assertThat(mAccountsDbAdapter.isHiddenAccount(imbalanceAcctUID)).isTrue(); //imbalance account should be hidden in single entry mode
assertThat(transaction.getSplits()).extracting("mAccountUID").contains(imbalanceAcctUID);
}
/**
* Tests input of transaction splits using the split editor.
* Also validates that the imbalance from the split editor will be automatically added as a split
* //FIXME: find a more reliable way to test opening of the split editor
*/
@Test
public void testSplitEditor(){
setDoubleEntryEnabled(true);
setDefaultTransactionType(TransactionType.DEBIT);
mTransactionsDbAdapter.deleteAllRecords();
//when we start there should be no imbalance account in the system
String imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE));
assertThat(imbalanceAcctUID).isNull();
validateTransactionListDisplayed();
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.input_transaction_name)).perform(typeText("Autobalance"));
onView(withId(R.id.input_transaction_amount)).perform(typeText("499"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.btn_split_editor)).perform(click());
onView(withId(R.id.split_list_layout)).check(matches(allOf(isDisplayed(), hasDescendant(withId(R.id.input_split_amount)))));
onView(allOf(withId(R.id.input_split_amount), withText("-499"))).perform(clearText());
onView(allOf(withId(R.id.input_split_amount), withText(""))).perform(typeText("400"));
onView(withId(R.id.menu_save)).perform(click());
//after we use split editor, we should not be able to toggle the transaction type
onView(withId(R.id.input_transaction_type)).check(matches(not(isDisplayed())));
onView(withId(R.id.menu_save)).perform(click());
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactions();
assertThat(transactions).hasSize(1);
Transaction transaction = transactions.get(0);
assertThat(transaction.getSplits()).hasSize(3); //auto-balanced
imbalanceAcctUID = mAccountsDbAdapter.getImbalanceAccountUID(Commodity.getInstance(CURRENCY_CODE));
assertThat(imbalanceAcctUID).isNotNull();
assertThat(imbalanceAcctUID).isNotEmpty();
assertThat(mAccountsDbAdapter.isHiddenAccount(imbalanceAcctUID)).isFalse();
//at least one split will belong to the imbalance account
assertThat(transaction.getSplits()).extracting("mAccountUID").contains(imbalanceAcctUID);
List<Split> imbalanceSplits = mSplitsDbAdapter.getSplitsForTransactionInAccount(transaction.getUID(), imbalanceAcctUID);
assertThat(imbalanceSplits).hasSize(1);
Split split = imbalanceSplits.get(0);
assertThat(split.getValue().asBigDecimal()).isEqualTo(new BigDecimal("99.00"));
assertThat(split.getType()).isEqualTo(TransactionType.CREDIT);
}
private void setDoubleEntryEnabled(boolean enabled){
SharedPreferences prefs = PreferenceActivity.getActiveBookSharedPreferences();
Editor editor = prefs.edit();
editor.putBoolean(mTransactionsActivity.getString(R.string.key_use_double_entry), enabled);
editor.apply();
}
@Test
public void testDefaultTransactionType(){
setDefaultTransactionType(TransactionType.CREDIT);
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.input_transaction_type)).check(matches(allOf(isChecked(), withText(R.string.label_spend))));
}
private void setDefaultTransactionType(TransactionType type) {
SharedPreferences prefs = PreferenceActivity.getActiveBookSharedPreferences();
Editor editor = prefs.edit();
editor.putString(mTransactionsActivity.getString(R.string.key_default_transaction_type), type.name());
editor.commit();
}
//FIXME: Improve on this test
public void childAccountsShouldUseParentTransferAccountSetting(){
Account transferAccount = new Account("New Transfer Acct");
mAccountsDbAdapter.addRecord(transferAccount, DatabaseAdapter.UpdateMethod.insert);
mAccountsDbAdapter.addRecord(new Account("Higher account"), DatabaseAdapter.UpdateMethod.insert);
Account childAccount = new Account("Child Account");
childAccount.setParentUID(TRANSACTIONS_ACCOUNT_UID);
mAccountsDbAdapter.addRecord(childAccount, DatabaseAdapter.UpdateMethod.insert);
ContentValues contentValues = new ContentValues();
contentValues.put(DatabaseSchema.AccountEntry.COLUMN_DEFAULT_TRANSFER_ACCOUNT_UID, transferAccount.getUID());
mAccountsDbAdapter.updateRecord(TRANSACTIONS_ACCOUNT_UID, contentValues);
Intent intent = new Intent(mTransactionsActivity, TransactionsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, childAccount.getUID());
mTransactionsActivity.startActivity(intent);
onView(withId(R.id.input_transaction_amount)).perform(typeText("1299"));
clickOnView(R.id.menu_save);
//if our transfer account has a transaction then the right transfer account was used
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(transferAccount.getUID());
assertThat(transactions).hasSize(1);
}
@Test
public void testToggleTransactionType(){
validateTransactionListDisplayed();
onView(withId(R.id.edit_transaction)).perform(click());
validateEditTransactionFields(mTransaction);
onView(withId(R.id.input_transaction_type)).check(matches(
allOf(isDisplayed(), withText(R.string.label_receive))
)).perform(click()).check(matches(withText(R.string.label_spend)));
onView(withId(R.id.input_transaction_amount)).check(matches(withText("-9.99")));
onView(withId(R.id.menu_save)).perform(click());
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
assertThat(transactions).hasSize(1);
Transaction trx = transactions.get(0);
assertThat(trx.getSplits()).hasSize(2); //auto-balancing of splits
assertThat(trx.getBalance(TRANSACTIONS_ACCOUNT_UID).isNegative()).isTrue();
}
@Test
public void testOpenTransactionEditShouldNotModifyTransaction(){
validateTransactionListDisplayed();
onView(withId(R.id.edit_transaction)).perform(click());
validateTimeInput(mTransactionTimeMillis);
clickOnView(R.id.menu_save);
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
assertThat(transactions).hasSize(1);
Transaction transaction = transactions.get(0);
assertThat(TRANSACTION_NAME).isEqualTo(transaction.getDescription());
Date expectedDate = new Date(mTransactionTimeMillis);
Date trxDate = new Date(transaction.getTimeMillis());
assertThat(TransactionFormFragment.DATE_FORMATTER.format(expectedDate))
.isEqualTo(TransactionFormFragment.DATE_FORMATTER.format(trxDate));
assertThat(TransactionFormFragment.TIME_FORMATTER.format(expectedDate))
.isEqualTo(TransactionFormFragment.TIME_FORMATTER.format(trxDate));
Split baseSplit = transaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0);
Money expectedAmount = new Money(TRANSACTION_AMOUNT, CURRENCY_CODE);
assertThat(baseSplit.getValue()).isEqualTo(expectedAmount);
assertThat(baseSplit.getQuantity()).isEqualTo(expectedAmount);
assertThat(baseSplit.getType()).isEqualTo(TransactionType.DEBIT);
Split transferSplit = transaction.getSplits(TRANSFER_ACCOUNT_UID).get(0);
assertThat(transferSplit.getValue()).isEqualTo(expectedAmount);
assertThat(transferSplit.getQuantity()).isEqualTo(expectedAmount);
assertThat(transferSplit.getType()).isEqualTo(TransactionType.CREDIT);
}
@Test
public void testDeleteTransaction(){
onView(withId(R.id.options_menu)).perform(click());
onView(withText(R.string.menu_delete)).perform(click());
assertThat(0).isEqualTo(mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID));
}
@Test
public void testMoveTransaction(){
Account account = new Account("Move account");
account.setCommodity(Commodity.getInstance(CURRENCY_CODE));
mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert);
assertThat(mTransactionsDbAdapter.getAllTransactionsForAccount(account.getUID())).hasSize(0);
onView(withId(R.id.options_menu)).perform(click());
onView(withText(R.string.menu_move_transaction)).perform(click());
onView(withId(R.id.btn_save)).perform(click());
assertThat(mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID)).hasSize(0);
assertThat(mTransactionsDbAdapter.getAllTransactionsForAccount(account.getUID())).hasSize(1);
}
/**
* This test edits a transaction from within an account and removes the split belonging to that account.
* The account should then have a balance of 0 and the transaction has "moved" to another account
*/
@Test
public void editingSplit_shouldNotSetAmountToZero(){
setDoubleEntryEnabled(true);
setDefaultTransactionType(TransactionType.DEBIT);
mTransactionsDbAdapter.deleteAllRecords();
Account account = new Account("Z Account", Commodity.getInstance(CURRENCY_CODE));
mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert);
//create new transaction "Transaction Acct" --> "Transfer Account"
onView(withId(R.id.fab_create_transaction)).perform(click());
onView(withId(R.id.input_transaction_name)).perform(typeText("Test Split"));
onView(withId(R.id.input_transaction_amount)).perform(typeText("1024"));
onView(withId(R.id.menu_save)).perform(click());
assertThat(mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID)).isEqualTo(1);
sleep(500);
onView(withText("Test Split")).perform(click());
onView(withId(R.id.fab_edit_transaction)).perform(click());
onView(withId(R.id.btn_split_editor)).perform(click());
onView(withText(TRANSACTIONS_ACCOUNT_NAME)).perform(click());
onView(withText(account.getFullName())).perform(click());
onView(withId(R.id.menu_save)).perform(click());
onView(withId(R.id.menu_save)).perform(click());
assertThat(mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID)).isZero();
assertThat(mAccountsDbAdapter.getAccountBalance(account.getUID()))
.isEqualTo(new Money("1024", CURRENCY_CODE));
}
@Test
public void testDuplicateTransaction(){
assertThat(mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID)).hasSize(1);
onView(withId(R.id.options_menu)).perform(click());
onView(withText(R.string.menu_duplicate_transaction)).perform(click());
List<Transaction> dummyAccountTrns = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
assertThat(dummyAccountTrns).hasSize(2);
assertThat(dummyAccountTrns.get(0).getDescription()).isEqualTo(dummyAccountTrns.get(1).getDescription());
assertThat(dummyAccountTrns.get(0).getTimeMillis()).isNotEqualTo(dummyAccountTrns.get(1).getTimeMillis());
}
//TODO: add normal transaction recording
@Test
public void testLegacyIntentTransactionRecording(){
int beforeCount = mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID);
Intent transactionIntent = new Intent(Intent.ACTION_INSERT);
transactionIntent.setType(Transaction.MIME_TYPE);
transactionIntent.putExtra(Intent.EXTRA_TITLE, "Power intents");
transactionIntent.putExtra(Intent.EXTRA_TEXT, "Intents for sale");
transactionIntent.putExtra(Transaction.EXTRA_AMOUNT, new BigDecimal(4.99));
transactionIntent.putExtra(Transaction.EXTRA_ACCOUNT_UID, TRANSACTIONS_ACCOUNT_UID);
transactionIntent.putExtra(Transaction.EXTRA_TRANSACTION_TYPE, TransactionType.DEBIT.name());
transactionIntent.putExtra(Account.EXTRA_CURRENCY_CODE, "USD");
new TransactionRecorder().onReceive(mTransactionsActivity, transactionIntent);
int afterCount = mTransactionsDbAdapter.getTransactionsCount(TRANSACTIONS_ACCOUNT_UID);
assertThat(beforeCount + 1).isEqualTo(afterCount);
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(TRANSACTIONS_ACCOUNT_UID);
for (Transaction transaction : transactions) {
if (transaction.getDescription().equals("Power intents")){
assertThat("Intents for sale").isEqualTo(transaction.getNote());
assertThat(4.99).isEqualTo(transaction.getBalance(TRANSACTIONS_ACCOUNT_UID).asDouble());
}
}
}
/**
* Opening a transactions and then hitting save button without changing anything should have no side-effects
* This is similar to the test @{@link #testOpenTransactionEditShouldNotModifyTransaction()}
* with the difference that this test checks multi-currency transactions
*/
@Test
public void openingAndSavingMultiCurrencyTransaction_shouldNotModifyTheSplits(){
Commodity bgnCommodity = CommoditiesDbAdapter.getInstance().getCommodity("BGN");
Account account = new Account("Zen Account", bgnCommodity);
mAccountsDbAdapter.addRecord(account);
onView(withId(R.id.fab_create_transaction)).perform(click());
String trnDescription = "Multi-currency trn";
onView(withId(R.id.input_transaction_name)).perform(typeText(trnDescription));
onView(withId(R.id.input_transaction_amount)).perform(typeText("10"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(account.getFullName())).perform(click());
//at this point, the transfer funds dialog should be shown
onView(withText(R.string.msg_provide_exchange_rate)).check(matches(isDisplayed()));
onView(withId(R.id.radio_converted_amount)).perform(click());
onView(withId(R.id.input_converted_amount)).perform(typeText("5"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.btn_save)).perform(click()); //close currency exchange dialog
onView(withId(R.id.menu_save)).perform(click()); //save transaction
List<Transaction> transactions = mTransactionsDbAdapter.getAllTransactionsForAccount(account.getUID());
assertThat(transactions).hasSize(1);
Transaction transaction = transactions.get(0);
assertThat(transaction.getSplits()).hasSize(2);
assertThat(transaction.getSplits()).extracting("mAccountUID")
.contains(account.getUID()).contains(mBaseAccount.getUID());
onView(allOf(withParent(hasDescendant(withText(trnDescription))),
withId(R.id.edit_transaction))).perform(click());
//do nothing to the transaction, just save it
onView(withId(R.id.menu_save)).perform(click());
transaction = mTransactionsDbAdapter.getRecord(transaction.getUID());
Split baseSplit = transaction.getSplits(mBaseAccount.getUID()).get(0);
Money expectedValueAmount = new Money(BigDecimal.TEN, COMMODITY);
assertThat(baseSplit.getValue()).isEqualTo(expectedValueAmount);
assertThat(baseSplit.getQuantity()).isEqualTo(expectedValueAmount);
Split transferSplit = transaction.getSplits(account.getUID()).get(0);
Money convertedQuantity = new Money("5", "BGN");
assertThat(transferSplit.getValue()).isEqualTo(expectedValueAmount);
assertThat(transferSplit.getQuantity()).isEqualTo(convertedQuantity);
}
/**
* If a multi-currency transaction is edited so that it is no longer multicurrency, then the
* values for split and quantity should be adjusted accordingly so that they are consistent
* <p>
* Basically the test works like this:
* <ol>
* <li>Create a multicurrency transaction</li>
* <li>Change the transfer account so that both splits are of the same currency</li>
* <li>We now expect both the values and quantities of the splits to be the same</li>
* </ol>
* </p>
*/
@Test
public void testEditingTransferAccountOfMultiCurrencyTransaction(){
mTransactionsDbAdapter.deleteAllRecords(); //clean slate
Commodity euroCommodity = CommoditiesDbAdapter.getInstance().getCommodity("EUR");
Account euroAccount = new Account("Euro Account", euroCommodity);
mAccountsDbAdapter.addRecord(euroAccount);
Money expectedValue = new Money(BigDecimal.TEN, COMMODITY);
Money expectedQty = new Money("5", "EUR");
String trnDescription = "Multicurrency Test Trn";
Transaction multiTransaction = new Transaction(trnDescription);
Split split1 = new Split(expectedValue, TRANSACTIONS_ACCOUNT_UID);
split1.setType(TransactionType.DEBIT);
Split split2 = new Split(expectedValue, expectedQty, euroAccount.getUID());
split2.setType(TransactionType.CREDIT);
multiTransaction.addSplit(split1);
multiTransaction.addSplit(split2);
multiTransaction.setCommodity(COMMODITY);
mTransactionsDbAdapter.addRecord(multiTransaction);
Transaction savedTransaction = mTransactionsDbAdapter.getRecord(multiTransaction.getUID());
assertThat(savedTransaction.getSplits()).extracting("mQuantity").contains(expectedQty);
assertThat(savedTransaction.getSplits()).extracting("mValue").contains(expectedValue);
refreshTransactionsList();
onView(withText(trnDescription)).check(matches(isDisplayed())); //transaction was added
onView(allOf(withParent(hasDescendant(withText(trnDescription))),
withId(R.id.edit_transaction))).perform(click());
//now change the transfer account to be no longer multi-currency
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(mTransferAccount.getFullName())).perform(click());
onView(withId(R.id.menu_save)).perform(click());
//no splits should be in the euro account anymore
List<Transaction> euroTransxns = mTransactionsDbAdapter.getAllTransactionsForAccount(euroAccount.getUID());
assertThat(euroTransxns).hasSize(0);
List<Transaction> transferAcctTrns = mTransactionsDbAdapter.getAllTransactionsForAccount(mTransferAccount.getUID());
assertThat(transferAcctTrns).hasSize(1);
Transaction singleCurrencyTrn = transferAcctTrns.get(0);
assertThat(singleCurrencyTrn.getUID()).isEqualTo(multiTransaction.getUID()); //should be the same one, just different splits
//the crux of the test. All splits should now have value and quantity of USD $10
List<Split> allSplits = singleCurrencyTrn.getSplits();
assertThat(allSplits).extracting("mAccountUID")
.contains(mTransferAccount.getUID())
.doesNotContain(euroAccount.getUID());
assertThat(allSplits).extracting("mValue").contains(expectedValue).doesNotContain(expectedQty);
assertThat(allSplits).extracting("mQuantity").contains(expectedValue).doesNotContain(expectedQty);
}
/**
* In this test we check that editing a transaction and switching the transfer account to one
* which is of a different currency and then back again should not have side-effects.
* The split value and quantity should remain consistent.
*/
@Test
public void editingTransferAccount_shouldKeepSplitAmountsConsistent() {
mTransactionsDbAdapter.deleteAllRecords(); //clean slate
Commodity euroCommodity = CommoditiesDbAdapter.getInstance().getCommodity("EUR");
Account euroAccount = new Account("Euro Account", euroCommodity);
mAccountsDbAdapter.addRecord(euroAccount);
Money expectedValue = new Money(BigDecimal.TEN, COMMODITY);
Money expectedQty = new Money("5", "EUR");
String trnDescription = "Multicurrency Test Trn";
Transaction multiTransaction = new Transaction(trnDescription);
Split split1 = new Split(expectedValue, TRANSACTIONS_ACCOUNT_UID);
split1.setType(TransactionType.CREDIT);
Split split2 = new Split(expectedValue, expectedQty, euroAccount.getUID());
split2.setType(TransactionType.DEBIT);
multiTransaction.addSplit(split1);
multiTransaction.addSplit(split2);
multiTransaction.setCommodity(COMMODITY);
mTransactionsDbAdapter.addRecord(multiTransaction);
Transaction savedTransaction = mTransactionsDbAdapter.getRecord(multiTransaction.getUID());
assertThat(savedTransaction.getSplits()).extracting("mQuantity").contains(expectedQty);
assertThat(savedTransaction.getSplits()).extracting("mValue").contains(expectedValue);
assertThat(savedTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0)
.isEquivalentTo(multiTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0)))
.isTrue();
refreshTransactionsList();
//open transaction for editing
onView(withText(trnDescription)).check(matches(isDisplayed())); //transaction was added
onView(allOf(withParent(hasDescendant(withText(trnDescription))),
withId(R.id.edit_transaction))).perform(click());
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(TRANSFER_ACCOUNT_NAME)).perform(click());
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(euroAccount.getFullName())).perform(click());
onView(withId(R.id.input_converted_amount)).perform(typeText("5"));
Espresso.closeSoftKeyboard();
onView(withId(R.id.btn_save)).perform(click());
onView(withId(R.id.input_transfer_account_spinner)).perform(click());
onView(withText(TRANSFER_ACCOUNT_NAME)).perform(click());
onView(withId(R.id.menu_save)).perform(click());
Transaction editedTransaction = mTransactionsDbAdapter.getRecord(multiTransaction.getUID());
assertThat(editedTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0)
.isEquivalentTo(savedTransaction.getSplits(TRANSACTIONS_ACCOUNT_UID).get(0)))
.isTrue();
Money firstAcctBalance = mAccountsDbAdapter.getAccountBalance(TRANSACTIONS_ACCOUNT_UID);
assertThat(firstAcctBalance).isEqualTo(editedTransaction.getBalance(TRANSACTIONS_ACCOUNT_UID));
Money transferBalance = mAccountsDbAdapter.getAccountBalance(TRANSFER_ACCOUNT_UID);
assertThat(transferBalance).isEqualTo(editedTransaction.getBalance(TRANSFER_ACCOUNT_UID));
assertThat(editedTransaction.getBalance(TRANSFER_ACCOUNT_UID)).isEqualTo(expectedValue);
Split transferAcctSplit = editedTransaction.getSplits(TRANSFER_ACCOUNT_UID).get(0);
assertThat(transferAcctSplit.getQuantity()).isEqualTo(expectedValue);
assertThat(transferAcctSplit.getValue()).isEqualTo(expectedValue);
}
/**
* Simple wrapper for clicking on views with espresso
* @param viewId View resource ID
*/
private void clickOnView(int viewId){
onView(withId(viewId)).perform(click());
}
/**
* Refresh the account list fragment
*/
private void refreshTransactionsList(){
try {
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mTransactionsActivity.refresh();
}
});
} catch (Throwable throwable) {
System.err.println("Failed to refresh fragment");
}
}
@After
public void tearDown() throws Exception {
if (mTransactionsActivity != null)
mTransactionsActivity.finish();
}
}