Taking Screen shot using espresso - android

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.

Related

How to check the current Activity in a UI test

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.

Using Android UI Automator to test content on a secondary display

I have a barebones app written to test support for multiple displays. My setup is a custom Android tablet running Android 8.1.0 (the "primary display") connected to a touchscreen (the "secondary display") via HDMI (to deliver video signal) and USB (to deliver touch events).
The app contains a single activity, which displays "Hello World!" on the primary display but also leverages DisplayManager and WindowManager to add a counter and two + / - buttons to the secondary display:
Running the app normally and interacting with the buttons on the secondary display works as expected.
Now, I want to use UI Automator to, say, click the + button and verify that the the counter records the correct value. This appears to be impossible. Does anyone know how I can do this?
Alternatively, if UI Automator is not the right tool for the job, but there is some other tool that will let me write end-to-end black box-style tests for apps that display content on a secondary display, I am happy to get recommendations.
Some things I've investigated
I've used the uiautomatorviewer tool to inspect the layout hierarchy of my app. Only the content on the primary display is visible with this tool:
I've used UiDevice.dumpWindowHierarchy() to get a text dump of everything on the device. Only the content on the primary device is dumped, though this includes information about system windows like the status bar and navigation bar.
I've used adb shell dumpsys window (with both the tokens and windows commands). This will show me information about the window that I've created on the secondary display, but I seem to have no way to access this window through Ui Automator:
Display #1
WindowToken{551f82f android.os.BinderProxy#87f65ac}:
windows=[Window{e40e275 u0 com.example.stackoverflow}]
windowType=2038 hidden=false hasVisible=true
Window #10 Window{20ac4ce u0 com.example.stackoverflow}:
mDisplayId=1 mSession=Session{91fdd93 8079:u0a10089} mClient=android.os.BinderProxy#a3e65c9
mOwnerUid=10089 mShowToOwnerOnly=true package=com.example.stackoverflow appop=SYSTEM_ALERT_WINDOW
mAttrs=WM.LayoutParams{(0,0)(fillxfill) sim=#20 ty=2038 fl=#1280480 colorMode=0}
Requested w=1280 h=800 mLayoutSeq=908
mBaseLayer=121000 mSubLayer=0 mAnimLayer=121000+0=121000 mLastLayer=121000
mToken=WindowToken{a42e219 android.os.BinderProxy#a3e65c9}
...
Relevant code samples
Adding the content to the secondary display (in my Activity's onResume() method):
DisplayManager manager = (DisplayManager) getApplicationContext().getSystemService(Context.DISPLAY_SERVICE);
Display display = manager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)[0];
Context displayContext = getApplicationContext().createDisplayContext(display);
WindowManager windowManager = (WindowManager) displayContext.getSystemService(Context.WINDOW_SERVICE);
LinearLayout root = new LinearLayout(displayContext);
WindowManager.LayoutParams params = createLayoutParams();
windowManager.addView(root, params);
View.inflate(root.getContext(), R.layout.overlay, root);
root.findViewById(R.id.minus).setOnClickListener(v -> decrementCounter());
root.findViewById(R.id.plus).setOnClickListener(v -> incrementCounter());
I'm using the Application context instead of the Activity context so that the content on the secondary display is not dependent on the lifetime of the Activity that creates it. Theoretically I could navigate to other activities and this content would remain on the secondary display and remain interactive.
Creating the LayoutParams object:
private WindowManager.LayoutParams createLayoutParams() {
return new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
0, 0,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
0,
PixelFormat.OPAQUE
);
}
There is Espresso which works for testing your app. The good thing is, you don't have choose which to use. You can use them both. Espresso is running in separate thread and it is really fast comparing to other test frameworks. Use both of them in your tests.
Espresso tests state expectations, interactions, and assertions clearly without the distraction of boilerplate content, custom infrastructure, or messy implementation details getting in the way.
Espresso tests run optimally fast! It lets you leave your waits, syncs, sleeps, and polls behind while it manipulates and asserts on the application UI when it is at rest.
This is Espresso's Multiprocess and this is the Cheat Sheet for Espresso.
Use Espresso with ActivityTestRule
The following section describes how to create a new Espresso test in the JUnit 4 style and use ActivityTestRule to reduce the amount of boilerplate code you need to write. By using ActivityTestRule, the testing framework launches the activity under test before each test method annotated with #Test and before any method annotated with #Before. The framework handles shutting down the activity after the test finishes and all methods annotated with #After are run.
package com.example.android.testing.espresso.BasicSample;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
#RunWith(AndroidJUnit4.class)
#LargeTest
public class ChangeTextBehaviorTest {
private String stringToBetyped;
#Rule
public ActivityTestRule<MainActivity> activityRule
= new ActivityTestRule<>(MainActivity.class);
#Before
public void initValidString() {
// Specify a valid string.
stringToBetyped = "Espresso";
}
#Test
public void changeText_sameActivity() {
// Type text and then press the button.
onView(withId(R.id.editTextUserInput))
.perform(typeText(stringToBetyped), closeSoftKeyboard());
onView(withId(R.id.changeTextBt)).perform(click());
// Check that the text was changed.
onView(withId(R.id.textToBeChanged))
.check(matches(withText(stringToBetyped)));
}
}
And if you want to learn more, here is the testing documentation

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.

How to write a test case method using Robotium in Android

I am writing a test case class for one of the activity in my application.
That Activity class contains license checking for the application in android market and also displays the splash screen for 3 seconds. Here I would like to test that activity is displaying the splash screen and checking the license using Robotium instrumentation in Android.
So please tell me how to do this.
To test that your splash screen is shown, you can try this, if you have set up robotium in your setup method, and named it solo:
public void testSplash() {
assertNotNull(solo.getCurrentActivity().findViewById( "the id of the splash" ));
}
public void testLicense() {
String licence = "my licence";
assertEquals(licence, (MyActivity) solo.getCurrentActivity().getLicence());
}

Categories

Resources