diff --git a/receipt/build.gradle b/receipt/build.gradle
index dac1fd822..6aa8d9d42 100644
--- a/receipt/build.gradle
+++ b/receipt/build.gradle
@@ -11,7 +11,13 @@ dependencies {
implementation "androidx.paging:paging-runtime:$pagingRuntimeVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
+
androidTestImplementation "androidx.test:rules:$testRulesVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
+ androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockServerVersion"
+ androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:$leakcanaryVersion"
+ androidTestImplementation "com.squareup.leakcanary:leakcanary-support-fragment:$leakcanaryVersion"
}
def aarFile = file("$buildDir/outputs/aar/receipt-$version" + ".aar")
diff --git a/receipt/src/androidTest/AndroidManifest.xml b/receipt/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..8495adb45
--- /dev/null
+++ b/receipt/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java b/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java
new file mode 100644
index 000000000..dfc559d2e
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java
@@ -0,0 +1,31 @@
+package com.hyperwallet.android;
+
+
+import android.app.Application;
+
+import com.squareup.leakcanary.InstrumentationLeakDetector;
+import com.squareup.leakcanary.LeakCanary;
+
+public class HyperwalletInstrumentedTestApplication extends Application {
+
+ @Override
+ public void onCreate() {
+
+ super.onCreate();
+ if (LeakCanary.isInAnalyzerProcess(this)) {
+ // This process is dedicated to LeakCanary for heap analysis.
+ // You should not init your app in this process.
+ return;
+ }
+ installLeakCanary();
+ }
+
+
+ protected void installLeakCanary() {
+
+ InstrumentationLeakDetector.instrumentationRefWatcher(this)
+ .buildAndInstall();
+
+ }
+
+}
\ No newline at end of file
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java b/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java
new file mode 100644
index 000000000..5eb430354
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java
@@ -0,0 +1,265 @@
+package com.hyperwallet.android.receipt;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withParent;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.instanceOf;
+
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+import static com.hyperwallet.android.util.EspressoUtils.atPosition;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.rule.ActivityTestRule;
+
+import com.hyperwallet.android.Hyperwallet;
+import com.hyperwallet.android.receipt.repository.ReceiptRepositoryFactory;
+import com.hyperwallet.android.receipt.view.ListReceiptActivity;
+import com.hyperwallet.android.rule.HyperwalletExternalResourceManager;
+import com.hyperwallet.android.rule.HyperwalletMockWebServer;
+import com.hyperwallet.android.util.RecyclerViewCountAssertion;
+import com.hyperwallet.android.util.TestAuthenticationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+public class ListReceiptsTest {
+
+ @ClassRule
+ public static HyperwalletExternalResourceManager sResourceManager = new HyperwalletExternalResourceManager();
+ @Rule
+ public HyperwalletMockWebServer mMockWebServer = new HyperwalletMockWebServer(8080);
+ @Rule
+ public ActivityTestRule mActivityTestRule =
+ new ActivityTestRule<>(ListReceiptActivity.class, true, false);
+
+ @Before
+ public void setup() {
+ Hyperwallet.getInstance(new TestAuthenticationProvider());
+
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("authentication_token_response.json")).mock();
+
+ setLocale(Locale.US);
+ }
+
+ @After
+ public void cleanup() {
+ ReceiptRepositoryFactory.clearInstance();
+ }
+
+ @Test
+ public void testListReceipts_userHasMultipleTransactions() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("receipt_list_response.json")).mock();
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ // run test
+ mActivityTestRule.launchActivity(null);
+
+ // assert
+ onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar))))
+ .check(matches(withText(R.string.title_activity_receipt_list)));
+ onView(withId(R.id.list_receipts)).check(matches(isDisplayed()));
+
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("June 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("Payment")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("+ 20.00")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("June 07, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("USD")))));
+
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(1,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(1, hasDescendant(withText("Payment")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(1, hasDescendant(withText("+ 25.00")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(1, hasDescendant(withText("June 02, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(1, hasDescendant(withText("CAD")))));
+
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(2,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(2, hasDescendant(withText("Card Activation Fee")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(2, hasDescendant(withText("- 1.95")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(2, hasDescendant(withText("June 01, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(2, hasDescendant(withText("USD")))));
+
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(3, hasDescendant(withText("December 2018")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(3,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(3, hasDescendant(withText("Card Load")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(3, hasDescendant(withText("- 18.05")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(3, hasDescendant(withText("December 01, 2018")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(3, hasDescendant(withText("USD")))));
+
+ onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(4));
+ }
+
+ @Test
+ public void testListReceipts_displayCreditTransaction() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("receipt_credit_response.json")).mock();
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ // run test
+ mActivityTestRule.launchActivity(null);
+
+ // assert
+ onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar))))
+ .check(matches(withText(R.string.title_activity_receipt_list)));
+ onView(withId(R.id.list_receipts)).check(matches(isDisplayed()));
+
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("June 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText(R.string.payment)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("+ 25.00")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("June 02, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("CAD")))));
+
+ onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1));
+ }
+
+ @Test
+ public void testListReceipts_displayDebitTransaction() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("receipt_debit_response.json")).mock();
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ // run test
+ mActivityTestRule.launchActivity(null);
+
+ // assert
+ onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar))))
+ .check(matches(withText(R.string.title_activity_receipt_list)));
+ onView(withId(R.id.list_receipts)).check(matches(isDisplayed()));
+
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("May 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText(R.string.transfer_to_prepaid_card)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("- 18.05")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("May 02, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("USD")))));
+
+ onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1));
+ }
+
+ @Test
+ public void testListReceipts_displayUnknownTransactionType() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("receipt_unknown_type_response.json")).mock();
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ // run test
+ mActivityTestRule.launchActivity(null);
+
+ // assert
+ onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar))))
+ .check(matches(withText(R.string.title_activity_receipt_list)));
+ onView(withId(R.id.list_receipts)).check(matches(isDisplayed()));
+
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("June 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0,
+ hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText(R.string.unknown_type)))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("+ 25.00")))));
+ onView(withId(R.id.list_receipts)).check(
+ matches(atPosition(0, hasDescendant(withText("June 02, 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("CAD")))));
+
+ onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1));
+ }
+
+ @Test
+ public void testListReceipt_userHasNoTransactions() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ // run test
+ mActivityTestRule.launchActivity(null);
+
+ onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
+ onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar))))
+ .check(matches(withText(R.string.title_activity_receipt_list)));
+ //todo: check empty view when it will be ready
+ }
+
+ @Test
+ public void testListReceipts_checkDateTextOnLocaleChange() {
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager
+ .getResourceContent("receipt_debit_response.json")).mock();
+ mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock();
+
+ setLocale(Locale.ITALY);
+ // run test
+ mActivityTestRule.launchActivity(null);
+ // assert
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("maggio 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("maggio 02, 2019")))));
+ mActivityTestRule.finishActivity();
+ setLocale(Locale.US);
+ mActivityTestRule.launchActivity(null);
+ onView(withId(R.id.list_receipts))
+ .check(matches(atPosition(0, hasDescendant(withText("May 2019")))));
+ onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("May 02, 2019")))));
+ }
+
+ private void setLocale(Locale locale) {
+ Context context = ApplicationProvider.getApplicationContext();
+ Locale.setDefault(locale);
+ Resources resources = context.getResources();
+ Configuration configuration = resources.getConfiguration();
+ if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ configuration.setLocale(locale);
+ } else {
+ configuration.locale = locale;
+ }
+ resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+ }
+}
diff --git a/receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java
similarity index 98%
rename from receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java
rename to receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java
index 0c5f6a743..e7e546063 100644
--- a/receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java
@@ -1,4 +1,4 @@
-package com.hyperwallet.android.receipt.rule;
+package com.hyperwallet.android.rule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
@@ -35,6 +35,7 @@ public String getResourceContent(final String resourceName) {
}
private String getContent(final String resourceName) {
+
URL resource = classLoader.getResource(resourceName);
InputStream inputStream = null;
Writer writer = new StringWriter();
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java
new file mode 100644
index 000000000..9f42f6128
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java
@@ -0,0 +1,115 @@
+package com.hyperwallet.android.rule;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+public final class HyperwalletMockWebServer extends TestWatcher {
+
+ private MockWebServer mServer;
+ private int port;
+
+ public HyperwalletMockWebServer(int port) {
+ this.port = port;
+ }
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ mServer = new MockWebServer();
+ try {
+ mServer.start(port);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to start mock server", e);
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ try {
+ mServer.shutdown();
+ mServer.close();
+ } catch (IOException e) {
+ throw new IllegalStateException("Un error occurred when shutting down mock server", e);
+ }
+ }
+
+ public HyperwalletMockResponse mockResponse() {
+ return new Builder(mServer).build();
+ }
+
+ public MockWebServer getServer() {
+ return mServer;
+ }
+
+ public static class HyperwalletMockResponse {
+
+ private String path;
+ private String body;
+ private int httpResponseCode;
+ private Builder builder;
+
+ HyperwalletMockResponse(Builder builder) {
+ this.path = builder.path;
+ this.httpResponseCode = builder.responseCode;
+ this.body = builder.body;
+ this.builder = builder;
+ }
+
+ public HyperwalletMockResponse withHttpResponseCode(final int code) {
+ builder.responseCode(code);
+ return builder.build();
+ }
+
+ public HyperwalletMockResponse withBody(final String body) {
+ builder.body(body);
+ return builder.build();
+ }
+
+ public void mock() {
+ mockRequest();
+ }
+
+ private String mockRequest() {
+ builder.server.enqueue(new MockResponse().setResponseCode(httpResponseCode).setBody(body));
+ return builder.server.url(path).toString();
+ }
+
+ }
+
+ private static class Builder {
+
+ private String path;
+ private String body;
+ private int responseCode;
+ private MockWebServer server;
+
+
+ Builder(final MockWebServer server) {
+ this.path = "";
+ this.responseCode = HttpURLConnection.HTTP_OK;
+ this.body = "";
+ this.server = server;
+ }
+
+ Builder responseCode(final int code) {
+ this.responseCode = code;
+ return this;
+ }
+
+ Builder body(final String body) {
+ this.body = body;
+ return this;
+ }
+
+ HyperwalletMockResponse build() {
+ return new HyperwalletMockResponse(this);
+ }
+ }
+}
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java
new file mode 100644
index 000000000..92a711f96
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java
@@ -0,0 +1,187 @@
+package com.hyperwallet.android.util;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.espresso.matcher.BoundedMatcher;
+
+import com.google.android.material.textfield.TextInputLayout;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+import java.util.Objects;
+
+public class EspressoUtils {
+
+ public static Matcher withHint(final String expectedHint) {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof TextInputLayout)) {
+ return false;
+ }
+
+ String hint = Objects.toString(((TextInputLayout) view).getHint());
+ return expectedHint.equals(hint);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(expectedHint);
+ }
+ };
+ }
+
+ public static Matcher hasErrorText(final String expectedErrorMessage) {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof TextInputLayout)) {
+ return false;
+ }
+
+ String errorMessage = Objects.toString(((TextInputLayout) view).getError());
+ return expectedErrorMessage.equals(errorMessage);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(expectedErrorMessage);
+ }
+ };
+ }
+
+ public static Matcher hasErrorText(final int resourceId) {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof TextInputLayout)) {
+ return false;
+ }
+ String expectedErrorMessage = view.getResources().getString(resourceId);
+ String errorMessage = Objects.toString(((TextInputLayout) view).getError());
+
+ return expectedErrorMessage.equals(errorMessage);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ }
+ };
+ }
+
+ public static Matcher withDrawable(final int resourceId) {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof ImageView)) {
+ return false;
+ }
+
+ Drawable drawable = ((ImageView) view).getDrawable();
+ if (drawable == null) {
+ return false;
+ }
+ Drawable expectedDrawable = view.getContext().getResources().getDrawable(resourceId);
+
+ Bitmap bitmap = getBitmap(drawable);
+ Bitmap expectedBitmap = getBitmap(expectedDrawable);
+
+ return bitmap.sameAs(expectedBitmap);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ }
+ };
+ }
+
+ public static Matcher atPosition(final int position, @NonNull final Matcher matcher) {
+ return new BoundedMatcher(RecyclerView.class) {
+
+ @Override
+ protected boolean matchesSafely(final RecyclerView view) {
+ RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(position);
+
+ if (viewHolder == null) {
+ return false;
+ }
+
+ return matcher.matches(viewHolder.itemView);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("has item at position " + position + ": ");
+ matcher.describeTo(description);
+ }
+ };
+ }
+
+ public static ViewAction nestedScrollTo() {
+ return ViewActions.actionWithAssertions(new NestedScrollToAction());
+ }
+
+ private static Bitmap getBitmap(Drawable drawable) {
+ Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+ public static Matcher hasNoErrorText() {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof TextInputLayout)) {
+ return false;
+ }
+ return ((TextInputLayout) view).getError() == null;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("has no error text: ");
+ }
+ };
+ }
+
+ public static Matcher hasEmptyText() {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof EditText)) {
+ return false;
+ }
+ String text = ((EditText) view).getText().toString();
+
+ return text.isEmpty();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ }
+ };
+ }
+}
+
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java
new file mode 100644
index 000000000..392867387
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java
@@ -0,0 +1,41 @@
+package com.hyperwallet.android.util;
+
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+
+import android.view.View;
+
+import androidx.core.widget.NestedScrollView;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.ScrollToAction;
+import androidx.test.espresso.matcher.ViewMatchers;
+
+import org.hamcrest.Matcher;
+
+public class NestedScrollToAction implements ViewAction {
+ private static final String TAG = ScrollToAction.class.getSimpleName();
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Matcher getConstraints() {
+ return allOf(
+ withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
+ isDescendantOfA(
+ anyOf(isAssignableFrom(NestedScrollView.class))));
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ new ScrollToAction().perform(uiController, view);
+ }
+
+ @Override
+ public String getDescription() {
+ return "scroll to";
+ }
+}
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java
new file mode 100644
index 000000000..bb3aecaff
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java
@@ -0,0 +1,30 @@
+package com.hyperwallet.android.util;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.espresso.ViewAssertion;
+
+public class RecyclerViewCountAssertion implements ViewAssertion {
+ private final int mCount;
+
+ public RecyclerViewCountAssertion(int count) {
+ this.mCount = count;
+ }
+
+ @Override
+ public void check(View view, NoMatchingViewException noViewFoundException) {
+ if (noViewFoundException != null) {
+ throw noViewFoundException;
+ }
+
+ RecyclerView recyclerView = (RecyclerView) view;
+ RecyclerView.Adapter adapter = recyclerView.getAdapter();
+
+ assertThat(adapter.getItemCount(), is(mCount));
+ }
+}
diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java
new file mode 100644
index 000000000..686ccbf30
--- /dev/null
+++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java
@@ -0,0 +1,51 @@
+package com.hyperwallet.android.util;
+
+import com.hyperwallet.android.HyperwalletAuthenticationTokenListener;
+import com.hyperwallet.android.HyperwalletAuthenticationTokenProvider;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.UUID;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class TestAuthenticationProvider implements HyperwalletAuthenticationTokenProvider {
+
+ public static final MediaType JSON
+ = MediaType.get("application/json; charset=utf-8");
+ private static final String mBaseUrl = "http://localhost:8080/rest/v3/users/{0}/authentication-token";
+ private static final String mUserToken = "user_token";
+
+ @Override
+ public void retrieveAuthenticationToken(final HyperwalletAuthenticationTokenListener authenticationTokenListener) {
+
+ OkHttpClient client = new OkHttpClient();
+
+ String payload = "{}";
+ String baseUrl = MessageFormat.format(mBaseUrl, mUserToken);
+
+ RequestBody body = RequestBody.create(JSON, payload);
+ Request request = new Request.Builder()
+ .url(baseUrl)
+ .post(body)
+ .build();
+
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ authenticationTokenListener.onFailure(UUID.randomUUID(), e.getMessage());
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ authenticationTokenListener.onSuccess(response.body().string());
+ }
+ });
+ }
+}
diff --git a/receipt/src/main/AndroidManifest.xml b/receipt/src/main/AndroidManifest.xml
index 251bb4f88..3039f6d18 100644
--- a/receipt/src/main/AndroidManifest.xml
+++ b/receipt/src/main/AndroidManifest.xml
@@ -2,7 +2,9 @@
-
+
+
+
diff --git a/receipt/src/main/res/xml/network_security_config.xml b/receipt/src/main/res/xml/network_security_config.xml
new file mode 100644
index 000000000..5e4ba9c97
--- /dev/null
+++ b/receipt/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+
+
+
+ localhost
+
+
\ No newline at end of file
diff --git a/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java b/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java
index efe3c4627..d09854710 100644
--- a/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java
+++ b/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java
@@ -27,7 +27,7 @@
import com.hyperwallet.android.model.paging.HyperwalletPageList;
import com.hyperwallet.android.model.receipt.Receipt;
import com.hyperwallet.android.model.receipt.ReceiptQueryParam;
-import com.hyperwallet.android.receipt.rule.HyperwalletExternalResourceManager;
+import com.hyperwallet.android.rule.HyperwalletExternalResourceManager;
import org.hamcrest.Matchers;
import org.json.JSONObject;
diff --git a/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java b/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java
new file mode 100644
index 000000000..9061af9b8
--- /dev/null
+++ b/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java
@@ -0,0 +1,72 @@
+package com.hyperwallet.android.rule;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class HyperwalletExternalResourceManager extends TestWatcher {
+
+ private static final String EMPTY = "";
+ private ClassLoader classLoader;
+ private Logger logger;
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ classLoader = description.getTestClass().getClassLoader();
+ logger = Logger.getLogger(description.getTestClass().getName());
+ }
+
+ public String getResourceContent(final String resourceName) {
+ if (resourceName == null) {
+ throw new IllegalArgumentException("Parameter resourceName cannot be null");
+ }
+
+ return getContent(resourceName);
+ }
+
+ private String getContent(final String resourceName) {
+ URL resource = classLoader.getResource(resourceName);
+ InputStream inputStream = null;
+ Writer writer = new StringWriter();
+ String resourceContent = EMPTY;
+ if (resource != null) {
+ try {
+ inputStream = resource.openStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+ String line = reader.readLine();
+ while (line != null) {
+ writer.write(line);
+ line = reader.readLine();
+ }
+ resourceContent = writer.toString();
+
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "There was an error loading an external resource", e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (Exception e) {
+ logger.log(Level.SEVERE, "There was an error closing input stream", e);
+ }
+ try {
+ writer.close();
+ } catch (IOException e) {
+ logger.log(Level.SEVERE, "There was an error closing writer", e);
+ }
+ }
+ }
+ return resourceContent;
+ }
+}
diff --git a/receipt/src/test/resources/authentication_token_response.json b/receipt/src/test/resources/authentication_token_response.json
new file mode 100644
index 000000000..e8e41bf37
--- /dev/null
+++ b/receipt/src/test/resources/authentication_token_response.json
@@ -0,0 +1,3 @@
+{
+ "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c3ItZjZlNmZjY2EtNTBmNy00ZWY1LWExYzUtNWZmMDJlMDU2MzgzIiwiaWF0IjoxNTQ5NTgwMzk0LCJleHAiOjI1NDk1ODA5OTQsImF1ZCI6InBndS03YTEyMzJlOC0xNDc5LTQzNzAtOWY1NC03ODc1ZjdiMTg2NmMiLCJpc3MiOiJwcmctY2NhODAyNWUtODVhMy0xMWU2LTg2MGEtNThhZDVlY2NlNjFkIiwicmVzdC11cmkiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVzdC92My8iLCJncmFwaHFsLXVyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9ncmFwaHFsIn0.kILSynYHbepbl4sVqENnNog09iGByfTrckHhSCjVgnuRnuspI72cx3rt0SB2V_neHwzYkD_VfhNKk9gJDOwXeQ"
+}
\ No newline at end of file
diff --git a/receipt/src/test/resources/receipt_credit_response.json b/receipt/src/test/resources/receipt_credit_response.json
new file mode 100644
index 000000000..4351d17a2
--- /dev/null
+++ b/receipt/src/test/resources/receipt_credit_response.json
@@ -0,0 +1,30 @@
+{
+ "count": 1,
+ "offset": 0,
+ "limit": 10,
+ "data": [
+ {
+ "journalId": "3051581",
+ "type": "PAYMENT",
+ "createdOn": "2019-06-02T17:09:07",
+ "entry": "CREDIT",
+ "sourceToken": "act-12345",
+ "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "amount": "25.00",
+ "fee": "0.00",
+ "currency": "CAD",
+ "details": {
+ "clientPaymentId": "ABC1234",
+ "payeeName": "A Person"
+ }
+ }
+ ],
+ "links": [
+ {
+ "params": {
+ "rel": "self"
+ },
+ "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/receipt/src/test/resources/receipt_debit_response.json b/receipt/src/test/resources/receipt_debit_response.json
new file mode 100644
index 000000000..8ed756455
--- /dev/null
+++ b/receipt/src/test/resources/receipt_debit_response.json
@@ -0,0 +1,25 @@
+{
+ "count": 1,
+ "offset": 0,
+ "limit": 10,
+ "data": [
+ {
+ "journalId": "3051590",
+ "type": "TRANSFER_TO_PREPAID_CARD",
+ "createdOn": "2019-05-02T17:12:18",
+ "entry": "DEBIT",
+ "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "destinationToken": "trm-12345",
+ "amount": "18.05",
+ "currency": "USD"
+ }
+ ],
+ "links": [
+ {
+ "params": {
+ "rel": "self"
+ },
+ "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/receipt/src/test/resources/receipt_list_response.json b/receipt/src/test/resources/receipt_list_response.json
new file mode 100644
index 000000000..7e7cc58af
--- /dev/null
+++ b/receipt/src/test/resources/receipt_list_response.json
@@ -0,0 +1,64 @@
+{
+ "count": 4,
+ "offset": 0,
+ "limit": 10,
+ "data": [
+ {
+ "journalId": "3051579",
+ "type": "PAYMENT",
+ "createdOn": "2019-06-07T17:08:58",
+ "entry": "CREDIT",
+ "sourceToken": "act-12345",
+ "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "amount": "20.00",
+ "fee": "0.00",
+ "currency": "USD",
+ "details": {
+ "clientPaymentId": "8OxXefx5",
+ "payeeName": "A Person"
+ }
+ },
+ {
+ "journalId": "3051581",
+ "type": "PAYMENT",
+ "createdOn": "2019-06-02T16:09:07",
+ "entry": "CREDIT",
+ "sourceToken": "act-12345",
+ "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "amount": "25.00",
+ "fee": "0.00",
+ "currency": "CAD",
+ "details": {
+ "clientPaymentId": "Q3SVvpv0",
+ "payeeName": "A Person"
+ }
+ },
+ {
+ "journalId": "3051582",
+ "type": "CARD_ACTIVATION_FEE",
+ "createdOn": "2019-06-01T11:09:16",
+ "entry": "DEBIT",
+ "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "amount": "1.95",
+ "currency": "USD"
+ },
+ {
+ "journalId": "3051590",
+ "type": "TRANSFER_TO_PREPAID_CARD",
+ "createdOn": "2018-12-01T17:12:18",
+ "entry": "DEBIT",
+ "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "destinationToken": "trm-12345",
+ "amount": "18.05",
+ "currency": "USD"
+ }
+ ],
+ "links": [
+ {
+ "params": {
+ "rel": "self"
+ },
+ "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/receipt/src/test/resources/receipt_unknown_type_response.json b/receipt/src/test/resources/receipt_unknown_type_response.json
new file mode 100644
index 000000000..bf856ed63
--- /dev/null
+++ b/receipt/src/test/resources/receipt_unknown_type_response.json
@@ -0,0 +1,30 @@
+{
+ "count": 1,
+ "offset": 0,
+ "limit": 10,
+ "data": [
+ {
+ "journalId": "3051581",
+ "type": "ICK",
+ "createdOn": "2019-06-02T17:09:07",
+ "entry": "CREDIT",
+ "sourceToken": "act-12345",
+ "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d",
+ "amount": "25.00",
+ "fee": "0.00",
+ "currency": "CAD",
+ "details": {
+ "clientPaymentId": "ABC1234",
+ "payeeName": "A Person"
+ }
+ }
+ ],
+ "links": [
+ {
+ "params": {
+ "rel": "self"
+ },
+ "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ui/src/androidTest/AndroidManifest.xml b/ui/src/androidTest/AndroidManifest.xml
index 9b2c52c35..0cd133def 100644
--- a/ui/src/androidTest/AndroidManifest.xml
+++ b/ui/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
+ package="com.hyperwallet.android.hyperwallet_ui.test">