How to check the current Activity in a UI test - android

For tests I use Espresso and Barista
I have a test in which I need to open another screen by pressing a button. How can I check if this screen opens? Did the screen I need open?
Can I somehow check the chain of screens? To understand that the screens open in the order I need?
If someone throws links to good tutorials on UI tests in Android, I will be very grateful.

An easy solution would be to just check for an element of the new screen to be shown like this:
onView(withId(R.id.id_of_element_in_your_new_screen)).check(matches(isDisplayed()))
If you really want to check out for the current activity that is shown, you could try something like this:
Gather the current activity via InstrumentationRegistry and check for the activity in stage RESUMED.
fun getTopActivity(): Activity? {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.iterator().hasNext()) {
resumedActivities.iterator().next()?.let {
activity = it
}
}
}
return activity
}
You could then check this in a test like this:
#Test
fun checkForActivity() {
val currentActivity = getTopActivity()
assertTrue(currentActivity?.javaClass == YourActivityToCheckAgainst::class.java)
}

I personally use intended(hasComponent(YourActivityToCheckAgainst::class.java.name)), which checks if the last intent was done with a desired activity, set as its component.
I also wrote an extensive Android UI testing tutorial using Espresso + Barista libraries.

Related

Android - Espresso How to test an view after clicked and moved to another Activity

