From dd86e2d326fed06fee8279d4537929e0cfdf7f9a Mon Sep 17 00:00:00 2001 From: Eduard Maximovich Date: Thu, 10 Jan 2019 20:42:44 +0300 Subject: [PATCH 1/6] send tab ui tests and fixes --- app/build.gradle | 31 +- app/proguard-test-rules.pro | 20 +- .../auth/ui/AdvancedModeAuthTest.java | 163 ------- .../data/KVStorageQueueSaveTest.java | 6 +- .../bipwallet/tests/internal/MyMatchers.java | 161 +++++++ .../{ => tests}/internal/TestWallet.java | 23 +- .../internal/WalletTestRunner.java | 12 +- .../internal/di/TestAnalyticsModule.java | 74 ++++ .../internal/di/TestWalletComponent.java | 49 ++- .../tests/internal/di/TestWalletModule.java | 211 +++++++++ .../{ => tests}/ui/AdvancedModeTest.java | 40 +- .../minter/bipwallet/tests/ui/BaseUiTest.java | 253 +++++++++++ .../bipwallet/tests/ui/SendCoinsTabTest.java | 411 ++++++++++++++++++ .../ui/actions/BottomNavigationExAction.java | 4 +- .../bipwallet/auth/ui/AuthActivity.java | 18 +- .../exchange/ui/BaseCoinTabFragment.java | 4 +- .../bipwallet/home/ui/HomeActivity.java | 8 +- .../bipwallet/home/views/HomePresenter.java | 3 +- .../minter/bipwallet/internal/Wallet.java | 19 +- .../internal/helpers/MathHelper.java | 31 +- .../validators/MinterUsernameValidator.java | 4 +- .../bipwallet/internal/storage/KVStorage.java | 4 +- .../bipwallet/internal/system/StringUtil.java | 66 +++ .../testing/CallbackIdlingResource.java | 76 ++++ .../bipwallet/sending/SendTabModule.java | 8 +- .../account/WalletAccountSelectorDialog.java | 7 +- .../bipwallet/sending/ui/SendTabFragment.java | 35 +- .../sending/views/SendTabPresenter.java | 20 +- .../settings/repo/MinterBotRepository.java | 4 +- app/src/main/res/values/strings.xml | 3 +- build.gradle | 4 +- gradlew.bat | 0 32 files changed, 1528 insertions(+), 244 deletions(-) delete mode 100644 app/src/androidTest/java/network/minter/bipwallet/auth/ui/AdvancedModeAuthTest.java rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/data/KVStorageQueueSaveTest.java (95%) create mode 100644 app/src/androidTest/java/network/minter/bipwallet/tests/internal/MyMatchers.java rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/internal/TestWallet.java (67%) rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/internal/WalletTestRunner.java (86%) create mode 100644 app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestAnalyticsModule.java rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/internal/di/TestWalletComponent.java (67%) create mode 100644 app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletModule.java rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/ui/AdvancedModeTest.java (84%) create mode 100644 app/src/androidTest/java/network/minter/bipwallet/tests/ui/BaseUiTest.java create mode 100644 app/src/androidTest/java/network/minter/bipwallet/tests/ui/SendCoinsTabTest.java rename app/src/androidTest/java/network/minter/bipwallet/{ => tests}/ui/actions/BottomNavigationExAction.java (97%) create mode 100644 app/src/main/java/network/minter/bipwallet/internal/system/StringUtil.java create mode 100644 app/src/main/java/network/minter/bipwallet/internal/system/testing/CallbackIdlingResource.java mode change 100644 => 100755 gradlew.bat diff --git a/app/build.gradle b/app/build.gradle index d034f60b..3a7fef98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -39,9 +39,9 @@ android { applicationId "network.minter.bipwallet" minSdkVersion minterMinSdk targetSdkVersion minterMaxSdk - versionCode 58 - versionName "1.2.5" - testInstrumentationRunner "network.minter.bipwallet.internal.WalletTestRunner" + versionCode 59 + versionName "1.2.6" + testInstrumentationRunner "network.minter.bipwallet.tests.internal.WalletTestRunner" vectorDrawables.useSupportLibrary = true multiDexEnabled true } @@ -60,6 +60,13 @@ android { additionalParameters "--no-version-vectors" } + testOptions { + animationsDisabled true + unitTests.all { + + } + } + signingConfigs { config { @@ -79,7 +86,7 @@ android { debug { signingConfig signingConfigs.config - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', 'proguard-test-rules.pro', 'proguard-release-rules.pro' testProguardFile 'proguard-test-rules.pro' } @@ -220,9 +227,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' // networking - implementation 'com.squareup.okhttp3:okhttp:3.11.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' - implementation 'com.squareup.retrofit2:retrofit:2.4.0' + implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' @@ -234,7 +241,6 @@ dependencies { implementation 'com.makeramen:roundedimageview:2.3.0' // don't update to 2.2.0 - some issues when using elevation - //noinspection GradleDependency implementation 'de.hdodenhof:circleimageview:2.1.0' annotationProcessor 'io.sweers.barber:barber-compiler:1.3.1' @@ -246,12 +252,19 @@ dependencies { implementation 'com.pnikosis:materialish-progress:1.7' // testing + implementation 'com.android.support.test.espresso:espresso-core:3.0.2' testImplementation 'junit:junit:4.12' + testImplementation "org.mockito:mockito-core:2.23.4" + androidTestImplementation 'org.mockito:mockito-android:2.23.4' + androidTestImplementation 'com.squareup.retrofit2:retrofit-mock:2.5.0' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:rules:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +// androidTest androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2' androidTestImplementation 'com.squareup.rx.idler:rx2-idler:0.9.0' + + androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.16' + androidTestAnnotationProcessor 'com.google.dagger:dagger-android-processor:2.16' } repositories { mavenCentral() diff --git a/app/proguard-test-rules.pro b/app/proguard-test-rules.pro index 1cc0df7e..f9e8696d 100644 --- a/app/proguard-test-rules.pro +++ b/app/proguard-test-rules.pro @@ -3,15 +3,15 @@ -keepattributes *Annotation* --dontnote junit.framework.** --dontnote junit.runner.** - --dontwarn android.test.** --dontwarn android.support.test.** --dontwarn org.junit.** --dontwarn org.hamcrest.** --dontwarn com.squareup.javawriter.JavaWriter +-keep class org.hamcrest.** {*;} +-keep class junit.framework.** {*;} +-keep class junit.runner.** {*;} +-keep class org.junit.** { *; } +-keep class org.mockito.** {*;} +-keep class android.test.** {*;} +-keep class android.support.test.** {*;} +-keep class com.squareup.javawriter.JavaWriter {*;} -keep class io.reactivex.plugins.RxJavaPlugins { *; } -keep class io.reactivex.disposables.CompositeDisposable { *; } -# Uncomment this if you use Mockito -#-dontwarn org.mockito.** \ No newline at end of file +-keep class network.minter.bipwallet.* { *; } +-keep class android.support.test.** { *; } diff --git a/app/src/androidTest/java/network/minter/bipwallet/auth/ui/AdvancedModeAuthTest.java b/app/src/androidTest/java/network/minter/bipwallet/auth/ui/AdvancedModeAuthTest.java deleted file mode 100644 index 39d3c4ad..00000000 --- a/app/src/androidTest/java/network/minter/bipwallet/auth/ui/AdvancedModeAuthTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) by MinterTeam. 2018 - * @link Org Github - * @link Maintainer Github - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package network.minter.bipwallet.auth.ui; - - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.support.test.espresso.ViewInteraction; -import android.support.test.filters.LargeTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; - -import com.squareup.rx2.idler.Rx2Idler; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import io.reactivex.plugins.RxJavaPlugins; -import network.minter.bipwallet.R; - -import static android.support.test.espresso.Espresso.onView; -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.replaceText; -import static android.support.test.espresso.action.ViewActions.scrollTo; -import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.matcher.ViewMatchers.hasErrorText; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.withClassName; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static android.support.test.espresso.matcher.ViewMatchers.withText; -import static junit.framework.Assert.assertNotNull; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; - -@LargeTest -@RunWith(AndroidJUnit4.class) -public class AdvancedModeAuthTest { - - @Rule - public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(AuthActivity.class); - - private static Matcher childAtPosition( - final Matcher parentMatcher, final int position) { - - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("Child at position " + position + " in parent "); - parentMatcher.describeTo(description); - } - - @Override - public boolean matchesSafely(View view) { - ViewParent parent = view.getParent(); - return parent instanceof ViewGroup && parentMatcher.matches(parent) - && view.equals(((ViewGroup) parent).getChildAt(position)); - } - }; - } - - @Before - public void regIdlingResource() { - RxJavaPlugins.setInitComputationSchedulerHandler( - Rx2Idler.create("RxJava 2.x Computation Scheduler") - ); - RxJavaPlugins.setInitIoSchedulerHandler( - Rx2Idler.create("RxJava 2.x IO Scheduler")); - } - - @Test - public void authActivityTest() { - // wait for fragments - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - ViewInteraction advanceModeButton = onView( - allOf(withId(R.id.action_advanced_mode), withText("Advanced mode"), isDisplayed())); - // click on advanced mode - advanceModeButton.perform(click()); - - ViewInteraction seedInput = onView(allOf(withId(R.id.input_seed))); - - // paste to seed invalid text - seedInput.perform(replaceText("WTF"), closeSoftKeyboard()); - - ViewInteraction advancedActivateButton = onView(allOf(withId(R.id.action_activate), withText("Activate"))); - - // try to login with invalid seed - advancedActivateButton.perform(click()); - - // check invalid seed error - seedInput.check(matches(hasErrorText("Phrase is not valid"))); - - ViewInteraction generateMnemonicButton = onView(allOf(withId(R.id.action_generate), withText("Generate Address"))); - // click on generate new seed - generateMnemonicButton.perform(click()); - - ViewInteraction appCompatTextView = onView(allOf(withId(R.id.action_copy), withText("Copy"))); - appCompatTextView.perform(scrollTo(), click()); - - ClipboardManager clipboardManager = ((ClipboardManager) mActivityTestRule.getActivity().getSystemService(Context.CLIPBOARD_SERVICE)); - ClipData copiedMnemonic = clipboardManager.getPrimaryClip(); - assertNotNull(copiedMnemonic); - CharSequence copiedMnemonicText = copiedMnemonic.getItemAt(0).getText(); - assertNotNull(copiedMnemonicText); - - - ViewInteraction switch_ = onView( - allOf(withId(R.id.switch_save_mnemonic), - childAtPosition( - allOf(withId(R.id.row_save_mnemonic), - childAtPosition( - withClassName(is("android.support.constraint.ConstraintLayout")), - 7)), - 1))); - switch_.perform(scrollTo(), click()); - - ViewInteraction walletButton4 = onView( - allOf(withId(R.id.action), withText("Launch the wallet"), - childAtPosition( - childAtPosition( - withClassName(is("android.widget.ScrollView")), - 0), - 9))); - walletButton4.perform(scrollTo(), click()); - } -} diff --git a/app/src/androidTest/java/network/minter/bipwallet/data/KVStorageQueueSaveTest.java b/app/src/androidTest/java/network/minter/bipwallet/tests/data/KVStorageQueueSaveTest.java similarity index 95% rename from app/src/androidTest/java/network/minter/bipwallet/data/KVStorageQueueSaveTest.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/data/KVStorageQueueSaveTest.java index 2122c812..c8593fde 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/data/KVStorageQueueSaveTest.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/data/KVStorageQueueSaveTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,7 +24,7 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.data; +package network.minter.bipwallet.tests.data; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -35,8 +35,8 @@ import java.util.LinkedList; import java.util.Queue; -import network.minter.bipwallet.internal.TestWallet; import network.minter.bipwallet.internal.storage.KVStorage; +import network.minter.bipwallet.tests.internal.TestWallet; import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertEquals; diff --git a/app/src/androidTest/java/network/minter/bipwallet/tests/internal/MyMatchers.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/MyMatchers.java new file mode 100644 index 00000000..a66e4c58 --- /dev/null +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/MyMatchers.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.tests.internal; + +import android.support.annotation.RestrictTo; +import android.support.annotation.StringRes; +import android.support.design.widget.TextInputLayout; +import android.view.View; +import android.view.ViewParent; + +import junit.framework.AssertionFailedError; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import network.minter.bipwallet.internal.Wallet; +import network.minter.bipwallet.internal.system.StringUtil; +import timber.log.Timber; + +/** + * minter-android-wallet. 2019 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +@RestrictTo(RestrictTo.Scope.TESTS) +public final class MyMatchers { + + public static Matcher withInputLayoutError(@StringRes int expectedErrorTextRes) { + return new TypeSafeMatcher() { + @Override + public boolean matchesSafely(View view) { + TextInputLayout inputLayout = findInParentTree(TextInputLayout.class, view); + if (inputLayout == null) { + throw new AssertionFailedError(String.format("View with id %s does not have a parent TextInputLayout", view.getContext().getResources().getResourceEntryName(view.getId()))); + } + + CharSequence inputError = inputLayout.getError(); + Timber.d("Input error: %s", inputError); + return StringUtil.safeCompare(inputLayout, inputError, expectedErrorTextRes); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("Input error is: '%s'", TestWallet.app().res().getString(expectedErrorTextRes))); + } + }; + } + + public static Matcher withInputLayoutError(final String expectedErrorText) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + TextInputLayout inputLayout = findInParentTree(TextInputLayout.class, view); + if (inputLayout == null) { + throw new AssertionFailedError(String.format("View with id %s does not have a parent TextInputLayout", view.getContext().getResources().getResourceEntryName(view.getId()))); + } + + CharSequence inputError = inputLayout.getError(); + Timber.d("Input error: %s", inputError); + return StringUtil.safeCompare(inputError, expectedErrorText); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("Input error is: '%s'", expectedErrorText)); + } + }; + } + + public static Matcher withInputLayoutHint(@StringRes int expectedErrorTextRes) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + TextInputLayout inputLayout = findInParentTree(TextInputLayout.class, view); + if (inputLayout == null) { + throw new AssertionFailedError(String.format("View with id %s does not have a parent TextInputLayout", view.getContext().getResources().getResourceEntryName(view.getId()))); + } + + CharSequence inputHint = inputLayout.getHint(); + Timber.d("Input hint: %s", inputHint); + return StringUtil.safeCompare(inputLayout, inputHint, expectedErrorTextRes); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("Input hint is: '%s'", Wallet.app().res().getString(expectedErrorTextRes))); + } + }; + } + + public static Matcher withInputLayoutHint(final String expectedErrorText) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + TextInputLayout inputLayout = findInParentTree(TextInputLayout.class, view); + if (inputLayout == null) { + throw new AssertionFailedError(String.format("View with id %s does not have a parent TextInputLayout", view.getContext().getResources().getResourceEntryName(view.getId()))); + } + + CharSequence inputHint = inputLayout.getHint(); + Timber.d("Input hint: %s", inputHint); + return StringUtil.safeCompare(inputHint, expectedErrorText); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("Input hint is: '%s'", expectedErrorText)); + } + }; + } + + private static T findInParentTree(Class cls, View view) { + if (view == null || cls == null) { + return null; + } + + ViewParent t = view.getParent(); + if (t == null) { + return null; + } + + if (cls.isInstance(t)) { + return cls.cast(t); + } + + while (t.getParent() != null) { + t = t.getParent(); + if (cls.isInstance(t)) { + return cls.cast(t); + } + } + return null; + } +} diff --git a/app/src/androidTest/java/network/minter/bipwallet/internal/TestWallet.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/TestWallet.java similarity index 67% rename from app/src/androidTest/java/network/minter/bipwallet/internal/TestWallet.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/internal/TestWallet.java index f56c5380..bd9ec9a7 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/internal/TestWallet.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/TestWallet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,18 +24,35 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.internal; +package network.minter.bipwallet.tests.internal; + +import network.minter.bipwallet.internal.Wallet; +import network.minter.bipwallet.internal.di.HelpersModule; +import network.minter.bipwallet.internal.di.RepoModule; +import network.minter.bipwallet.tests.internal.di.DaggerTestWalletComponent; +import network.minter.bipwallet.tests.internal.di.TestWalletModule; /** * minter-android-wallet. 2018 - * * @author Eduard Maximovich */ public class TestWallet extends Wallet { + static { + sEnableInject = false; + } + @Override public void onCreate() { super.onCreate(); + + app = DaggerTestWalletComponent.builder() + .testWalletModule(new TestWalletModule(this)) + .helpersModule(new HelpersModule()) + .repoModule(new RepoModule()) + .build(); + + app.inject(this); } } diff --git a/app/src/androidTest/java/network/minter/bipwallet/internal/WalletTestRunner.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/WalletTestRunner.java similarity index 86% rename from app/src/androidTest/java/network/minter/bipwallet/internal/WalletTestRunner.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/internal/WalletTestRunner.java index 19a9c184..aa28906b 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/internal/WalletTestRunner.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/WalletTestRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,10 +24,12 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.internal; +package network.minter.bipwallet.tests.internal; import android.app.Application; import android.content.Context; +import android.os.Bundle; +import android.os.StrictMode; import android.support.test.runner.AndroidJUnitRunner; import com.squareup.rx2.idler.Rx2Idler; @@ -46,6 +48,12 @@ public Application newApplication(ClassLoader cl, String className, Context cont return super.newApplication(cl, TestWallet.class.getName(), context); } + @Override + public void onCreate(Bundle arguments) { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build()); + super.onCreate(arguments); + } + @Override public void onStart() { RxJavaPlugins.setInitComputationSchedulerHandler( diff --git a/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestAnalyticsModule.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestAnalyticsModule.java new file mode 100644 index 00000000..9dfed055 --- /dev/null +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestAnalyticsModule.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.tests.internal.di; + +import java.util.Collections; +import java.util.Set; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoSet; +import network.minter.bipwallet.BuildConfig; +import network.minter.bipwallet.analytics.AnalyticsManager; +import network.minter.bipwallet.analytics.AnalyticsProvider; +import network.minter.bipwallet.analytics.providers.DummyProvider; +import network.minter.bipwallet.internal.di.WalletApp; +import network.minter.bipwallet.internal.di.annotations.AnalyticsProviders; + +/** + * minter-android-wallet. 2018 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +@Module +public class TestAnalyticsModule { + @Provides + @IntoSet + @AnalyticsProviders + @WalletApp + public AnalyticsProvider provideFabricAnalytics() { + return new DummyProvider(); + } + + @Provides + @IntoSet + @AnalyticsProviders + @WalletApp + public AnalyticsProvider provideAppMetricaAnalytics() { + return new DummyProvider(); + } + + @SuppressWarnings("ConstantConditions") + @Provides + @WalletApp + public AnalyticsManager provideAnalyticsManager(@AnalyticsProviders Set providerSet) { + if (BuildConfig.FLAVOR.equalsIgnoreCase("netTest") || BuildConfig.FLAVOR.equalsIgnoreCase("netMain")) { + return new AnalyticsManager(providerSet); + } + + return new AnalyticsManager(Collections.emptySet()); + } +} diff --git a/app/src/androidTest/java/network/minter/bipwallet/internal/di/TestWalletComponent.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletComponent.java similarity index 67% rename from app/src/androidTest/java/network/minter/bipwallet/internal/di/TestWalletComponent.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletComponent.java index 3a7f1412..9c94f4a5 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/internal/di/TestWalletComponent.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletComponent.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,7 +24,7 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.internal.di; +package network.minter.bipwallet.tests.internal.di; import android.content.Context; import android.content.SharedPreferences; @@ -40,21 +40,38 @@ import network.minter.bipwallet.advanced.models.UserAccount; import network.minter.bipwallet.advanced.repo.AccountStorage; import network.minter.bipwallet.advanced.repo.SecretStorage; +import network.minter.bipwallet.analytics.AnalyticsManager; import network.minter.bipwallet.apis.explorer.CachedExplorerTransactionRepository; -import network.minter.bipwallet.data.KVStorageQueueSaveTest; import network.minter.bipwallet.internal.Wallet; import network.minter.bipwallet.internal.auth.AuthSession; import network.minter.bipwallet.internal.data.CacheManager; import network.minter.bipwallet.internal.data.CachedRepository; +import network.minter.bipwallet.internal.di.CacheModule; +import network.minter.bipwallet.internal.di.HelpersModule; +import network.minter.bipwallet.internal.di.InjectorsModule; +import network.minter.bipwallet.internal.di.NotificationModule; +import network.minter.bipwallet.internal.di.RepoModule; +import network.minter.bipwallet.internal.di.WalletApp; +import network.minter.bipwallet.internal.di.WalletComponent; import network.minter.bipwallet.internal.helpers.DisplayHelper; import network.minter.bipwallet.internal.helpers.ImageHelper; import network.minter.bipwallet.internal.helpers.NetworkHelper; +import network.minter.bipwallet.internal.helpers.SoundManager; import network.minter.bipwallet.internal.storage.KVStorage; +import network.minter.bipwallet.sending.repo.RecipientAutocompleteStorage; +import network.minter.bipwallet.services.livebalance.notification.BalanceNotificationManager; +import network.minter.bipwallet.settings.repo.CachedMyProfileRepository; +import network.minter.bipwallet.tests.data.KVStorageQueueSaveTest; import network.minter.blockchain.repo.BlockChainAccountRepository; +import network.minter.blockchain.repo.BlockChainCoinRepository; +import network.minter.blockchain.repo.BlockChainTransactionRepository; import network.minter.core.internal.api.ApiService; import network.minter.explorer.models.HistoryTransaction; import network.minter.explorer.repo.ExplorerAddressRepository; +import network.minter.explorer.repo.ExplorerCoinsRepository; +import network.minter.explorer.repo.ExplorerSettingsRepository; import network.minter.explorer.repo.ExplorerTransactionRepository; +import network.minter.profile.models.User; import network.minter.profile.repo.ProfileAddressRepository; import network.minter.profile.repo.ProfileAuthRepository; import network.minter.profile.repo.ProfileInfoRepository; @@ -66,14 +83,16 @@ * @author Eduard Maximovich */ @Component(modules = { - WalletModule.class, + TestWalletModule.class, HelpersModule.class, RepoModule.class, InjectorsModule.class, CacheModule.class, + NotificationModule.class, + TestAnalyticsModule.class, }) @WalletApp -public interface TestWalletComponent { +public interface TestWalletComponent extends WalletComponent { void inject(Wallet app); void inject(KVStorageQueueSaveTest test); @@ -96,18 +115,32 @@ public interface TestWalletComponent { SharedPreferences prefs(); GsonBuilder gsonBuilder(); CacheManager cache(); + AnalyticsManager analytics(); + SoundManager sounds(); + + // notification + BalanceNotificationManager balanceNotifications(); // repositories + // local SecretStorage secretStorage(); AccountStorage accountStorage(); + RecipientAutocompleteStorage recipientStorage(); CachedRepository accountStorageCache(); - ExplorerTransactionRepository explorerTransactionsRepo(); CachedRepository, CachedExplorerTransactionRepository> explorerTransactionsRepoCache(); + CachedRepository profileCachedRepo(); + // profile ProfileAuthRepository authRepo(); ProfileInfoRepository infoRepo(); ProfileAddressRepository addressMyRepo(); - ExplorerAddressRepository addressExplorerRepo(); - ProfileRepository profileRepo(); + // explorer + ExplorerTransactionRepository explorerTransactionsRepo(); + ExplorerAddressRepository addressExplorerRepo(); + ExplorerCoinsRepository explorerCoinsRepo(); + ExplorerSettingsRepository explorerSettingsRepo(); + // blockchain BlockChainAccountRepository accountRepoBlockChain(); + BlockChainCoinRepository coinRepoBlockChain(); + BlockChainTransactionRepository txRepoBlockChain(); } diff --git a/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletModule.java b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletModule.java new file mode 100644 index 00000000..e69a01db --- /dev/null +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/internal/di/TestWalletModule.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.tests.internal.di; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; + +import com.fatboyindustrial.gsonjodatime.Converters; +import com.google.gson.GsonBuilder; + +import net.danlew.android.joda.JodaTimeAndroid; + +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; +import network.minter.bipwallet.BuildConfig; +import network.minter.bipwallet.R; +import network.minter.bipwallet.internal.auth.AuthSession; +import network.minter.bipwallet.internal.di.WalletApp; +import network.minter.bipwallet.internal.di.WalletModule; +import network.minter.bipwallet.internal.helpers.DateHelper; +import network.minter.bipwallet.internal.storage.KVStorage; +import network.minter.blockchain.MinterBlockChainApi; +import network.minter.core.internal.api.ApiService; +import network.minter.explorer.MinterExplorerApi; +import timber.log.Timber; + +/** + * minter-android-wallet. 2018 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +@Module +public class TestWalletModule { + private final static Map sMockStorage = new HashMap<>(); + private final Context mContext; + + public TestWalletModule(Context context) { + mContext = context; + + WalletModule.initCoreSdk(mContext); + MinterBlockChainApi.initialize(true); + MinterExplorerApi.initialize(true); + + Timber.uprootAll(); + Timber.plant(new Timber.DebugTree()); + + JodaTimeAndroid.init(context); + } + + @Provides + @WalletApp + @Named("uuid") + public String provideUUID(SharedPreferences preferences) { + final String appKey = mContext.getString(R.string.app_name) + "_uuid"; + + final String uuid = UUID.randomUUID().toString().toUpperCase(); + if (preferences.contains(appKey)) { + return preferences.getString(appKey, uuid); + } + + preferences.edit() + .putString(appKey, uuid) + .apply(); + + return uuid; + } + + @Provides + @WalletApp + public Context provideContext() { + return mContext; + } + + @Provides + @WalletApp + public boolean provideDebugMode() { + return true; + } + + @Provides + public Resources provideResources(Context context) { + return context.getResources(); + } + + @Provides + public ApiService.Builder provideApiService(AuthSession session, GsonBuilder gsonBuilder) { + ApiService.Builder builder = new ApiService.Builder("", gsonBuilder); + + builder + .setEmptyAuthTokenListener(session::logout) + .setDebug(true) + .setAuthHeaderName("Authorization") + .addHeader("User-Agent", "Minter Android " + String.valueOf(BuildConfig.VERSION_CODE)) + .addHeader("X-Client-Version", BuildConfig.VERSION_NAME) + .addHeader("X-Client-Build", String.valueOf(BuildConfig.VERSION_CODE)); + + return builder; + } + + @Provides + @WalletApp + public AuthSession provideAuthSession(KVStorage sessionStorage) { + return new AuthSession(sessionStorage); + } + + @Provides + public GsonBuilder provideGsonBuilder() { + GsonBuilder gsonBuilder = new GsonBuilder() + .setDateFormat(DateHelper.DATE_FORMAT_SIMPLE); + Converters.registerAll(gsonBuilder); + + return gsonBuilder; + } + + @Provides + public SharedPreferences providePreferences(Context context) { + return context + .getSharedPreferences(context.getString(R.string.user_local_settings_key), + Context.MODE_PRIVATE); + } + + + @WalletApp + @Provides + public KVStorage provideKeyValueStorage() { + return new KVStorage() { + @Override + public synchronized boolean put(String key, T value) { + sMockStorage.put(key, value); + return true; + } + + @Override + public synchronized T get(String key) { + return (T) sMockStorage.get(key); + } + + @Override + public synchronized Queue getQueue(String key) { + return get(key); + } + + @Override + public synchronized boolean putQueue(String key, Queue queue) { + return put(key, queue); + } + + @Override + public T get(String key, T defaultValue) { + if (sMockStorage.containsKey(key)) { + return (T) sMockStorage.get(key); + } + + return defaultValue; + } + + @Override + public synchronized boolean delete(String key) { + sMockStorage.remove(key); + return true; + } + + @Override + public synchronized boolean deleteAll() { + sMockStorage.clear(); + return true; + } + + @Override + public long count() { + return sMockStorage.size(); + } + + @Override + public boolean contains(String key) { + return sMockStorage.containsKey(key); + } + }; + } +} diff --git a/app/src/androidTest/java/network/minter/bipwallet/ui/AdvancedModeTest.java b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/AdvancedModeTest.java similarity index 84% rename from app/src/androidTest/java/network/minter/bipwallet/ui/AdvancedModeTest.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/ui/AdvancedModeTest.java index d98d7bf7..c91dd89d 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/ui/AdvancedModeTest.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/AdvancedModeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,25 +24,28 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.ui; +package network.minter.bipwallet.tests.ui; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.support.test.espresso.IdlingRegistry; import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.intent.Intents; import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; import network.minter.bipwallet.R; import network.minter.bipwallet.auth.ui.AuthActivity; import network.minter.bipwallet.home.ui.HomeActivity; +import network.minter.bipwallet.internal.system.testing.CallbackIdlingResource; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; @@ -59,20 +62,35 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static junit.framework.Assert.assertNotNull; -import static network.minter.bipwallet.ui.actions.BottomNavigationExAction.selectCurrentItem; +import static network.minter.bipwallet.tests.ui.actions.BottomNavigationExAction.selectCurrentItem; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.not; @LargeTest -@RunWith(AndroidJUnit4.class) +@RunWith(MockitoJUnitRunner.class) public class AdvancedModeTest { + private CallbackIdlingResource mAuthWaitIdlingRes = new CallbackIdlingResource(); + @Rule public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(AuthActivity.class); + public AdvancedModeTest() { + } + @Before public void setUp() { Intents.init(); + mActivityTestRule.getActivity().registerIdling(mAuthWaitIdlingRes); + IdlingRegistry.getInstance().register(mAuthWaitIdlingRes); + } + + @After + public void unregisterIdlingResource() { + Intents.release(); + if (mAuthWaitIdlingRes != null) { + IdlingRegistry.getInstance().unregister(mAuthWaitIdlingRes); + } } @Test @@ -80,9 +98,9 @@ public void advancedAuthTest() { // STEP 1 - creating new mnemonic // wait for fragments - waitSeconds(3); +// waitSeconds(5); ViewInteraction advanceModeButton = onView( - allOf(withId(R.id.action_advanced_mode), withText("Advanced mode"), isDisplayed())); + allOf(withId(R.id.action_advanced_mode), withText(R.string.btn_advanced_mode), isDisplayed())); // click on advanced mode advanceModeButton.perform(click()); @@ -91,7 +109,7 @@ public void advancedAuthTest() { // paste to seed invalid text seedInput.perform(replaceText("WTF"), closeSoftKeyboard()); - ViewInteraction advancedActivateButton = onView(allOf(withId(R.id.action_activate), withText("Activate"))); + ViewInteraction advancedActivateButton = onView(allOf(withId(R.id.action_activate), withText(R.string.btn_activate))); // try to login with invalid seed advancedActivateButton.perform(click()); @@ -99,11 +117,11 @@ public void advancedAuthTest() { // check invalid seed error seedInput.check(matches(hasErrorText("Phrase is not valid"))); - ViewInteraction generateMnemonicButton = onView(allOf(withId(R.id.action_generate), withText("Generate Address"))); + ViewInteraction generateMnemonicButton = onView(allOf(withId(R.id.action_generate), withText(R.string.btn_generate_address))); // click on generate new seed generateMnemonicButton.perform(click()); - ViewInteraction appCompatTextView = onView(allOf(withId(R.id.action_copy), withText("Copy"))); + ViewInteraction appCompatTextView = onView(allOf(withId(R.id.action_copy), withText(R.string.btn_copy))); appCompatTextView.perform(scrollTo(), click()); @@ -117,7 +135,7 @@ public void advancedAuthTest() { mnemonicTextView.check(matches(withText(copiedMnemonicText.toString()))); - ViewInteraction launchWalletButton = onView(allOf(withId(R.id.action), withText("Launch the wallet"))); + ViewInteraction launchWalletButton = onView(allOf(withId(R.id.action), withText(R.string.btn_launch_wallet))); launchWalletButton.check(matches(not(isEnabled()))); ViewInteraction switchEnableLaunching = onView(allOf(withId(R.id.switch_save_mnemonic))); diff --git a/app/src/androidTest/java/network/minter/bipwallet/tests/ui/BaseUiTest.java b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/BaseUiTest.java new file mode 100644 index 00000000..f5d0d06b --- /dev/null +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/BaseUiTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.tests.ui; + +import android.support.test.espresso.ViewInteraction; + +import java.math.BigDecimal; +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import network.minter.bipwallet.R; +import network.minter.bipwallet.tests.internal.TestWallet; +import network.minter.core.bip39.MnemonicResult; +import network.minter.core.bip39.NativeBip39; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdEQ; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdGT; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdGTE; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdHuman; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdLT; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdLTE; +import static network.minter.bipwallet.tests.ui.actions.BottomNavigationExAction.selectCurrentItem; + +/** + * minter-android-wallet. 2018 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +@SuppressWarnings("SameParameterValue") +public abstract class BaseUiTest { + + private ViewInteraction mBottomNavigation; + private BigDecimal mMntBalance; + private Disposable mDisposable; + private AtomicBoolean mUpdatedBalance = new AtomicBoolean(false); + + public void setUp() { + mMntBalance = new BigDecimal(0); + mDisposable = TestWallet.app().accountStorageCache() + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(res -> { + mMntBalance = res.getAccounts().get(0).getBalance(); + mUpdatedBalance.set(true); + }); + + } + + public void tearDown() { + mMntBalance = null; + mDisposable.dispose(); + mUpdatedBalance.set(false); + } + + protected void selectTab(int pos) { + if (mBottomNavigation == null) { + mBottomNavigation = onView(withId(R.id.navigation_bottom)); + } + + mBottomNavigation.perform(selectCurrentItem(pos)); + } + + protected MnemonicResult generateMnemonic() { + SecureRandom random = new SecureRandom(); + return NativeBip39.encodeBytes(random.generateSeed(16)); + } + + protected void waitForBalance(BigDecimal balance) { + waitForBalance(10, balance); + } + + protected void waitForBalance(double balance) { + waitForBalance(10, new BigDecimal(balance)); + } + + protected void waitForBalanceUpdate() { + while (!mUpdatedBalance.get()) { + try { + Thread.sleep(1000); + updateAccounts(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void updateAccounts() { + TestWallet.app().accountStorageCache().update(true); + } + + protected void waitForBalance(int seconds, BigDecimal balance) { + int times = 0; + + while (!bdEQ(mMntBalance, balance)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s, given %s", bdHuman(balance), bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void waitForBalanceGT(BigDecimal balance) { + waitForBalanceGT(10, balance); + } + + protected void waitForBalanceGT(double balance) { + waitForBalanceGT(10, new BigDecimal(balance)); + } + + protected void waitForBalanceGT(int seconds, BigDecimal balance) { + int times = 0; + while (!bdGT(mMntBalance, balance)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s > %s", bdHuman(balance), bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void waitForBalanceLTE(double balance) { + waitForBalanceLTE(10, new BigDecimal(balance)); + } + + protected void waitForBalanceLTE(BigDecimal balance) { + waitForBalanceLTE(10, balance); + } + + protected void waitForBalanceLTE(int seconds, BigDecimal balance) { + int times = 0; + while (!bdLTE(mMntBalance, balance)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s ≤ %s", bdHuman(balance), bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void waitForBalanceLT(double balance) { + waitForBalanceLT(10, new BigDecimal(balance)); + } + + protected void waitForBalanceLT(BigDecimal balance) { + waitForBalanceLT(10, balance); + } + + protected void waitForBalanceLT(int seconds, BigDecimal balance) { + int times = 0; + while (!bdLT(mMntBalance, balance)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s < %s", bdHuman(balance), bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void waitForBalanceGTE(double balance) { + waitForBalanceGTE(10, new BigDecimal(balance)); + } + + protected void waitForBalanceGTE(BigDecimal balance) { + waitForBalanceGTE(10, balance); + } + + protected void waitForBalanceGTE(int seconds, BigDecimal balance) { + int times = 0; + while (!bdGTE(mMntBalance, balance)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s ≥ %s", bdHuman(balance), bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + protected void waitForBalanceMoreThanZero() { + waitForBalanceMoreThanZero(10); + } + + protected void waitForBalanceMoreThanZero(int seconds) { + int times = 0; + + while (!bdGT(mMntBalance, BigDecimal.ZERO)) { + try { + Thread.sleep(1000); + updateAccounts(); + times++; + if (times >= seconds) { + throw new IllegalStateException(String.format("Balance is invalid: expected %s > 0", bdHuman(mMntBalance))); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + diff --git a/app/src/androidTest/java/network/minter/bipwallet/tests/ui/SendCoinsTabTest.java b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/SendCoinsTabTest.java new file mode 100644 index 00000000..e2afb216 --- /dev/null +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/SendCoinsTabTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.tests.ui; + +import android.support.test.espresso.IdlingRegistry; +import android.support.test.espresso.ViewInteraction; +import android.support.test.espresso.intent.Intents; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigDecimal; + +import network.minter.bipwallet.R; +import network.minter.bipwallet.advanced.models.SecretData; +import network.minter.bipwallet.advanced.repo.SecretStorage; +import network.minter.bipwallet.home.ui.HomeActivity; +import network.minter.bipwallet.internal.auth.AuthSession; +import network.minter.bipwallet.internal.system.testing.CallbackIdlingResource; +import network.minter.bipwallet.sending.ui.SendTabFragment; +import network.minter.bipwallet.settings.repo.MinterBotRepository; +import network.minter.bipwallet.tests.internal.TestWallet; +import network.minter.blockchain.models.operational.OperationType; +import network.minter.core.MinterSDK; +import network.minter.core.bip39.MnemonicResult; +import network.minter.core.crypto.MinterAddress; +import network.minter.profile.models.User; +import timber.log.Timber; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.replaceText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +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.withEffectiveVisibility; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withSubstring; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static network.minter.bipwallet.internal.helpers.MathHelper.bdHuman; +import static network.minter.bipwallet.tests.internal.MyMatchers.withInputLayoutError; +import static network.minter.bipwallet.tests.internal.MyMatchers.withInputLayoutHint; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.not; + +/** + * minter-android-wallet. 2018 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +@LargeTest +@RunWith(MockitoJUnitRunner.class) +public class SendCoinsTabTest extends BaseUiTest { + + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(HomeActivity.class, true, false); + private MinterAddress mAddress; + + @Before + public void setUp() { + super.setUp(); + final SecretStorage ss = TestWallet.app().secretStorage(); + ss.destroy(); + + MnemonicResult mnemonicResult = generateMnemonic(); + mAddress = ss.add(mnemonicResult); + + TestWallet.app().session().login( + AuthSession.AUTH_TOKEN_ADVANCED, + new User(AuthSession.AUTH_TOKEN_ADVANCED), + AuthSession.AuthType.Advanced + ); + + MinterBotRepository botRepo = new MinterBotRepository(); + MinterBotRepository.MinterBotResult botResult = botRepo.requestFreeCoins(mAddress).blockingFirst(); + if (!botResult.isOk()) { + throw new RuntimeException("Unable to get free coins, which is required for testing!"); + } + TestWallet.app().accountStorageCache().update(true); + mActivityTestRule.launchActivity(null); + + Timber.d("Secret storage by address %s: %s", mAddress, TestWallet.app().secretStorage().getSecret(mAddress) != null ? "true" : "false"); + Intents.init(); + } + + @After + public void tearDown() { + super.tearDown(); + Intents.release(); + mActivityTestRule.finishActivity(); + } + + @Test + public void testSendCoinsInsufficient() { + selectTab(1); + + // wait while balance be a 100 + waitForBalance(100); + // wait while balance isn't update + waitForBalanceUpdate(); + + + BigDecimal amountToSend = new BigDecimal(10000); + + // register idlings + SendTabFragment fragment = ((SendTabFragment) mActivityTestRule.getActivity().getActiveTabs().get(1)); + CallbackIdlingResource confirmIdling = new CallbackIdlingResource(); + CallbackIdlingResource completeIdling = new CallbackIdlingResource(); + + IdlingRegistry.getInstance().register(confirmIdling); + IdlingRegistry.getInstance().register(completeIdling); + + fragment.registerIdlings(confirmIdling, completeIdling); + + SecretData sd = SecretStorage.generateAddress(); + final String address = sd.getMinterAddress().toString(); + + // recipient + ViewInteraction recipient = onView(withId(R.id.input_recipient)); + recipient.perform(replaceText(address)); + recipient.check(matches(withText(address))); + + // amount + ViewInteraction amount = onView(withId(R.id.input_amount)); + amount.perform(replaceText(amountToSend.toString())); + + // submit + ViewInteraction submit = onView(allOf(withId(R.id.action), withText(R.string.btn_send))); + submit.check(matches((isEnabled()))); + + submit.perform(click()); + + + ViewInteraction confirmTitle = onView(allOf(isDescendantOfA(withId(android.R.id.content)), withId(R.id.title))); + confirmTitle.check(matches(withText("You're sending"))); + + onView(withId(R.id.dialog_amount)).check(matches(withText(String.format("%s %s", bdHuman(amountToSend), MinterSDK.DEFAULT_COIN)))); + onView(withId(R.id.tx_recipient_name)).check(matches(withText(address))); + + + ViewInteraction confirm = onView(allOf(withId(R.id.action_confirm), withText(R.string.btn_send))); + confirm.perform(click()); + + onView(allOf(isDescendantOfA(withId(android.R.id.content)), withId(R.id.title))) + .check(matches( + withText("Unable to send transaction")) + ); + + onView(allOf(isDescendantOfA(withId(android.R.id.content)), withId(R.id.dialog_text))) + .check(matches( + allOf( + withText(containsString("Insufficient")), + withText(containsString(mAddress.toString())) + ) + )); + + // close dialog to prevent leaking + onView(allOf(withText("Close"), withId(R.id.action_confirm))).perform(click()); + + + IdlingRegistry.getInstance().unregister(confirmIdling); + IdlingRegistry.getInstance().unregister(completeIdling); + + // as before + waitForBalance(100); + } + + @Test + public void testSendCoinsSuccessfully() { + selectTab(1); + + waitForBalance(100d); + waitForBalanceUpdate(); + + // register idlings + SendTabFragment fragment = ((SendTabFragment) mActivityTestRule.getActivity().getActiveTabs().get(1)); + CallbackIdlingResource confirmIdling = new CallbackIdlingResource(); + CallbackIdlingResource completeIdling = new CallbackIdlingResource(); + + IdlingRegistry.getInstance().register(confirmIdling); + IdlingRegistry.getInstance().register(completeIdling); + + fragment.registerIdlings(confirmIdling, completeIdling); + + SecretData sd = SecretStorage.generateAddress(); + final String address = sd.getMinterAddress().toString(); + + // recipient + ViewInteraction recipient = onView(withId(R.id.input_recipient)); + recipient.perform(replaceText(address)); + recipient.check(matches(withText(address))); + + // amount + ViewInteraction amount = onView(withId(R.id.input_amount)); + amount.perform(replaceText("1")); + + // submit + ViewInteraction submit = onView(allOf(withId(R.id.action), withText(R.string.btn_send))); + submit.check(matches((isEnabled()))); + + submit.perform(click()); + + + ViewInteraction confirmTitle = onView(allOf(isDescendantOfA(withId(android.R.id.content)), withId(R.id.title))); + confirmTitle.check(matches(withText("You're sending"))); + + onView(withId(R.id.dialog_amount)).check(matches(withText(String.format("%s %s", bdHuman(1d), MinterSDK.DEFAULT_COIN)))); + onView(withId(R.id.tx_recipient_name)).check(matches(withText(address))); + + + ViewInteraction confirm = onView(allOf(withId(R.id.action_confirm), withText(R.string.btn_send))); + confirm.perform(click()); + + onView(withId(R.id.tx_description)).check(matches(withText(R.string.tx_send_success_dialog_description))); + onView(withId(R.id.tx_recipient_name)).check(matches(withText(address))); + onView(withId(R.id.action_view_tx)) + .check(matches(isDisplayed())) + .check(matches(withText("VIEW TRANSACTION"))); + + // close dialog to prevent leaking + onView(allOf(withText("Close"), withId(R.id.action_close))).perform(click()); + + IdlingRegistry.getInstance().unregister(confirmIdling); + IdlingRegistry.getInstance().unregister(completeIdling); + + // 100 - 1 - 0.01 fee - 98.99 + waitForBalance(new BigDecimal(99).subtract(OperationType.SendCoin.getFee())); + + } + + @Test + public void testAccountSelector() { + selectTab(1); + + waitForBalance(100d); + final String balanceString = String.format("%s (%s)", MinterSDK.DEFAULT_COIN, bdHuman(new BigDecimal(100.d))); + + ViewInteraction submit = onView(allOf(withId(R.id.action), withText(R.string.btn_send))); + submit.check(matches(not(isEnabled()))); + + ViewInteraction accountInput = onView(withId(R.id.input_coin)); + accountInput.check(matches(withSubstring(MinterSDK.DEFAULT_COIN))); + accountInput.check(matches(withText(balanceString))); + + accountInput.perform(click()); + ViewInteraction accountDialogTitle = onView(withText(R.string.title_select_account)); + accountDialogTitle.check(matches(isDisplayed())); + ViewInteraction accountItem = onView(allOf(withId(R.id.item_title), withText(balanceString))); + accountItem.perform(click()); + + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + + + ViewInteraction recipient = onView(withId(R.id.input_recipient)); + + recipient.check(matches(not(withText("aaa")))); + recipient.check(matches(withInputLayoutHint(R.string.tx_send_recipient_hint))); + // set something strange + { + recipient.perform(replaceText("aaa")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set something illegal + { + recipient.perform(replaceText("whywhywhye")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set too short username, minimum 5 chars + { + recipient.perform(replaceText("@aabb")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set too long username, max 16 chars + { + recipient.perform(replaceText("@abcdefghijklmnopqrtstuwxyz")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set invalid email #1 + { + recipient.perform(replaceText("test@fly")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set invalid email #2 + { + recipient.perform(replaceText("test.fly.com")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set invalid email #2 (but this will parsed as invalid username) + { + recipient.perform(replaceText("@gmail.com")); + recipient.check(matches(withInputLayoutError("Incorrect recipient format"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + // set valid username, minimum 5 chars + { + recipient.perform(replaceText("@aabbc")); + recipient.check(matches(withInputLayoutError(null))); + // check submit still not enabled + submit.check(matches((isEnabled()))); + } + + // set valid username, minimum 5 chars + { + recipient.perform(replaceText("@12345")); + recipient.check(matches(withInputLayoutError(null))); + // check submit still not enabled + submit.check(matches((isEnabled()))); + } + + ViewInteraction amount = onView(withId(R.id.input_amount)); + amount.check(matches(withText(""))); + amount.check(matches(withInputLayoutHint(R.string.label_amount))); + + // set 0 + { + amount.perform(replaceText("0")); + amount.check(matches(withInputLayoutError("Amount must be greater than 0"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set . + { + amount.perform(replaceText(".")); + amount.check(matches(withInputLayoutError("Amount must be greater than 0"))); + // check submit still not enabled + submit.check(matches(not(isEnabled()))); + } + + // set .1 + { + amount.perform(replaceText(".1")); + amount.check(matches(withInputLayoutError(null))); + // check submit is enabled + submit.check(matches((isEnabled()))); + } + + // maximum is 10^-18 + { + amount.perform(replaceText("0.102030405060708090")); + amount.check(matches(withInputLayoutError(null))); + // check submit is enabled + } + + ViewInteraction useMaxButton = onView(withId(R.id.action_maximum)); + useMaxButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); + useMaxButton.perform(click()); + + amount.check(matches(withText("100"))); + + ViewInteraction feeLabel = onView(withId(R.id.fee_label)); + feeLabel.check(matches(withText(R.string.tx_send_fee_hint))); + ViewInteraction feeText = onView(withId(R.id.fee_value)); + feeText.check(matches(withText( + String.format("%s %s", bdHuman(OperationType.SendCoin.getFee()), MinterSDK.DEFAULT_COIN) + ))); + + } +} diff --git a/app/src/androidTest/java/network/minter/bipwallet/ui/actions/BottomNavigationExAction.java b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/actions/BottomNavigationExAction.java similarity index 97% rename from app/src/androidTest/java/network/minter/bipwallet/ui/actions/BottomNavigationExAction.java rename to app/src/androidTest/java/network/minter/bipwallet/tests/ui/actions/BottomNavigationExAction.java index 8dffd28a..5a3e3cd7 100644 --- a/app/src/androidTest/java/network/minter/bipwallet/ui/actions/BottomNavigationExAction.java +++ b/app/src/androidTest/java/network/minter/bipwallet/tests/ui/actions/BottomNavigationExAction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -24,7 +24,7 @@ * THE SOFTWARE. */ -package network.minter.bipwallet.ui.actions; +package network.minter.bipwallet.tests.ui.actions; import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; diff --git a/app/src/main/java/network/minter/bipwallet/auth/ui/AuthActivity.java b/app/src/main/java/network/minter/bipwallet/auth/ui/AuthActivity.java index 99d2c876..f8ef8417 100644 --- a/app/src/main/java/network/minter/bipwallet/auth/ui/AuthActivity.java +++ b/app/src/main/java/network/minter/bipwallet/auth/ui/AuthActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -27,6 +27,7 @@ package network.minter.bipwallet.auth.ui; import android.os.Bundle; +import android.support.annotation.VisibleForTesting; import android.support.transition.ChangeBounds; import android.support.transition.ChangeClipBounds; import android.support.transition.Slide; @@ -37,11 +38,14 @@ import network.minter.bipwallet.R; import network.minter.bipwallet.internal.BaseMvpInjectActivity; +import network.minter.bipwallet.internal.system.testing.CallbackIdlingResource; public class AuthActivity extends BaseMvpInjectActivity implements SplashFragment.AuthSwitchActivity { private SplashFragment mSplashFragment; private AuthFragment mAuthFragment; + private CallbackIdlingResource mAuthWait; + @SuppressWarnings("ConstantConditions") @Override public void showAuth(View sharedView) { TransitionSet sharedSet = new TransitionSet(); @@ -65,6 +69,15 @@ public void showAuth(View sharedView) { .addSharedElement(sharedView, ViewCompat.getTransitionName(sharedView)) .replace(R.id.container_auth, mAuthFragment) .commit(); + + if (mAuthWait != null) { + mAuthWait.setIdleState(true); + } + } + + @VisibleForTesting + public void registerIdling(CallbackIdlingResource authWaitIdlingRes) { + mAuthWait = authWaitIdlingRes; } @Override @@ -73,6 +86,9 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_auth); mSplashFragment = new SplashFragment(); mAuthFragment = new AuthFragment(); + if (mAuthWait != null) { + mAuthWait.setIdleState(false); + } getSupportFragmentManager() .beginTransaction() diff --git a/app/src/main/java/network/minter/bipwallet/exchange/ui/BaseCoinTabFragment.java b/app/src/main/java/network/minter/bipwallet/exchange/ui/BaseCoinTabFragment.java index 4f70bcf5..c94223c6 100644 --- a/app/src/main/java/network/minter/bipwallet/exchange/ui/BaseCoinTabFragment.java +++ b/app/src/main/java/network/minter/bipwallet/exchange/ui/BaseCoinTabFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -172,7 +172,7 @@ public void setTextChangedListener(InputGroup.OnTextChangedListener listener) { @Override public void startAccountSelector(List accounts, AccountSelectedAdapter.OnClickListener clickListener) { - new WalletAccountSelectorDialog.Builder(getActivity(), "Select account") + new WalletAccountSelectorDialog.Builder(getActivity(), R.string.title_select_account) .setItems(accounts) .setOnClickListener(clickListener) .create().show(); diff --git a/app/src/main/java/network/minter/bipwallet/home/ui/HomeActivity.java b/app/src/main/java/network/minter/bipwallet/home/ui/HomeActivity.java index 69091766..fc91d7be 100644 --- a/app/src/main/java/network/minter/bipwallet/home/ui/HomeActivity.java +++ b/app/src/main/java/network/minter/bipwallet/home/ui/HomeActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -34,6 +34,7 @@ import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; @@ -81,6 +82,11 @@ public class HomeActivity extends BaseMvpActivity implements HomeModule.HomeView private List mBackPressedListeners = new ArrayList<>(1); private boolean mIsLowRamDevice = false; + @VisibleForTesting + public final Map getActiveTabs() { + return mActiveTabs; + } + @Override public void setCurrentPage(int position) { runOnUiThread(() -> { diff --git a/app/src/main/java/network/minter/bipwallet/home/views/HomePresenter.java b/app/src/main/java/network/minter/bipwallet/home/views/HomePresenter.java index 870e8a71..2bb6fef0 100644 --- a/app/src/main/java/network/minter/bipwallet/home/views/HomePresenter.java +++ b/app/src/main/java/network/minter/bipwallet/home/views/HomePresenter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -57,7 +57,6 @@ @InjectViewState public class HomePresenter extends MvpBasePresenter { - private final HashMap mBottomIdPositionMap = new HashMap() {{ put(R.id.bottom_coins, 0); put(R.id.bottom_send, 1); diff --git a/app/src/main/java/network/minter/bipwallet/internal/Wallet.java b/app/src/main/java/network/minter/bipwallet/internal/Wallet.java index 69afee76..029c40d7 100644 --- a/app/src/main/java/network/minter/bipwallet/internal/Wallet.java +++ b/app/src/main/java/network/minter/bipwallet/internal/Wallet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -67,7 +67,8 @@ public class Wallet extends MultiDexApplication implements HasActivityInjector, public static final Locale LC_EN = Locale.US; @SuppressWarnings("ConstantConditions") public final static boolean ENABLE_CRASHLYTICS = BuildConfig.FLAVOR.equalsIgnoreCase("netTest") || BuildConfig.FLAVOR.equalsIgnoreCase("netMain"); - private static WalletComponent app; + protected static WalletComponent app; + protected static boolean sEnableInject = true; static { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { @@ -117,13 +118,15 @@ public void onCreate() { Locale.setDefault(LC_EN); - app = DaggerWalletComponent.builder() - .walletModule(new WalletModule(this, BuildConfig.DEBUG, ENABLE_CRASHLYTICS)) - .helpersModule(new HelpersModule()) - .repoModule(new RepoModule()) - .build(); + if (sEnableInject) { + app = DaggerWalletComponent.builder() + .walletModule(new WalletModule(this, BuildConfig.DEBUG, ENABLE_CRASHLYTICS)) + .helpersModule(new HelpersModule()) + .repoModule(new RepoModule()) + .build(); - app.inject(this); + app.inject(this); + } } @Override diff --git a/app/src/main/java/network/minter/bipwallet/internal/helpers/MathHelper.java b/app/src/main/java/network/minter/bipwallet/internal/helpers/MathHelper.java index 078b86d4..35e02151 100644 --- a/app/src/main/java/network/minter/bipwallet/internal/helpers/MathHelper.java +++ b/app/src/main/java/network/minter/bipwallet/internal/helpers/MathHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -136,6 +136,10 @@ public static boolean bdLTE(BigDecimal from, BigDecimal to) { return from.compareTo(to) <= 0; } + public static String bdHuman(double source) { + return bdHuman(new BigDecimal(source)); + } + public static String bdHuman(BigDecimal source) { BigDecimal num = firstNonNull(source, new BigDecimal(0)); @@ -155,6 +159,23 @@ public static String bdHuman(BigDecimal source) { return formatDecimalCurrency(out, 4, true); } + public static boolean bdEQ(double a, BigDecimal b) { + return bdEQ(new BigDecimal(a), b); + } + + public static boolean bdEQ(double a, double b) { + return bdEQ(new BigDecimal(a), new BigDecimal(b)); + } + + public static boolean bdEQ(BigDecimal a, double b) { + return bdEQ(a, new BigDecimal(b)); + } + + public static boolean bdEQ(BigDecimal a, BigDecimal b) { + if (a == null) return false; + return a.compareTo(b) == 0; + } + private static String formatDecimalCurrency(BigDecimal in, int fractions, boolean exactFractions) { DecimalFormat fmt = (DecimalFormat) NumberFormat.getInstance(Locale.US); DecimalFormatSymbols symbols = fmt.getDecimalFormatSymbols(); @@ -173,7 +194,13 @@ private static String formatDecimalCurrency(BigDecimal in, int fractions, boolea } public static boolean bdNull(BigDecimal source) { - return source.setScale(18).equals(new BigDecimal("0e-18")); + BigDecimal test; + if (source.scale() > 18) { + test = source.setScale(18, BigDecimal.ROUND_UP); + } else { + test = source; + } + return test.setScale(18).equals(new BigDecimal("0e-18")); } // BigInteger diff --git a/app/src/main/java/network/minter/bipwallet/internal/helpers/forms/validators/MinterUsernameValidator.java b/app/src/main/java/network/minter/bipwallet/internal/helpers/forms/validators/MinterUsernameValidator.java index e22db1f1..4f3a250a 100644 --- a/app/src/main/java/network/minter/bipwallet/internal/helpers/forms/validators/MinterUsernameValidator.java +++ b/app/src/main/java/network/minter/bipwallet/internal/helpers/forms/validators/MinterUsernameValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -33,7 +33,7 @@ * @author Eduard Maximovich */ public class MinterUsernameValidator extends RegexValidator { - private final static Pattern PATTERN = Pattern.compile("^@[a-zA-Z0-9]{5,16}$"); + public final static Pattern PATTERN = Pattern.compile("^@[a-zA-Z0-9]{5,16}$"); public MinterUsernameValidator() { super(PATTERN.pattern()); diff --git a/app/src/main/java/network/minter/bipwallet/internal/storage/KVStorage.java b/app/src/main/java/network/minter/bipwallet/internal/storage/KVStorage.java index 34ca0b9d..5bfaf804 100644 --- a/app/src/main/java/network/minter/bipwallet/internal/storage/KVStorage.java +++ b/app/src/main/java/network/minter/bipwallet/internal/storage/KVStorage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -38,7 +38,7 @@ * * @author Eduard Maximovich */ -public final class KVStorage implements Storage { +public class KVStorage implements Storage { @Override public boolean put(String key, T value) { return Hawk.put(key, value); diff --git a/app/src/main/java/network/minter/bipwallet/internal/system/StringUtil.java b/app/src/main/java/network/minter/bipwallet/internal/system/StringUtil.java new file mode 100644 index 00000000..104009c0 --- /dev/null +++ b/app/src/main/java/network/minter/bipwallet/internal/system/StringUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.internal.system; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.view.View; + +/** + * minter-android-wallet. 2019 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +public final class StringUtil { + + public static boolean safeCompare(View context, CharSequence a, @StringRes int b) { + return safeCompare(a, context.getResources().getString(b)); + } + + public static boolean safeCompare(View context, @StringRes int a, CharSequence b) { + return safeCompare(context.getResources().getString(a), b); + } + + public static boolean safeCompare(@NonNull Context context, @StringRes int a, CharSequence b) { + return safeCompare(context.getResources().getString(a), b); + } + + public static boolean safeCompare(@Nullable CharSequence a, @Nullable CharSequence b) { + if (a == null && b == null) { + return true; + } else if (a != null && b == null) { + return false; + } else if (a == null && b != null) { + return false; + } else if (a.length() != b.length()) { + return false; + } + + return a.toString().equals(b.toString()); + } +} diff --git a/app/src/main/java/network/minter/bipwallet/internal/system/testing/CallbackIdlingResource.java b/app/src/main/java/network/minter/bipwallet/internal/system/testing/CallbackIdlingResource.java new file mode 100644 index 00000000..ce242f38 --- /dev/null +++ b/app/src/main/java/network/minter/bipwallet/internal/system/testing/CallbackIdlingResource.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) by MinterTeam. 2019 + * @link Org Github + * @link Maintainer Github + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package network.minter.bipwallet.internal.system.testing; + +import android.support.annotation.Nullable; +import android.support.test.espresso.IdlingResource; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * minter-android-wallet. 2019 + * @author Eduard Maximovich [edward.vstock@gmail.com] + */ +public class CallbackIdlingResource implements IdlingResource { + + private volatile static AtomicInteger sInc = new AtomicInteger(-1); + @Nullable private volatile ResourceCallback mCallback; + // Idleness is controlled with this boolean. + private AtomicBoolean mIsIdleNow = new AtomicBoolean(true); + + public CallbackIdlingResource() { + sInc.incrementAndGet(); + } + + @Override + public String getName() { + return this.getClass().getName() + "_" + String.valueOf(sInc.get()); + } + + @Override + public boolean isIdleNow() { + return mIsIdleNow.get(); + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + mCallback = callback; + } + + /** + * Sets the new idle state, if isIdleNow is true, it pings the {@link ResourceCallback}. + * @param isIdleNow false if there are pending operations, true if idle. + */ + @SuppressWarnings("ConstantConditions") + public void setIdleState(boolean isIdleNow) { + mIsIdleNow.set(isIdleNow); + if (isIdleNow && mCallback != null) { + mCallback.onTransitionToIdle(); + } + } +} diff --git a/app/src/main/java/network/minter/bipwallet/sending/SendTabModule.java b/app/src/main/java/network/minter/bipwallet/sending/SendTabModule.java index aa08a705..aa7b7ee8 100644 --- a/app/src/main/java/network/minter/bipwallet/sending/SendTabModule.java +++ b/app/src/main/java/network/minter/bipwallet/sending/SendTabModule.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -26,6 +26,7 @@ package network.minter.bipwallet.sending; +import android.support.annotation.VisibleForTesting; import android.view.View; import com.arellomobile.mvp.MvpView; @@ -77,6 +78,11 @@ public interface SendView extends MvpView, ErrorViewWithRetry { void setAmount(CharSequence amount); void setFee(CharSequence fee); void setRecipientsAutocomplete(List items, RecipientListAdapter.OnItemClickListener listener); + + @VisibleForTesting + void setConfirmIdlingState(boolean b); + @VisibleForTesting + void setCompleteIdlingState(boolean b); } public static class TxData { diff --git a/app/src/main/java/network/minter/bipwallet/sending/account/WalletAccountSelectorDialog.java b/app/src/main/java/network/minter/bipwallet/sending/account/WalletAccountSelectorDialog.java index fb30909a..4e812d96 100644 --- a/app/src/main/java/network/minter/bipwallet/sending/account/WalletAccountSelectorDialog.java +++ b/app/src/main/java/network/minter/bipwallet/sending/account/WalletAccountSelectorDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -29,6 +29,7 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -92,6 +93,10 @@ public Builder(Context context, CharSequence title) { super(context, title); } + public Builder(Context context, @StringRes int titleRes) { + super(context, titleRes); + } + @Override public WalletAccountSelectorDialog create() { return new WalletAccountSelectorDialog(getContext(), this); diff --git a/app/src/main/java/network/minter/bipwallet/sending/ui/SendTabFragment.java b/app/src/main/java/network/minter/bipwallet/sending/ui/SendTabFragment.java index 8614268d..f263a52c 100644 --- a/app/src/main/java/network/minter/bipwallet/sending/ui/SendTabFragment.java +++ b/app/src/main/java/network/minter/bipwallet/sending/ui/SendTabFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -34,6 +34,7 @@ import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.design.widget.TextInputLayout; import android.support.v7.widget.AppCompatEditText; import android.util.Patterns; @@ -65,13 +66,16 @@ import network.minter.bipwallet.internal.helpers.ViewHelper; import network.minter.bipwallet.internal.helpers.forms.DecimalInputFilter; import network.minter.bipwallet.internal.helpers.forms.InputGroup; +import network.minter.bipwallet.internal.helpers.forms.validators.MinterUsernameValidator; import network.minter.bipwallet.internal.helpers.forms.validators.RegexValidator; +import network.minter.bipwallet.internal.system.testing.CallbackIdlingResource; import network.minter.bipwallet.sending.SendTabModule; import network.minter.bipwallet.sending.account.AccountSelectedAdapter; import network.minter.bipwallet.sending.account.WalletAccountSelectorDialog; import network.minter.bipwallet.sending.adapters.RecipientListAdapter; import network.minter.bipwallet.sending.models.RecipientItem; import network.minter.bipwallet.sending.views.SendTabPresenter; +import network.minter.core.crypto.MinterAddress; import network.minter.explorer.MinterExplorerApi; import permissions.dispatcher.NeedsPermission; import permissions.dispatcher.OnPermissionDenied; @@ -101,6 +105,32 @@ public class SendTabFragment extends HomeTabFragment implements SendTabModule.Se private InputGroup mInputGroup; private WalletDialog mCurrentDialog = null; + @VisibleForTesting + private CallbackIdlingResource mConfirmDialogIdling, mCompleteDialogIdling; + + @VisibleForTesting + public final void registerIdlings(CallbackIdlingResource confirmIdling, CallbackIdlingResource completeIdling) { + mConfirmDialogIdling = confirmIdling; + mCompleteDialogIdling = completeIdling; + } + + @VisibleForTesting + @Override + public void setConfirmIdlingState(boolean b) { + if (mConfirmDialogIdling != null) { + mConfirmDialogIdling.setIdleState(b); + } + + } + + @VisibleForTesting + @Override + public void setCompleteIdlingState(boolean b) { + if (mCompleteDialogIdling != null) { + mCompleteDialogIdling.setIdleState(b); + } + } + @Override public void onAttach(Context context) { HomeModule.getComponent().inject(this); @@ -129,10 +159,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mInputGroup.addInput(amountInput); mInputGroup.addValidator(amountInput, new RegexValidator("^(\\d*)(\\.)?(\\d{1,18})$", "Invalid number", false)); /* ideal case */ + mInputGroup.addValidator(recipientInput, new RegexValidator( // address or username with @ at begin or email - String.format("(((0|M|m)x)?([a-fA-F0-9]{40}))|(@[a-zA-Z0-9_]{5,32})|%s", Patterns.EMAIL_ADDRESS), + String.format("%s|%s|%s", MinterAddress.ADDRESS_PATTERN, MinterUsernameValidator.PATTERN, Patterns.EMAIL_ADDRESS), "Incorrect recipient format" )); diff --git a/app/src/main/java/network/minter/bipwallet/sending/views/SendTabPresenter.java b/app/src/main/java/network/minter/bipwallet/sending/views/SendTabPresenter.java index d7951178..83032cc0 100644 --- a/app/src/main/java/network/minter/bipwallet/sending/views/SendTabPresenter.java +++ b/app/src/main/java/network/minter/bipwallet/sending/views/SendTabPresenter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -267,7 +267,7 @@ private void onSubmit(View view) { } private SearchByType getSearchByType(String input) { - if (input.substring(0, 2).equals(MinterSDK.PREFIX_ADDRESS) && input.length() == 42) { + if (MinterAddress.testString(input)) { // searching data by address return SearchByType.Address; } else if (input.substring(0, 1).equals("@")) { @@ -330,8 +330,10 @@ private void resolveUserInfo(final String searchBy, final boolean failOnNotFound } private void startSendDialog() { + getViewState().setConfirmIdlingState(false); getViewState().startDialog(ctx -> { try { + getViewState().setConfirmIdlingState(true); getAnalytics().send(AppEvent.SendCoinPopupScreen); final WalletTxSendStartDialog dialog = new WalletTxSendStartDialog.Builder(ctx, R.string.tx_send_overall_title) .setAmount(mAmount) @@ -404,6 +406,8 @@ private Optional findAccountByCoin(String coin) { } private void onStartExecuteTransaction(boolean express) { + getViewState().setCompleteIdlingState(false); + getViewState().startDialog(ctx -> { final WalletProgressDialog dialog = new WalletProgressDialog.Builder(ctx, R.string.please_wait) .setText(R.string.tx_send_in_progress) @@ -518,10 +522,11 @@ public SendInitData apply(BCExplorerResult txCommiss return Observable.just(errorRes); } - Timber.tag("TX Send").d("Send data: gasCoin=%s, coin=%s, to=%s, amount=%s", + Timber.tag("TX Send").d("Send data: gasCoin=%s, coin=%s, to=%s, from=%s, amount=%s", mFromAccount.getCoin(), mFromAccount.getCoin(), mToAddress, + mFromAccount.getAddress().toString(), amountToSend ); // creating tx @@ -541,13 +546,20 @@ public SendInitData apply(BCExplorerResult txCommiss .onErrorResumeNext(convertToBcExpErrorResult()) ); - }).subscribe(this::onSuccessExecuteTransaction, this::onFailedExecuteTransaction); + }) + .doFinally(this::onExecuteComplete) + .subscribe(this::onSuccessExecuteTransaction, this::onFailedExecuteTransaction); unsubscribeOnDestroy(d); return dialog; }); } + private void onExecuteComplete() { + getViewState().setConfirmIdlingState(true); + getViewState().setCompleteIdlingState(true); + } + private void onFailedExecuteTransaction(final Throwable throwable) { Timber.w(throwable, "Uncaught tx error"); getViewState().startDialog(ctx -> new WalletConfirmDialog.Builder(ctx, "Unable to send transaction") diff --git a/app/src/main/java/network/minter/bipwallet/settings/repo/MinterBotRepository.java b/app/src/main/java/network/minter/bipwallet/settings/repo/MinterBotRepository.java index 8d04e62e..45cf86d4 100644 --- a/app/src/main/java/network/minter/bipwallet/settings/repo/MinterBotRepository.java +++ b/app/src/main/java/network/minter/bipwallet/settings/repo/MinterBotRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) by MinterTeam. 2018 + * Copyright (C) by MinterTeam. 2019 * @link Org Github * @link Maintainer Github * @@ -60,7 +60,7 @@ */ public class MinterBotRepository extends DataRepository implements DataRepository.Configurator { public MinterBotRepository() { - super(new ApiService.Builder("https://minter-bot-wallet.dl-dev.ru/api/") + super(new ApiService.Builder("https://testnet.tgbot.minter.network/api/") .setDebugRequestLevel(HttpLoggingInterceptor.Level.BODY) .setDebug(true) .setRetrofitClientConfig(b -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76214d28..b30f0c7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@