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
Related
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.
My Android application has a button to download a file and then send it to an application on the device. Android pops up a screen listing the Applications on the device for the user to select which application to use.
I would like to automate this flow but i can not see how I can automate clicking on the Application Picker that Android presents. I presume this is because it is outside of my application.
I tried using Android Studio's "Record Expresso Test", I performed the following test steps
click on the action which sends my image to an app on the device (action1)
saw the Android Application picker appear and chose photos
Clicked back to close photos app and go back to my app
click on a different action in my app (action2)
I see in the recorded test code for steps 1 and 4 above, but nothing for 2 and 3. Therefore it makes me think that Expresso can not be used for this particular test flow.
Does anyone know how I could test this flow using Expresso?
EDIT:
Thank you to "John O'Reilly" for recommending UI Automator. I can see that I can use the UI Automator code within my Expresso test successfully. However I am having trouble writing a precise verification of the Application Selector.
The selector will have a title of "Open With". Using Android Device Monitor I can see the hierarchy of objects as illustrated below.
Some classes and IDs are internal so I can not search on those things. I do not want to code to look for a specific application as when the test is run on another machine it may not have that application. I just need to verify that the application picker has been displayed.
// the app selector has a FrameLayout as one of its parent views, and a child Text View which has the "Open With" title
UiObject labelOnly = new UiObject(new UiSelector()
.className("android.widget.FrameLayout")
.childSelector(new UiSelector()
.className("android.widget.TextView")
.text(openWithLabel)
)
);
boolean labelOnly_exists = labelOnly.exists();
// the app selector has a FrameLayout as one of its parent views, and a child ListView (containing the apps)
UiObject listOnly = new UiObject(new UiSelector()
.className("android.widget.FrameLayout")
.childSelector(new UiSelector()
.className("android.widget.ListView")
)
);
boolean listOnly_exists = listOnly.exists();
// I can use the listView to search for a specific app, but this makes the tests fragile if a different device does not have that app installed
UiObject listAndAppName = new UiObject(new UiSelector()
.className("android.widget.ListView")
.instance(0)
.childSelector(new UiSelector()
.text("Photos")));
boolean listAndAppName_exists = listAndAppName.exists();
How could i write a statement that verifies that what is on the screen is the application picker? I was hoping maybe have a selector that searches for a FrameLayout which has a child textView containing "Open With" and also contains a child ListView. With these 2 checks together it should identify only the application picker.
The credit for the answer to this question should go to John O'Reilly who pointed me to use the UI Automator.
I resolved the issue of checking what Android screen is invoked when my test clicks on an action by just checking there is a TextView on the screen with the title I am expecting. Its not perfect as this will pass if there is any TextView on the screen with the text so does not precisely check its the application picker.
However, for my test this should be enough of a check as my app (which would be behind the app picker) should not have a TextView with the expected title, so if the title is found its pretty much likely to be the application picker.
public static boolean verifyAndroidScreenTitlePresent(String title) {
UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
UiObject titleTextUI = new UiObject(new UiSelector()
.className("android.widget.TextView")
.text(title)
);
boolean titleExists = titleTextUI.exists();
// close the app selector to go back to our app so we can carry on with Expresso
mDevice.pressBack();
return titleExists;
}
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.
Testing a Progressive Web Application.
When I start the app in airplane mode, I get an unexpected startup/splash experience (Android/Chrome).
Launch from Home Screen Experience
I see a white screen, followed by a brief flash of the "offline dinosaur" before the app successfully starts and all is well. The startup time is longer than I expected, especially after testing with Chrome Devtools on a laptop, where startup is near instant.
Since it is a little tricky to debug where this time is being spent (especially in the “service-worker-not-running” case), it would be helpful to have some baseline knowledge:
Launch from Browser Experience
Just a brief flash of the "offline dinosaur" before the app successfully starts. Starts much faster.
Questions
What is the expected startup time and experience on Android/Chrome?
Is the experience described above just the current state of things (11/2015)?
Is there any way to specify the startup (splash) experience for Chrome? (I'm aware of background color and 144x144 icon in app manifest for splash, but for Opera only)
First time PWA for me, so any information on this would be helpful.
My platform:
Samsung GS 5,
Android 5.0,
Chrome 46.0.2490.76
The reason for the existence of the splash screen is because on mobile it can take over a second to start the render process so we paint something (the background colour and icons) util you have a first paint generated by your app.
If you are seeing a white screen on startup it might be because you added to the homescreen prior to Chrome landing (46) the splash screen feature. Some things to lookout for:
Ensure your manifest has a short_name and name
Ensure your start_url is in the same scope as a SW that is registered on the page
Have good icons in the manifest ideally > 192px
Set background_color in the manifest to the color of your background on the page (ideally.) This will ensure that the splash screen is the expected colour of your site.
You shouldn't see the offline dinosaur at all, even when you are in aeroplane mode. Airhorner should represent the ideal experience: Blue splash screen with an icon that morphs into the display of the app.
re: Icons - I recommend actually 192px icon or higher.
Regarding the offline-dino flash:
I was using sw-toolbox and performing asynchronous work to setup route handlers at worker startup. This caused an offline dino flash when the application was offline and starting up.
To avoid that, set a sw-toolbox default handler that waits for the asynchronous route handler setup to complete.
var toolbox = require('sw-toolbox');
var setupPromise = someAsyncHandlerSetup()
.then(function () {
// make default handler temporary, allows other fetch handlers (like sw-precache, for example)
toolbox.router.default = null;
});
// until the async handler setup is done, provide a default handler
// to avoid an offline-dino flash when starting up while offline.
toolbox.router.default = function defaultHandler (request) {
return setupPromise.then(function () {
var handler = toolbox.router.match(request);
if (handler) {
return handler(request);
}
throw new Error('default handler could not handle ' + request.url);
});
};
Android added Presentation in API Level 17 (Android 4.2) to support displaying content on an external Display, such as as TV or monitor connected via HDMI, MHL, Miracast, or SlimPort. However, Presentation extends Dialog, and so it is only usable from an Activity.
And, as far as I knew, that was the end of the story.
However, this StackOverflow answer hints at a possible way to use an external Display from a Service, by means of createDisplayContext() and WindowManager created from that Context. Then, the addView() method on that WindowManager should render the View onto the indicated Display. If this can be made to work, it really opens the door for interesting uses of external displays, such as playing a video on a TV while being able to use unrelated apps (e.g., a Web browser) on the device's own touchscreen.
However, that answer glosses over a key detail: how to set up the WindowManager.LayoutParams for the addView() call. In particular, there are a dizzying array of possible TYPE_ values for the type field. I have crashed in two attempts, though with different messages:
TYPE_APPLICATION results in android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
TYPE_APPLICATION_MEDIA results in android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
For example, here is my WindowManager.LayoutParams for the second scenario above:
WindowManager.LayoutParams p=
new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
0,
0,
WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
0, PixelFormat.OPAQUE);
Reading through the docs for type suggest that none of the TYPE_APPLICATION will be correct, as I do not have a token. Moreover, the point behind this exercise is to not have a token, as least as far as I can tell, as the Service is supposed to run independently from any UI.
If you look at the source to Presentation, it defers the WindowManager work to Dialog, which uses com.android.internal.policy.PolicyManager, which quickly dead-ends in an IPolicy. An SDK app does not have access to PolicyManager, anyway.
Has anyone gotten the createDisplayContext() approach to work from a Service? If so, what did you use for the type (or, more generally, for the WindowManager.LayoutParams in general)? Bonus points for a solution that does not involve some icky permission. :-)
Thanks!
TYPE_SYSTEM_ALERT type used conjunctly with the SYSTEM_ALERT_WINDOW permission should work.
It makes sense that launching a dialog from a service
requires "icky" permissions, it basically allows you to draw over other apps :)