Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

Commit

Permalink
Add automated card scanning tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lkorth committed Oct 20, 2016
1 parent 6b65331 commit c27299a
Show file tree
Hide file tree
Showing 15 changed files with 231 additions and 560 deletions.
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ There are a few bugs in the build process, so these steps are required for the f

You should see the app open and run through some tests.

#### Recordings

Any card recordings present in `SampleApp/src/main/assets/recordings` will be loaded when the sample
app is run and will be available for testing.

### Un-official Release

`$ ./gradlew clean :card.io:assembleRelease` Cleans and builds an aar file for distribution.
Expand Down
4 changes: 4 additions & 0 deletions SampleApp/src/androidTest/assets/test_card_images/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Test Card Images

Card images in this folder are included for automated testing. Images should be 640px x 480px for
tests.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions SampleApp/src/androidTest/java/io/card/payment/CardScannerTester.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.card.payment;

/* CardScannerTester.java
* See the file "LICENSE.md" for the full license governing this code.
*/

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.view.SurfaceHolder;

import java.io.IOException;

import static android.support.test.InstrumentationRegistry.getInstrumentation;

public class CardScannerTester extends CardScanner {

private static final long FRAME_INTERVAL = (long) (1000.0 / 30);

private static String sCardAssetName;

private boolean mScanAllowed;
private Handler mHandler;
private byte[] mFrame;

public static void setCardAsset(String cardAssetName) {
sCardAssetName = cardAssetName;
}

public CardScannerTester(CardIOActivity scanActivity, int currentFrameOrientation) {
super(scanActivity, currentFrameOrientation);
useCamera = false;
mScanAllowed = false;
mHandler = new Handler();

try {
Bitmap bitmap = BitmapFactory.decodeStream(getInstrumentation().getContext().getAssets()
.open("test_card_images/" + sCardAssetName));
mFrame = getNV21FormattedImage(bitmap.getWidth(), bitmap.getHeight(), bitmap);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Runnable mFrameRunnable = new Runnable() {
@Override
public void run() {
if (!mScanAllowed) {
return;
}

onPreviewFrame(mFrame, null);
mHandler.postDelayed(this, FRAME_INTERVAL);
}
};

@Override
boolean resumeScanning(SurfaceHolder holder) {
boolean result = super.resumeScanning(holder);
mScanAllowed = true;
mHandler.postDelayed(mFrameRunnable, FRAME_INTERVAL);
return result;
}

@Override
public void pauseScanning() {
mScanAllowed = false;
super.pauseScanning();
}

private byte[] getNV21FormattedImage(int width, int height, Bitmap bitmap) {
int [] argb = new int[width * height];
byte [] yuv = new byte[width * height * 3 / 2];

bitmap.getPixels(argb, 0, width, 0, 0, width, height);
encodeYUV420SP(yuv, argb, width, height);
bitmap.recycle();

return yuv;
}

private void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;

int R, G, B, Y, U, V;
int index = 0;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
R = (argb[index] & 0xff0000) >> 16;
G = (argb[index] & 0xff00) >> 8;
B = (argb[index] & 0xff);

// well known RGB to YUV algorithm
Y = ( ( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ( ( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ( ( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
// meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every
// other pixel AND every other scanline.
yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (j % 2 == 0 && index % 2 == 0) {
yuv420sp[uvIndex++] = (byte)((V<0) ? 0 : ((V > 255) ? 255 : V));
yuv420sp[uvIndex++] = (byte)((U<0) ? 0 : ((U > 255) ? 255 : U));
}

index ++;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.card.test;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.support.test.rule.ActivityTestRule;
import android.view.WindowManager;

import org.junit.Rule;
import org.junit.Test;

import java.lang.reflect.Field;

import io.card.payment.CardIOActivity;
import io.card.payment.CardScannerTester;
import io.card.payment.CreditCard;

import static com.lukekorth.deviceautomator.DeviceAutomator.onDevice;
import static junit.framework.Assert.assertEquals;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class CardIOActivityTest {

private CardIOTestActivity mActivity;

@Rule
public final ActivityTestRule<CardIOTestActivity> mActivityTestRule =
new ActivityTestRule<>(CardIOTestActivity.class, false, false);

@Test(timeout = 30000)
public void scansAmexCards() {
CardScannerTester.setCardAsset("amex.png");

startScan();

waitForActivityToFinish();
CreditCard result = getActivityResultIntent().getParcelableExtra(CardIOActivity.EXTRA_SCAN_RESULT);
assertEquals("3743 260055 74998", result.getFormattedCardNumber());
}

private void startScan() {
mActivityTestRule.launchActivity(null);
mActivity = mActivityTestRule.getActivity();
mActivity.runOnUiThread(new Runnable() {
public void run() {
mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});

onDevice().acceptRuntimePermission(Manifest.permission.CAMERA);
}

private void waitForActivityToFinish() {
long endTime = System.currentTimeMillis() + 10000;

do {
try {
if (mActivity.isFinishing()) {
return;
}
} catch (Exception ignored) {}
} while (System.currentTimeMillis() < endTime);

throw new RuntimeException("Maximum wait elapsed (10s) while waiting for activity to finish");
}

private Intent getActivityResultIntent() {
assertThat("Activity did not finish", mActivity.isFinishing(), is(true));

try {
Field resultDataField = Activity.class.getDeclaredField("mResultData");
resultDataField.setAccessible(true);
return (Intent) resultDataField.get(mActivity);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package io.card;
package io.card.test;

import android.Manifest;
import android.app.Activity;
import android.os.SystemClock;
import android.support.test.rule.ActivityTestRule;
import android.view.WindowManager;

Expand All @@ -15,7 +14,6 @@
import io.card.payment.i18n.LocalizedStrings;
import io.card.payment.i18n.StringKey;

import static android.support.test.espresso.Espresso.onData;
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.typeText;
Expand All @@ -25,9 +23,6 @@
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static com.lukekorth.deviceautomator.DeviceAutomator.onDevice;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.Is.is;

public class SampleActivityTest {

Expand Down Expand Up @@ -94,19 +89,6 @@ public void canEnterManualEntryFromScanActivity() {
onView(withText(LocalizedStrings.getString(StringKey.DONE))).perform(click());
}

@Test
public void recordingPlayback() {
onView(withText("Expiry")).perform(click());
onView(withId(R.id.recordings)).perform(click());
onData(allOf(is(instanceOf(String.class)), is("recording_320455133.550273.zip"))).perform(click());

SystemClock.sleep(5000);
onView(withId(100)).perform(click(), typeText("1222"));
onView(withText(LocalizedStrings.getString(StringKey.DONE))).perform(click());

onView(withId(R.id.result)).check(matches(withText(containsString("Expiry: 12/2022"))));
}

private void fillInCardForm() {
onView(withId(100)).perform(click(), typeText("4111111111111111"));
onView(withId(101)).perform(click(), typeText("1222"));
Expand Down
7 changes: 2 additions & 5 deletions SampleApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.card.development">

<!-- for testing -->
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>

<application
android:name=".SampleApplication"
android:label="card.io Sample App"
android:theme="@style/MyTheme">

<activity android:name=".SampleActivity">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

</activity>

<activity android:name="io.card.test.CardIOTestActivity"/>

</application>

</manifest>
7 changes: 0 additions & 7 deletions SampleApp/src/main/assets/recordings/README.md

This file was deleted.

Binary file not shown.
58 changes: 1 addition & 57 deletions SampleApp/src/main/java/io/card/development/SampleActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,27 @@
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import io.card.development.recording.Recording;
import io.card.payment.CardIOActivity;
import io.card.payment.CardScannerTester;
import io.card.payment.CardType;
import io.card.payment.CreditCard;
import io.card.payment.i18n.StringKey;
import io.card.payment.i18n.SupportedLocale;
import io.card.payment.i18n.locales.LocalizedStringsList;

public class SampleActivity extends Activity implements AdapterView.OnItemSelectedListener {
public class SampleActivity extends Activity {

protected static final String TAG = SampleActivity.class.getSimpleName();

private static final String RECORDING_DIR = "recordings";

private static final int REQUEST_SCAN = 100;
private static final int REQUEST_AUTOTEST = 200;

Expand Down Expand Up @@ -98,7 +92,6 @@ public void onCreate(Bundle savedInstanceState) {

setScanExpiryEnabled();
setupLanguageList();
setupRecordingList();
}

private void setScanExpiryEnabled() {
Expand Down Expand Up @@ -227,53 +220,4 @@ private void setupLanguageList() {
mLanguageSpinner.setAdapter(adapter);
mLanguageSpinner.setSelection(adapter.getPosition("en"));
}

private void setupRecordingList() {
Spinner recordingSpinner = (Spinner) findViewById(R.id.recordings);
try {
String[] allFiles = getAssets().list(RECORDING_DIR);
ArrayList<String> recordingNames = new ArrayList<>();
recordingNames.add("Select a recording");
if (allFiles != null && allFiles.length > 0) {
for (String name : allFiles) {
if (name.startsWith("recording_") && name.endsWith(".zip")) {
recordingNames.add(name);
}
}
}

if (recordingNames.size() > 1) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_dropdown_item_1line, recordingNames);
recordingSpinner.setAdapter(adapter);
recordingSpinner.setOnItemSelectedListener(this);
recordingSpinner.setVisibility(View.VISIBLE);
}
} catch (IOException ignored) {}
}

@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position <= 0) {
return;
}

String path = RECORDING_DIR + "/" + parent.getAdapter().getItem(position);
Recording recording = new Recording(SampleActivity.this, path);
CardScannerTester.setRecording(recording);

Intent intent = new Intent(SampleActivity.this, CardIOActivity.class)
.putExtra("io.card.payment.cameraBypassTestMode", true)
.putExtra(CardIOActivity.EXTRA_REQUIRE_EXPIRY, mEnableExpiryToggle.isChecked())
.putExtra(CardIOActivity.EXTRA_SCAN_EXPIRY, mScanExpiryToggle.isChecked())
.putExtra(CardIOActivity.EXTRA_REQUIRE_CVV, mCvvToggle.isChecked())
.putExtra(CardIOActivity.EXTRA_REQUIRE_POSTAL_CODE, mPostalCodeToggle.isChecked())
.putExtra(CardIOActivity.EXTRA_RESTRICT_POSTAL_CODE_TO_NUMERIC_ONLY, mPostalCodeNumericOnlyToggle.isChecked())
.putExtra(CardIOActivity.EXTRA_REQUIRE_CARDHOLDER_NAME, mCardholderNameToggle.isChecked());

startActivityForResult(intent, REQUEST_SCAN);
}

@Override
public void onNothingSelected(AdapterView<?> parent) {}
}
Loading

0 comments on commit c27299a

Please sign in to comment.