/* * 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.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences.Editor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.preference.PreferenceManager; import android.support.test.espresso.Espresso; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.support.v4.app.Fragment; import android.util.Log; import com.kobakei.ratethisapp.RateThisApp; import org.gnucash.android.R; import org.gnucash.android.app.GnuCashApplication; import org.gnucash.android.db.DatabaseHelper; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.BooksDbAdapter; 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.AccountType; 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.receivers.AccountCreator; import org.gnucash.android.test.ui.util.DisableAnimationsRule; import org.gnucash.android.ui.account.AccountsActivity; import org.gnucash.android.ui.account.AccountsListFragment; 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.util.List; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; 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.closeSoftKeyboard; import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.action.ViewActions.swipeRight; import static android.support.test.espresso.action.ViewActions.typeText; import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; import static android.support.test.espresso.assertion.ViewAssertions.matches; 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.isEnabled; import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked; import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 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.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @RunWith(AndroidJUnit4.class) public class AccountsActivityTest { private static final String ACCOUNTS_CURRENCY_CODE = "USD"; // Don't add static here, otherwise it gets set to null by super.tearDown() private final Commodity ACCOUNTS_CURRENCY = Commodity.getInstance(ACCOUNTS_CURRENCY_CODE); private static final String SIMPLE_ACCOUNT_NAME = "Simple account"; private static final String SIMPLE_ACCOUNT_UID = "simple-account"; private static final String ROOT_ACCOUNT_NAME = "Root account"; private static final String ROOT_ACCOUNT_UID = "root-account"; private static final String PARENT_ACCOUNT_NAME = "Parent account"; private static final String PARENT_ACCOUNT_UID = "parent-account"; private static final String CHILD_ACCOUNT_UID = "child-account"; private static final String CHILD_ACCOUNT_NAME = "Child account"; public static final String TEST_DB_NAME = "test_gnucash_db.sqlite"; private static DatabaseHelper mDbHelper; private static SQLiteDatabase mDb; private static AccountsDbAdapter mAccountsDbAdapter; private static TransactionsDbAdapter mTransactionsDbAdapter; private static SplitsDbAdapter mSplitsDbAdapter; private AccountsActivity mAccountsActivity; public AccountsActivityTest() { // super(AccountsActivity.class); } @ClassRule public static DisableAnimationsRule disableAnimationsRule = new DisableAnimationsRule(); @Rule public ActivityTestRule<AccountsActivity> mActivityRule = new ActivityTestRule<>(AccountsActivity.class); @BeforeClass public static void prepTest(){ preventFirstRunDialogs(GnuCashApplication.getAppContext()); String activeBookUID = BooksDbAdapter.getInstance().getActiveBookUID(); mDbHelper = new DatabaseHelper(GnuCashApplication.getAppContext(), activeBookUID); try { mDb = mDbHelper.getWritableDatabase(); } catch (SQLException e) { Log.e("AccountsActivityTest", "Error getting database: " + e.getMessage()); mDb = mDbHelper.getReadableDatabase(); } mSplitsDbAdapter = SplitsDbAdapter.getInstance(); mTransactionsDbAdapter = TransactionsDbAdapter.getInstance(); mAccountsDbAdapter = AccountsDbAdapter.getInstance(); CommoditiesDbAdapter commoditiesDbAdapter = new CommoditiesDbAdapter(mDb); //initialize commodity constants } @Before public void setUp() throws Exception { mAccountsActivity = mActivityRule.getActivity(); // testPreconditions(); mAccountsDbAdapter.deleteAllRecords(); //clear the data Account simpleAccount = new Account(SIMPLE_ACCOUNT_NAME); simpleAccount.setUID(SIMPLE_ACCOUNT_UID); simpleAccount.setCommodity(Commodity.getInstance(ACCOUNTS_CURRENCY_CODE)); mAccountsDbAdapter.addRecord(simpleAccount, DatabaseAdapter.UpdateMethod.insert); refreshAccountsList(); } /** * Prevents the first-run dialogs (Whats new, Create accounts etc) from being displayed when testing * @param context Application context */ public static void preventFirstRunDialogs(Context context) { AccountsActivity.rateAppConfig = new RateThisApp.Config(10000, 10000); Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); //do not show first run dialog editor.putBoolean(context.getString(R.string.key_first_run), false); editor.putInt(AccountsActivity.LAST_OPEN_TAB_INDEX, AccountsActivity.INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT); //do not show "What's new" dialog String minorVersion = context.getString(R.string.app_minor_version); int currentMinor = Integer.parseInt(minorVersion); editor.putInt(context.getString(R.string.key_previous_minor_version), currentMinor); editor.commit(); } public void testDisplayAccountsList(){ AccountsActivity.createDefaultAccounts("EUR", mAccountsActivity); mAccountsActivity.recreate(); refreshAccountsList(); sleep(1000); onView(withText("Assets")).perform(scrollTo()); onView(withText("Expenses")).perform(click()); onView(withText("Books")).perform(scrollTo()); } @Test public void testSearchAccounts(){ String SEARCH_ACCOUNT_NAME = "Search Account"; Account account = new Account(SEARCH_ACCOUNT_NAME); account.setParentUID(SIMPLE_ACCOUNT_UID); mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert); //enter search query // ActionBarUtils.clickSherlockActionBarItem(mSolo, R.id.menu_search); onView(withId(R.id.menu_search)).perform(click()); onView(withId(R.id.search_src_text)).perform(typeText("Se")); onView(withText(SEARCH_ACCOUNT_NAME)).check(matches(isDisplayed())); onView(withId(R.id.search_src_text)).perform(clearText()); onView(withText(SEARCH_ACCOUNT_NAME)).check(doesNotExist()); } /** * Tests that an account can be created successfully and that the account list is sorted alphabetically. */ @Test public void testCreateAccount(){ assertThat(mAccountsDbAdapter.getAllRecords()).hasSize(1); onView(allOf(isDisplayed(), withId(R.id.fab_create_account))).perform(click()); String NEW_ACCOUNT_NAME = "A New Account"; onView(withId(R.id.input_account_name)).perform(typeText(NEW_ACCOUNT_NAME), closeSoftKeyboard()); sleep(1000); onView(withId(R.id.checkbox_placeholder_account)) .check(matches(isNotChecked())) .perform(click()); onView(withId(R.id.menu_save)).perform(click()); List<Account> accounts = mAccountsDbAdapter.getAllRecords(); assertThat(accounts).isNotNull(); assertThat(accounts).hasSize(2); Account newestAccount = accounts.get(0); //because of alphabetical sorting assertThat(newestAccount.getName()).isEqualTo(NEW_ACCOUNT_NAME); assertThat(newestAccount.getCommodity().getCurrencyCode()).isEqualTo(Money.DEFAULT_CURRENCY_CODE); assertThat(newestAccount.isPlaceholderAccount()).isTrue(); } @Test public void testChangeParentAccount() { final String accountName = "Euro Account"; Account account = new Account(accountName, Commodity.EUR); mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert); refreshAccountsList(); onView(withText(accountName)).perform(click()); openActionBarOverflowOrOptionsMenu(mAccountsActivity); onView(withText(R.string.title_edit_account)).perform(click()); onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); Espresso.closeSoftKeyboard(); onView(withId(R.id.checkbox_parent_account)).perform(scrollTo()) .check(matches(isNotChecked())) .perform(click()); // FIXME: explicitly select the parent account onView(withId(R.id.input_parent_account)).check(matches(isEnabled())).perform(click()); onView(withText(SIMPLE_ACCOUNT_NAME)).perform(click()); onView(withId(R.id.menu_save)).perform(click()); Account editedAccount = mAccountsDbAdapter.getRecord(account.getUID()); String parentUID = editedAccount.getParentUID(); assertThat(parentUID).isNotNull(); assertThat(parentUID).isEqualTo(SIMPLE_ACCOUNT_UID); } /** * When creating a sub-account (starting from within another account), if we change the account * type to another type with no accounts of that type, then the parent account list should be hidden. * The account which is then created is not a sub-account, but rather a top-level account */ @Test public void shouldHideParentAccountViewWhenNoParentsExist(){ onView(allOf(withText(SIMPLE_ACCOUNT_NAME), isDisplayed())).perform(click()); onView(withId(R.id.fragment_transaction_list)).perform(swipeRight()); onView(withId(R.id.fab_create_transaction)).check(matches(isDisplayed())).perform(click()); sleep(1000); onView(withId(R.id.checkbox_parent_account)).check(matches(allOf(isChecked()))); onView(withId(R.id.input_account_name)).perform(typeText("Trading account")); Espresso.closeSoftKeyboard(); onView(withId(R.id.layout_parent_account)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); onView(withId(R.id.input_account_type_spinner)).perform(click()); onData(allOf(is(instanceOf(String.class)), is(AccountType.TRADING.name()))).perform(click()); onView(withId(R.id.layout_parent_account)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); onView(withId(R.id.layout_parent_account)).check(matches(not(isDisplayed()))); onView(withId(R.id.menu_save)).perform(click()); sleep(1000); //no sub-accounts assertThat(mAccountsDbAdapter.getSubAccountCount(SIMPLE_ACCOUNT_UID)).isEqualTo(0); assertThat(mAccountsDbAdapter.getSubAccountCount(mAccountsDbAdapter.getOrCreateGnuCashRootAccountUID())).isEqualTo(2); assertThat(mAccountsDbAdapter.getSimpleAccountList()).extracting("mAccountType").contains(AccountType.TRADING); } @Test public void testEditAccount(){ refreshAccountsList(); onView(allOf(withParent(hasDescendant(withText(SIMPLE_ACCOUNT_NAME))), withId(R.id.options_menu))).perform(click()); // onView(withId(R.id.options_menu)).perform(click()); //there should only be one account visible sleep(1000); onView(withText(R.string.title_edit_account)).check(matches(isDisplayed())).perform(click()); // onView(withId(R.id.context_menu_edit_accounts)).check(matches(isDisplayed())).perform(click()); onView(withId(R.id.fragment_account_form)).check(matches(isDisplayed())); String editedAccountName = "An Edited Account"; onView(withId(R.id.input_account_name)).perform(clearText()).perform(typeText(editedAccountName)); onView(withId(R.id.menu_save)).perform(click()); List<Account> accounts = mAccountsDbAdapter.getAllRecords(); Account latest = accounts.get(0); //will be the first due to alphabetical sorting assertThat(latest.getName()).isEqualTo(editedAccountName); assertThat(latest.getCommodity().getCurrencyCode()).isEqualTo(ACCOUNTS_CURRENCY_CODE); } @Test public void editingAccountShouldNotDeleteTransactions(){ onView(allOf(withParent(hasDescendant(withText(SIMPLE_ACCOUNT_NAME))), withId(R.id.options_menu), isDisplayed())).perform(click()); Account account = new Account("Transfer Account"); account.setCommodity(Commodity.getInstance(ACCOUNTS_CURRENCY.getCurrencyCode())); Transaction transaction = new Transaction("Simple transaction"); transaction.setCommodity(ACCOUNTS_CURRENCY); Split split = new Split(new Money(BigDecimal.TEN, ACCOUNTS_CURRENCY), account.getUID()); transaction.addSplit(split); transaction.addSplit(split.createPair(SIMPLE_ACCOUNT_UID)); account.addTransaction(transaction); mAccountsDbAdapter.addRecord(account, DatabaseAdapter.UpdateMethod.insert); assertThat(mAccountsDbAdapter.getRecord(SIMPLE_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(2); onView(withText(R.string.title_edit_account)).perform(click()); onView(withId(R.id.menu_save)).perform(click()); assertThat(mAccountsDbAdapter.getRecord(SIMPLE_ACCOUNT_UID).getTransactionCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.fetchSplitsForAccount(SIMPLE_ACCOUNT_UID).getCount()).isEqualTo(1); assertThat(mSplitsDbAdapter.getSplitsForTransaction(transaction.getUID())).hasSize(2); } /** * 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(); } } public void testDeleteSimpleAccount() { refreshAccountsList(); assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(2); onView(allOf(withParent(hasDescendant(withText(SIMPLE_ACCOUNT_NAME))), withId(R.id.options_menu))).perform(click()); onView(withText(R.string.menu_delete)).perform(click()); assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(1); List<Account> accounts = mAccountsDbAdapter.getAllRecords(); assertThat(accounts).hasSize(0); //root account is never returned } @Test public void testDeleteAccountWithSubaccounts() { refreshAccountsList(); Account account = new Account("Sub-account"); account.setParentUID(SIMPLE_ACCOUNT_UID); account.setUID(CHILD_ACCOUNT_UID); mAccountsDbAdapter.addRecord(account); refreshAccountsList(); onView(allOf(withParent(hasDescendant(withText(SIMPLE_ACCOUNT_NAME))), withId(R.id.options_menu))).perform(click()); onView(withText(R.string.menu_delete)).perform(click()); onView(allOf(withParent(withId(R.id.accounts_options)), withId(R.id.radio_delete))).perform(click()); onView(withText(R.string.alert_dialog_ok_delete)).perform(click()); assertThat(accountExists(SIMPLE_ACCOUNT_UID)).isFalse(); assertThat(accountExists(CHILD_ACCOUNT_UID)).isFalse(); } @Test public void testDeleteAccountMovingSubaccounts() { long accountCount = mAccountsDbAdapter.getRecordsCount(); Account subAccount = new Account("Child account"); subAccount.setParentUID(SIMPLE_ACCOUNT_UID); Account tranferAcct = new Account("Other account"); mAccountsDbAdapter.addRecord(subAccount, DatabaseAdapter.UpdateMethod.insert); mAccountsDbAdapter.addRecord(tranferAcct, DatabaseAdapter.UpdateMethod.insert); assertThat(mAccountsDbAdapter.getRecordsCount()).isEqualTo(accountCount+2); refreshAccountsList(); onView(allOf(withParent(hasDescendant(withText(SIMPLE_ACCOUNT_NAME))), withId(R.id.options_menu))).perform(click()); onView(withText(R.string.menu_delete)).perform(click()); //// FIXME: 17.08.2016 This enabled check fails during some test runs - not reliable, investigate why onView(allOf(withParent(withId(R.id.accounts_options)), withId(R.id.radio_move))).check(matches(isEnabled())).perform(click()); onView(withText(R.string.alert_dialog_ok_delete)).perform(click()); assertThat(accountExists(SIMPLE_ACCOUNT_UID)).isFalse(); assertThat(accountExists(subAccount.getUID())).isTrue(); String newParentUID = mAccountsDbAdapter.getParentAccountUID(subAccount.getUID()); assertThat(newParentUID).isEqualTo(tranferAcct.getUID()); } /** * Checks if an account exists in the database * @param accountUID GUID of the account * @return {@code true} if the account exists, {@code false} otherwise */ private boolean accountExists(String accountUID) { try { mAccountsDbAdapter.getID(accountUID); return true; } catch (IllegalArgumentException e) { return false; } } //TODO: Test import of account file //TODO: test settings activity @Test public void testIntentAccountCreation(){ Intent intent = new Intent(Intent.ACTION_INSERT); intent.putExtra(Intent.EXTRA_TITLE, "Intent Account"); intent.putExtra(Intent.EXTRA_UID, "intent-account"); intent.putExtra(Account.EXTRA_CURRENCY_CODE, "EUR"); intent.setType(Account.MIME_TYPE); new AccountCreator().onReceive(mAccountsActivity, intent); Account account = mAccountsDbAdapter.getRecord("intent-account"); assertThat(account).isNotNull(); assertThat(account.getName()).isEqualTo("Intent Account"); assertThat(account.getUID()).isEqualTo("intent-account"); assertThat(account.getCommodity().getCurrencyCode()).isEqualTo("EUR"); } /** * Tests that the setup wizard is displayed on first run */ @Test public void shouldShowWizardOnFirstRun() throws Throwable { Editor editor = PreferenceManager.getDefaultSharedPreferences(mAccountsActivity) .edit(); //commit for immediate effect editor.remove(mAccountsActivity.getString(R.string.key_first_run)).commit(); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { mAccountsActivity.recreate(); } }); //check that wizard is shown onView(withText(mAccountsActivity.getString(R.string.title_setup_gnucash))) .check(matches(isDisplayed())); editor.putBoolean(mAccountsActivity.getString(R.string.key_first_run), false).apply(); } @After public void tearDown() throws Exception { if (mAccountsActivity != null) { mAccountsActivity.finish(); } } /** * Refresh the account list fragment */ private void refreshAccountsList(){ try { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { Fragment fragment = mAccountsActivity.getCurrentAccountListFragment(); ((AccountsListFragment) fragment).refresh(); } }); } catch (Throwable throwable) { System.err.println("Failed to refresh fragment"); } } }