I need to automate my UI testing for following operation with espresso test project.
Operation:
Click a button that opens my phone camera. Capture Image, and save the image in sdcard storage. Also update a small image view on the screen when done.
App works fine but with all other operations and similar type of above operation it becomes a time consuming process to test it manually again and again.
I was working with similar problem
and found best available solution at below link
Camera UI test
// CameraActivityInstrumentationTest.java
public class CameraActivityInstrumentationTest {
// IntentsTestRule is an extension of ActivityTestRule. IntentsTestRule sets up Espresso-Intents
// before each Test is executed to allow stubbing and validation of intents.
#Rule
public IntentsTestRule<CameraActivity> intentsRule = new IntentsTestRule<>(CameraActivity.class);
#Test
public void validateCameraScenario() {
// Create a bitmap we can use for our simulated camera image
Bitmap icon = BitmapFactory.decodeResource(
InstrumentationRegistry.getTargetContext().getResources(),
R.mipmap.ic_launcher);
// Build a result to return from the Camera app
Intent resultData = new Intent();
resultData.putExtra("data", icon);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
// Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
// with the ActivityResult we just created
intending(toPackage("com.android.camera2")).respondWith(result);
// Now that we have the stub in place, click on the button in our app that launches into the Camera
onView(withId(R.id.btnTakePicture)).perform(click());
// We can also validate that an intent resolving to the "camera" activity has been sent out by our app
intended(toPackage("com.android.camera2"));
// ... additional test steps and validation ...
}
}
If you still have the need, you can use the new Espresso-Intent that mocks the activity result that you can use to test this flow. See the sample from Android Testing
Google has provided an example on this camera problem, which they show how to stub an camera intent, launch camera, and test if image is get from stub intent, and display on imageView.
#Rule
public IntentsTestRule<ImageViewerActivity> mIntentsRule = new IntentsTestRule<>(
ImageViewerActivity.class);
#Before
public void stubCameraIntent() {
ActivityResult result = createImageCaptureActivityResultStub();
// Stub the Intent.
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
}
#Test
public void takePhoto_drawableIsApplied() {
// Check that the ImageView doesn't have a drawable applied.
onView(withId(R.id.imageView)).check(matches(not(hasDrawable())));
// Click on the button that will trigger the stubbed intent.
onView(withId(R.id.button_take_photo)).perform(click());
// With no user interaction, the ImageView will have a drawable.
onView(withId(R.id.imageView)).check(matches(hasDrawable()));
}
private ActivityResult createImageCaptureActivityResultStub() {
// Put the drawable in a bundle.
Bundle bundle = new Bundle();
bundle.putParcelable(ImageViewerActivity.KEY_IMAGE_DATA, BitmapFactory.decodeResource(
mIntentsRule.getActivity().getResources(), R.drawable.ic_launcher));
// Create the Intent that will include the bundle.
Intent resultData = new Intent();
resultData.putExtras(bundle);
// Create the ActivityResult with the Intent.
return new ActivityResult(Activity.RESULT_OK, resultData);
}
Check this link for more detail IntentsAdvancedsample
For me none of the solutions worked out of the box but the following did. It's a combination of this answer and this comment.
Without calling Intents.init() my test would fail with the following exception:
java.lang.NullPointerException: Attempt to invoke virtual method 'androidx.test.espresso.intent.OngoingStubbing androidx.test.espresso.intent.Intents.internalIntending(org.hamcrest.Matcher)' on a null object reference
Complete solution:
#Before
fun setUp() {
Intents.init()
}
#After
fun tearDown() {
Intents.release()
}
#Test
fun testCameraIntent() {
// Build an ActivityResult that will return from the camera app and set your extras (if any).
val resultData = Intent()
resultData.putExtra(MediaStore.EXTRA_OUTPUT, "test.file.url")
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
// Mock the camera response. Now whenever an intent is sent to the camera, Espresso will respond with the result we pass here.
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result)
// Now click on the button in your app that launches the camera.
onView(withId(R.id.button_camera)).perform(click());
// Optionally, we can also verify that the intent to the camera has actually been sent from out.
intended(hasAction(MediaStore.ACTION_IMAGE_CAPTURE))
// At this point the onActivityResult() has been called so verify that whatever view is supposed to be displayed is indeed displayed.
onView(withId(R.id.some_view)).check(matches(isDisplayed()))
}
Related
I am currently working on a library project for Android which I plan on open sourcing. The library has an activity that I need to return a result for so the app that's utilising the library will need to call startActivityForResult.
My question is, if the user has an activity within their app, which calls a second activity within their own app which also needs to return a result, and this activity needs to call the activity from my library, my libraries activity, and their own activity will be using the same onActivityResult callback. Is there a way to avoid my activities request code, not conflicting with one of their own request codes, is it just a case of assume their own request codes are 1, 2, 3 etc and I start my libraries activity request code from some arbitrary number like 1000.
Is this just the way it works or is there a better way to avoid my request code for my library conflicting with another apps activities request code?
I think the library should give a parameter to specify request code by developer themselves for startActivityForResult, so they could never be conflicted in the same activity or fragment they have been called from.
Activity A can choose its own request codes, and Activity B will never know which request code is used by A.
Which is no problem because request codes are purely local. Each Activity instance is separate from other Activity instances - they won't be mixed up just because they all implement the same method (like onCreate() or in your case onActivityResult() ).
Let's take a look at some lines from the source code for android.app.Activity, starting at line 4614
public void startActivityForResult(#RequiresPermission Intent intent, int requestCode,
#Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
// If this start is requesting a result, we can avoid making
// the activity visible until the result is received. Setting
// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
// activity hidden during this time, to avoid flickering.
// This can only be done when a result is requested because
// that guarantees we will get information back when the
// activity is finished, no matter what happens to it.
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
The comments in the quoted code snippet show that the request code is used to determine if there is a result to be returned.
Please note that the parameters for startActivityForResult(Intent intent, int requestCode, Bundle options) are passed into a method execStartActivity() from the Instrumentation class in the same package android.app.
But there are four other parameters to execStartActivity() which serve to identify the calling app and the current Activity instance (I won't go into details of the Binder framework here, but there is for example a youtube video on this topic):
Context who, IBinder contextThread, IBinder token, Activity target
Again, the request code is only used to determine if there is a result to be returned (for a negative request code, startActivityForResult() is handled just like startActivity())
return requestCode >= 0 ? am.getResult() : null;
Besides that, the request code is just passed back to the Activity which called startActivityForResult().
So if an Activity doesn't use the same request code for different types of requests, all is good.
I test my app with UI testing and would like to check if the camera app opens.
I have did this with:
#Test
public void profileImageClickOpensCamera() {
mIntentsRule.getActivity().startActivity(new Intent(mIntentsRule.getActivity(), ProfileActivity.class));
onView(withId(R.id.circleProfileImage)).perform(click());
intended(toPackage("com.android.camera"));
}
It is working fine on most devices, however if I rain it on SAMSUNG Galaxy S8, which has "com.sec.android.app.camera" package of it's camera app, the test fails.
My question is, how could I check with espresso that the package contains the word "camera" ?
It's not the best solution because a device's camera app's package name could be anything, but even better then what I got know.
So I would like to do something like:
intended(StringContains(toPackage("com.android.camera")));
Any suggestions?
Thanks in advance.
You can test the intent action instead of package.
Something like intended(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)) or intended(hasAction(equalTo(MediaStore.ACTION_IMAGE_CAPTURE))) should work.
I had the same situation and I've managed to solve it like this:
PackageManager packageManager = InstrumentationRegistry.getTargetContext().getPackageManager();
String pack = resultData.resolveActivity(packageManager).getPackageName();
intended(toPackage(pack));
In my situation I had an activity with a button which opens the camera, lets you take a picture and returns with it in your activity. The full code of this test would be:
#Test
public void testCameraIntent() {
Bitmap icon = BitmapFactory.decodeResource(
InstrumentationRegistry.getTargetContext().getResources(),
R.drawable.husky);
// Build a result to return from the Camera app
Intent resultData = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
resultData.putExtra("data", icon);
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
PackageManager packageManager = InstrumentationRegistry.getTargetContext().getPackageManager();
String pack = resultData.resolveActivity(packageManager).getPackageName();
// Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond
// with the ActivityResult we just created
intending(toPackage(pack)).respondWith(result);
// Now that we have the stub in place, click on the button in our app that launches into the Camera
onView(withId(R.id.btn_takePicture)).perform(click());
intended(toPackage(pack));
}
And this is the result :-) the image with Husky dog is a local image I've set to be sent in my custom ActivityResult:
I tried to send id of an image from an activity to another activity. But unfortunately stopped. But i couldn't find any solution. This is my main activity. I kept all image id in a class Utils.
gridview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v,
int position, long id) {
Intent intent=new Intent(MainActivity.this,SendActivity.class);
intent.putExtra("id",Utils.THUMBNAIL_IDS[position]);
startActivity(intent);
}
});
And this is my sendActivity from where i tried to share this image to messenger:
Intent intent = getIntent();
position=getIntent().getExtras().getInt("id");
Now the code to share to messenger:
private void onMessengerButtonClicked() {
// The URI can reference a file://, content://, or android.resource. Here we use
// android.resource for sample purposes.
Uri uri = Uri.parse("android.resource://com.example.amit.bengalistickerfun/drawable/" +
MainActivity.mAdapter.getItem(position));
// Create the parameters for what we want to send to Messenger.
ShareToMessengerParams shareToMessengerParams =
ShareToMessengerParams.newBuilder(uri, "image/jpeg")
.setMetaData("{ \"image\" : \"tree\" }")
.build();
// Sharing from an Activity
MessengerUtils.shareToMessenger(this, 0, shareToMessengerParams);
if (mPicking) {
// If we were launched from Messenger, we call MessengerUtils.finishShareToMessenger to return
// the content to Messenger.
MessengerUtils.finishShareToMessenger(this, shareToMessengerParams);
} else {
// Otherwise, we were launched directly (for example, user clicked the launcher icon). We
// initiate the broadcast flow in Messenger. If Messenger is not installed or Messenger needs
// to be upgraded, this will direct the user to the play store.
MessengerUtils.shareToMessenger(
this,
REQUEST_CODE_SHARE_TO_MESSENGER,
shareToMessengerParams);
}
Can anybody help me?
I would suggest to put try catch block here , so that you can catch the exception. if any
try to observe log-cat and view the errors
try using android inbuilt logging class to see the logs
Post your error logs here ,so that we can see the issue and provide our inputs accordingly.
In my app, I have a button that pops up a dialog "Call xxxx-xxxx" Yes / No. After clicking Yes the number shall be called.
This is the test code:
#Test
public void testPhoneButton() {
clickContactTab();
ViewInteraction phoneButtonInteraction = Espresso.onView(ViewMatchers.withId(R.id.button_phone));
phoneButtonInteraction.perform(ViewActions.scrollTo());
phoneButtonInteraction.perform(ViewActions.click());
Espresso.onView(ViewMatchers.withText(R.string.dialog_phone_title)).inRoot(RootMatchers.isDialog()).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
Espresso.onView(ViewMatchers.withId(android.R.id.button2)).perform(ViewActions.click());
Intents.assertNoUnverifiedIntents();
phoneButtonInteraction.perform(ViewActions.click());
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click());
Intents.intended(Matchers.allOf(IntentMatchers.hasAction(Intent.ACTION_CALL), IntentMatchers.hasData(Uri.parse("tel:" + tel))));
}
Everything works fine, but how can I cancel the phone call after the test?
yogurtearls answer works for me, thanks:
#Test
public void testPhoneButton() {
clickContactTab();
ViewInteraction phoneButtonInteraction = Espresso.onView(ViewMatchers.withId(R.id.button_phone));
phoneButtonInteraction.perform(ViewActions.scrollTo());
phoneButtonInteraction.perform(ViewActions.click());
Espresso.onView(ViewMatchers.withText(R.string.dialog_phone_title)).inRoot(RootMatchers.isDialog()).check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
Espresso.onView(ViewMatchers.withId(android.R.id.button2)).perform(ViewActions.click());
Intents.assertNoUnverifiedIntents();
phoneButtonInteraction.perform(ViewActions.click());
Intent stubIntent = new Intent();
Instrumentation.ActivityResult stubResult = new Instrumentation.ActivityResult(Activity.RESULT_OK, stubIntent);
Intents.intending(IntentMatchers.hasAction(Intent.ACTION_CALL)).respondWith(stubResult);
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click());
Intents.intended(Matchers.allOf(IntentMatchers.hasAction(Intent.ACTION_CALL), IntentMatchers.hasData(Uri.parse("tel:" + tel))));
}
You should use Intent stubbing.
You can avoid actually bringing up the dialer, and instead check that the right intent was sent.
Before you click the yes button, call intendING .
var camera = {
settings : {
quality : 50,
targetWidth : 1024,
targetHeight : 1024,
correctOrientation : true
}
};
var error = function(message) {
alert("Error happened while trying to get a picture", message);
};
document.addEventListener("deviceready", function() {
camera.toFile = function() {
this.settings.destinationType = navigator.camera.DestinationType.FILE_URI;
return this;
},
camera.toBase64 = function() {
this.settings.destinationType = navigator.camera.DestinationType.DATA_URL;
return this;
},
camera.fromCamera = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.CAMERA;
return this;
};
camera.fromLibrary = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.PHOTOLIBRARY;
return this;
};
camera.fromPhotoAlbum = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.SAVEDPHOTOALBUM;
return this;
}
camera.get = function(callback) {
navigator.camera.getPicture(function(data) {
alert("taking a picture successful");
callback(data);
}, error, camera.settings);
};
}, false);
This is my small wrapper for the camera. And I call it like this:
camera.fromPhotoAlbum().toBase64().get(function(base64){});
About 20% of the time, the "alert("taking a picture successful");" is not called, while no error is shown. If I cancel taking a picture, an alert with the message "Error happened while trying to get a picture" is shown, so the error callback works.
Basically nothing happens. I've tested it on a Samsung Galaxy S2 on CM9 and a brand new HTC One X.
There was another question recently about this same problem that I answered. We ran into this at my company and solved it. It has more to do with the Android system than Phonegap.
What's happening is when you start the camera, your app goes into onStop(). While there, the Android system has the right to kill your app if it needs memory. It just so happens that memory usually gets low when the camera takes a picture and dumps it into memory, so there's a good chance your app will get killed while your user takes a picture.
Now that your app is dead, when the camera finishes, it restarts your app. That's why it's acting so weird; the camera comes back into your app, but not the same instance that it had before, so your callback never gets called, since it doesn't exist anymore.
You can reduce the frequency at which this occurs by reducing the quality of the picture and passing it by URI instead of data to your app, but the problem won't go away completely.
To work around the callback never happening, we made a Java callback that starts the camera and saves the picture to the same location every time it takes one. Then, when the app starts back up from getting killed, it looks in that location for the picture.
It's a weird solution to a stupid problem, but if your app gets killed, that camera callback simply won't happen. If you need more information on how to make the Java callback to do this, let me know and I'll put our code up here. Otherwise, take a look at this SO answer for more info.
EDIT: Here's the code we use in our main DroidGap activity:
private static final String folderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/appName";
private static final String filePath = "phonegapImage.jpg";
#Override
public void onCreate(Bundle savedState) {
//...The rest of onCreate, this makes the Java available in JavaScript
appView.addJavascriptInterface(this, "Camera");
}
public void takePhoto(final String callback) {
Log.v("Camera Plugin", "Starting takePhoto callback");
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(folderPath, filePath)));
startActivityForResult(intent, TAKE_PICTURE);
}
public String getPhotoUri() {
return Uri.fromFile(new File(folderPath, filePath)).toString();
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case TAKE_PICTURE:
if (resultCode == Activity.RESULT_OK) {
//Do whatever you need to do when the camera returns
//This is after the picture is already saved, we return to the page
}
break;
default:
Log.v("Camera", "Something strange happened...");
break;
}
}
Then, in your JavaScript, you can invoke the camera with:
Camera.takePhoto("onPhotoURISuccess");
//Then, to get the location of the photo after you take it and load the page again
var imgPath = Camera.getPhotoUri();
So, that's about it. Just make sure to change all of the path/file/page/etc names to what you want to use in your app. This will overwrite that image every time a picture is taken, but you can probably figure something out to dynamically name them if you don't want that. You can use that URI just as you would any other path in your JavaScript.