I'm beginner of Espresso UI Testing. I have an issue, I have found solution but I'm not know how to do that correctly :((
Problem:
I have 2 ImageView, when I click on once will change drawable of it and start an Activity. I want to check drawable after click does correct?
My code
// In Main Activity
val imageView1 = findViewById(R.id.iv_button1)
imageView1.setOnClickListener {
imageView1.setImageDrawable(resources.getDrawable(R.drawable.image1))
startActivity(Intent(applicationContext, OtherAcitivy1::class.java))
}
val imageView2 = findViewById(R.id.iv_button2)
imageView2.setOnClickListener {
imageView2.setImageDrawable(resources.getDrawable(R.drawable.image2))
startActivity(Intent(applicationContext, OtherAcitivy2::class.java))
}
// In Android Test Class
...After run activiy
#Test
fun checkClickImageView1() {
onView(withId(R.id.iv_button1)).perform(click())
// In here, I want to check the imageview has displayed drawable correctly
onView(withId(R.id.iv_button1)).check(withDrawableMatcher(R.drawable.image1))
}
But, It throw an exception is could not found view with R.id.iv_button1.
I think, because I start OtherActivty2 on action click so it could not found view with that id from root view of OtherActivty2
Have any solution can help me check drawable of ImageView in this case?
Thanks so much.
I would use espresso intents
This way you capture that your app is going to open the other activity and respond with a result. Then, as the activity you are has not changed you can check the drawable there.
The code in your case should be something like:
#Test
fun checkClickImageView1() {
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, null)
// Set up result stubbing when an intent sent to "OtherAcitivy2" is seen.
intending(hasComponent(OtherAcitivy2::class.java.name)).respondWith(result)
onView(withId(R.id.iv_button1)).perform(click())
onView(withId(R.id.iv_button1)).check(withDrawableMatcher(R.drawable.image1))
}
don't forget to add the espresso-intents dependencies in your build.gradle file.

View added programatically not changing visibility after a long time not used

I have a problem I have not yet been able to understand, nor solve.
On my android project, I have a BaseActivity. There, among other functions, I decided to add a function to show or hide a loading view when necessary. It works as intended, but sometimes an error happens.
I will try to raise some important info about my project I think can be useful. My app is integrated with an external login app. I call it when the services I call need to refresh it's token. When the user logs in on the app, it calls a listener and give me back control on mine.
The problem is the next one:
I come to an activity that needs to call a service and I have the token all right, but then I lock the phone. After a long period of time, I unlock the phone and, from the same activity, I call again the service. My activity shows de loader as intended and, as my token is expired, I call the login app from it's SDK.
When I come back to my app, and I call the service I wanted successfully, the app tries to hide the loader. This is where the fail comes, as I can't change the visibility to GONE. I looked for it on the view hierarchy and find it, but with visibility = VISIBLE.
Here is the piece of code from the loader, hope someone can find where I'm making the mistake!
abstract class BaseActivity : DaggerAppCompatActivity(){
// These are the IDS of the Views I'm adding to the activity, so I can track them and change their visibility
var imgLoadingID = -1
var rvLoadingID = -1
fun showLoading() {
// If the views are added I show them
if (imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.VISIBLE
imageView.visibility = VISIBLE
imageView.isClickable = false
imageView.isFocusable = false
} else {
// else I create them and show them
val imgLoading = ImageView(this)
imgLoading.id = View.generateViewId()
imgLoadingID = imgLoading.id
val maxpx = CustomUtils.ViewUtils.converIntToDps(65, this)
Glide.with(this).asGif().load(R.mipmap.loading).into(imgLoading)
val relativeLayout = RelativeLayout(this)
relativeLayout.id = View.generateViewId()
rvLoadingID = relativeLayout.id
var params = RelativeLayout.LayoutParams(maxpx, WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_IN_PARENT)
relativeLayout.addView(imgLoading, params)
relativeLayout.background = getDrawable(R.color.pure_white_97)
relativeLayout.isClickable = true
relativeLayout.isFocusable = true
findViewById<ViewGroup>(android.R.id.content).addView(relativeLayout, RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
imgLoading.visibility = VISIBLE
}
// I lock the back button so people don't cancel my requests
esBackPressedBlocked = true
}
// Here I find the views and change their visibility.
fun hideLoading(){
if(imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.GONE
imageView.visibility = View.GONE
}
esBackPressedBlocked = false
}
}
I deleted some logs I added to the whole function, but when it fails, it enters on the hideLoading() function, even on the relativeLayout.visibility = View.GONE part.
The function used to be with the Views as an whole object instead of their ids, but I found it more reliable this way, and saving the views instead of their Id's had the same problem.
My main concern is how Android manages my application while the phone is locked for this period of time (the fails happened after 8-10 hours of inactivity). I think something there can be creating this issue. I thought also about the external Login app, since it's sdk is launching their app's intent and calling me from a listener when coming back, but, since my code is being executed, I think Android it's managing my views on an strange way. Or maybe I try to hide the loading view before I'm on the resumed activity... I don't really know.
BTW, I know there are easier solutions on showing a loader, but I wanted to create it the cleanest way. If you have any cleaner approach I'm open to any solution.
If there is anything unclear let me know in the comments, and I hope my English was clear enough to express myself, it's a tricky problem that I can't understand, so it's difficult for me to explain it.
Thanks!!

Taking Screen shot using espresso

I am using Espresso for my UI Testing in my project. I want to take screen shot of each Activity(Screen). I am using ScreenShooter from GoogleCloudTestLab for taking screen shot.
ScreenShotter.takeScreenshot("main_screen_2", getActivity());
But it only taking the screen shot of the 1st activity which i defined in my ActivityTestRule. How can I take other activity screen shot with in the same testcase.
My understanding is ActivityTestRule is designed to test only one Activity within the testcase so getActivity() will only return the activity that you specified in the ActivityTestRule.
To capture the screenshot, the library currently uses:
View screenView = activity.getWindow().getDecorView().getRootView();
screenView.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache());
screenView.setDrawingCacheEnabled(false);
(where activity is the activity the user passes us.)
So because the same activity is being given to takescreenshot, we are only able to capture that activity's view hierarchy at that time. Would you be able to split up your tests to test only one activity per testcase?
Also, we are currently exploring other ways to capture the screen and will add to this thread if we change this method.
Note: If you are using this library to run tests in Firebase Test Lab and you have a prefered way of capturing screenshots (instead of using the library), as long as they end up in the /sdcard/screenshots directory then they will be pulled and uploaded to the dashboard at the end of the test.
I had the same issue since my tests cover flows which span multiple activities. A helper method such as this one can be used to get a reference to the currently active (on top) activity:
/**
* A helper method to get the currently running activity under test when a test run spans across multiple
* activities. The {#link android.support.test.rule.ActivityTestRule} only returns the initial activity that
* was started.
*/
public static final Activity getCurrentActivity(Instrumentation instrumentation)
{
final Activity[] currentActivity = new Activity[1];
instrumentation.runOnMainSync(new Runnable()
{
public void run()
{
Collection<Activity> resumedActivities =
ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
if (resumedActivities.iterator().hasNext())
{
currentActivity[0] = resumedActivities.iterator().next();
}
}
});
return currentActivity[0];
}
Pass it getInstrumentation() from within your test and you should be good to go.

How to rotate activity, I mean: screen orientation change using Espresso?

I have decided that one of the testing criteria for my application tests with Google's Espresso is:
Test should maintain Activity state after screen orientation rotation
How do I rotate the screen when using Espresso?
I have tried the following Robotium code (Yes I placed Robotium code in my Espresso test so sue me)
solo.setActivityOrientation(solo.LANDSCAPE);
solo.setActivityOrientation(solo.PORTRAIT);
but It crashes the application when I run it within my Espresso test.
Is there any way to do this?
Thanks in advance for any help
If you have the only Activity in your test case, you can do:
1. Declare you test Rule.
#Rule
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(TestActivity.class);
2. Get you Activity and apply a screen rotation.
mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
That's a piece of pie!
You can do it with uiautomator library
dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
ui automator require min sdk version 18 so if your app has a lower min sdk you need to create a new AndroidManifest.xml in androidTest folder
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:tools="http://schemas.android.com/tools"
package="your.package.name">
<uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
</manifest>
and then in your test
UiDevice device = UiDevice.getInstance(getInstrumentation());
device.setOrientationLeft();
device.setOrientationNatural();
device.setOrientationRight();
This more complete solution creates a custom Espresso ViewActionand works well. It shows how to get the Activity (even when it is an AppCompatActivity) before calling its setRequestedOrientation() method. It also has a clean caller interface:
onView(isRoot()).perform(orientationLandscape());
onView(isRoot()).perform(orientationPortrait());
I follow each orientation change with a 100 ms delay, though you may not need it.
How to rotate the screen:
public static void rotateScreen(Activity activity) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
final int orientation = InstrumentationRegistry.getTargetContext()
.getResources()
.getConfiguration()
.orientation;
final int newOrientation = (orientation == Configuration.ORIENTATION_PORTRAIT) ?
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
activity.setRequestedOrientation(newOrientation);
getInstrumentation().waitForIdle(new Runnable() {
#Override
public void run() {
countDownLatch.countDown();
}
});
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException("Screen rotation failed", e);
}
}
The Activity can be obtained from the ActivityRule.
You can't mix Robotium and Espresso tests. The best way sometimes to solve any issue is to check source code of desired but not comaptible method.
I'm pretty sure that you have already setUp() method, which has code like:
myActivity = this.getActivity();
Use this to change your screen orientation change:
myActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
or
myActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
You may also need to use myActivity.getInstrumentation().waitForIdleSync(); or Thread.sleep(milliseconds); in order to wait for the rotation end because it is performed in Async manner. The second methods depends on emulator/device so choose it wisely.
Hope it help.
After my own troubles with testing of the orientation I want to add that while lelloman's advice to use UiDevice is correct from the documentation standpoint - unfortunately it doesn't work as expected in some cases.
I found that at least on the API 23 emulator rotation could stuck after the test's crash:
Do uiDevice.setOrientationLeft()
Application crashes;
Rotation is stuck;
3.1 Forcing rotation through UiDevice works but when test ends rotation is back the wrong one until you manually change rotation to "Auto-rotate";
3.2 uiDevice.unfreezeRotation() helps while test is running but after the test's end rotation is back to the wrong one.
I don't have this issue on API 28.
So I found that using setRequestedOrientation is the only solution for me.
In addition, if you are using ActivityTestRule in order to access your activity, the documentation states that this class is deprecated and you are better off using ActivityScenarioRule.
Instead of directly accessing properties of the activity, you need to put your interactions with the activity inside a runnable callback.
So the screen rotation example may look something like this:
#get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private fun rotate() {
activityRule.scenario.onActivity {
it.requestedOrientation =
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
P.S. not sure about the proper constant to use, but ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE(or USER_PORTRAIT) was the one that worked for me

AppGyver Supersonic navigating between Views and creating duplicates

I'm using AppGyver Steroids and Supersonic to build an app and I'm having some issues navigating between views programmatically.
Based on the docs, you navigate between views like this:
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.push(view_obj);
However, when I inspect things via the Chrome DevTools, it appears that a second duplicate view is created i.e. If I navigate away from the index page and then navigate back, I now have two index pages, instead of what [I think] should be one. It also doesn't close the previous view I was on.
How can I prevent this from happening and simply move to the existing view, instead of duplicating views? How do I close a view after I have navigated away from it?
Thanks.
The problem you're encountering is that you're creating a new supersonic.ui.View("main#index") every time you navigate. On top of this, I think you want to return to the same view when you navigate back to a view for the second time, i.e. you want the view to remain in memory even if it has been removed from the navigation stack with pop() (rather than pushing a new instance of that view). For this, you need to preload or "start()" the view, as described in the docs here.
I implemented my own helper function to make this easier; here is my code:
start = function(dest, isModal) {
var viewId=dest,
view=new supersonic.ui.View({
location: dest,
id: viewId
});
view.isStarted().then(function(started) {
if (started) {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
} else {
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
Use it like start('module#view');. As a bonus, you can pass true as the second argument and it gets pushed as a modal instead.
It checks if you've already started a view - if so, it just pushes that view back onto the stack. If not, it start()s (i.e. preloads) it, then pushes it. This ensures that the view stays in memory (with any user input that has been modified) even when you pop() it from the stack.
You have to imagine that the layer stack is actually a stack in the Computer Science sense. You can only add and remove views at the top of the stack. The consequence of this is that complex navigations such as A > B > C > D > B are difficult/hacky to do (in this case, you'd have to pop() D and C in succession to get back to B).
Views will close if you pop() them, as long as you didn't start() them. If you did, and you pop() them, they remain in memory. To kill that view, you have to call stop() on it, as described in the docs I linked above.
try
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.replace(view_obj);
And take a look at supersonic.ui.layers.pop();
Thanks to LeedsEbooks for helping me get my head around this challenge. I was able to find a solution. Here is the code:
var start = function(route_str, isModal) {
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view_id_str = match_obj[2],
view_location_str = route_str,
view = new supersonic.ui.View({
location: view_location_str,
id: view_id_str
});
view.isStarted().then(function(started) {
if (started)
{
if (isModal)
{
supersonic.ui.modal.show(view);
}
else {
supersonic.ui.layers.push(view);
}
}
else
{
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal)
{
supersonic.ui.modal.show(view);
}
else
{
supersonic.ui.layers.push(view);
}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
You must ensure that your route has the format module#view as defined in the documentation.
PLEASE NOTE
There seems to some problem with the supersonic ui method for starting views. If you run the following code:
supersonic.ui.views.start("myapp#first-view");
supersonic.ui.views.find("first-view").then( function(startedView) {
console.log(startedView);
});
You'll notice that your view id and location are identical. This seems to be wrong as the id should be first-view and location should be myapp#first-view.
So I decided to not use the AppGyver methods and create my own preload method instead, which I run from the controller attached to my home view (this ensures that all the views I want to preload are handled when the app loads). Here is the function to do this:
var preload = function(route_str)
{
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view = new supersonic.ui.View({
location: route_str,
id: match_obj[2]
});
view.start();
};
By doing this, I'm sure that the view will get loaded with the right location and id, and that when I use my start() function later, I won't have any problems.
You'll want to make sure that your structure.coffee file doesn't have any preload instructions so as not to create duplicate views that you'll have problems with later.
Finally, I have a view that is 2 levels in that is a form that posts data via AJAX operation. I wanted the view to go back to the previous view when the AJAX operation was complete. Using my earlier function resulted in the push() being rejected. It would be nice if AppGyver Supersonic could intelligently detect that pushing to a previous view should default to a layers.pop operation, but you don't always get what you want. Anyway, I managed to solve this using supersonic.ui.layers.pop(), which simply does what the Back button would have done.
Everything working as intended now.

Categories

Resources