I'm writing test to my project with Robolectric-2.3.
I'd like to test my UI properties such as Views visibility. The actions of showing/hiding views are wrapped into Animations. How to test it ?
I tried to use ShadowSystemClock.sleep() method to wait until animation ends but it doesn't seem to work as I expected.
#Test
public void testHideSearch() throws Exception {
mListFragment.hideSearch(); //<--- animation launched here
sleep(1000);
View searchEditText = mListFragment.getView().findViewById(R.id.filterEditText);
assertFalse(searchEditText.getVisibility() == View.VISIBLE);
}
What is the correct approach to the issue ?
Try using this instead of sleep:
Robolectric.getUiThreadScheduler.advanceBy(1000);
If you use Animator's your approach should work, but if you use Animations, you'll need to use ShadowAnimation instead. Assuming the actual visibility of your view is changed in onAnimationEnd(), something like the following should work:
ShadowAnimation animation = Robolectric.shadowOf(searchEditText.getAnimation());
animation.invokeEnd();
Related
I need to check the presence of the button that is located below. To do this, scroll through the page
#Test
public void checkMoneyBackButton() throws Exception {
onView(withId(R.id.btnAuthLogin)).perform(click());
Thread.sleep(10000);
onView(withId(R.id.etSessionKey1)).perform(typeText("1234"));
closeSoftKeyboard();
SystemClock.sleep(45000);
ViewInteraction viewInteraction = Espresso.onView(withText("**** 0431"));
viewInteraction.perform(click());
Thread.sleep(3000);
onView(withId(R.id.cardContainer))
.perform(swipeUp());
onView(withId(R.id.statementMoneyBack)).check(matches(isDisplayed()));
}
But using this code, I get an error:
Error performing 'fast swipe' on view 'with id:...
Hacky style, but should do the trick:
`` // Scroll down the view with for loop (as many times you need to iterate)
for(int i=0;i<=20;i++){
onView(withText("Use_here_any_text_from_the_view")).perform(swipeUp());
}
``
Add Barista into your Espresso and use this command. Barista will help you for some action in your automation (Especially if your background is not engineering like me).
scrollTo(R.id.far_away_widget);
scrollTo(R.string.text);
scrollTo("A widget with this text");
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
While writing a Robolectric unit test, I noticed my getVisibility() call returned 0 (VISIBLE) after calling fab.hide(), so I assumed it was due to animation and to test it out, added a delayed check. Surprisingly it has also returned VISIBLE. On the actual device it works as expected and returns the correct values.
EDIT: just to clarify I'm using the FAB from the design support library.
My test code is really simple:
fab.performClick();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertThat(fab.isShown()).isFalse();
Code under test:
mActionBunnot.hide();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
boolean shown = mActionBunnot.isShown();
Log.d(TAG,""+shown);
}
},2000);
When run through Robolectric, both here and in the test, isShown returns true
You can also .isShown() method to get the visibility.
I know it is a bit late, but maybe useful for other people.
Robolectric executes all operations on one single thread. In the past this happened synchronously. Since version 4.3 Robolectric has the Looper PAUSED mode, which improves this behaviour as described in this blog: http://robolectric.org/blog/2019/06/04/paused-looper/
I am new to Android development. I am currently building an application that must flash different colors when a button is clicked. When the button is clicked I call a function. This function loops through a list of items and basically at the moment attempts to show blue and green after each other a couple of times. Problem is that it only shows the last color. And this only happens when the thread has exited the method. In the example below it is blue. I have noticed with some of the work I have done that screen changes reflects once the program have left the current method where the loop occurs and sometimes even longer after that. Below is the code :
//Method that sets the color
public void SetVisualLayoutColor(int Color)
{
linearLayout.setBackgroundColor(Color);
linearLayout.invalidate();
linearLayout.refreshDrawableState();
}
//Method that loops and calls above method to set color
public boolean ShowRepititive()
{
boolean successfull = false;
try
{
boolean isGreen = true;
//for (TimingItem timingItem : items) {
for (int i=0;i<=10;i++) {
if (isGreen) {
SetVisualLayoutColor(Color.BLUE);
isGreen = false;
} else
{
SetVisualLayoutColor(Color.BLUE);
isGreen = true;
}
}
successfull = true;
} catch (Exception e)
{
Log.e("Repeating Flash", "showFlashRepititive: ", e);
successfull = false;
}
return successfull;
}
Is there some way to get past this or to force the changes to occur timeously ?
Thank you in advance.
Unless you explicitly run your code on a different thread, the code you write in an Activity, Fragment, or View will run on the UI thread.
When you are performing work on the UI thread, the UI cannot update. The UI only updates after your code has finished executing. Thus your for loop blocks any UI updates until it completes, showing the last color you set.
A more appropriate pattern to use would be to use an animation to change the color
Here is an example of a very basic animation that does a cross-fade between two background colors:
ObjectAnimator.ofObject(view, "backgroundColor", new ArgbEvaluator(), 0xFF0000, 0x00FF00)
.start();
Obviously this won't achieve the flashing effect you are looking for, but you can define your own animations in a similar manner to achieve the desired effect.
As Tanis.7x said, you can't create the desired animation while your code is executed in the main thread. All potential solutions have to have a background Thread and a way for it to communicate with your UI thread. (see AsyncTasks,Threads,Handlers)
An alternative to ObjectAnimator that can work in your specific case is TransitionDrawable. It's not that powerful but if it suits your needs, it's preferable since it's API 1 compatible:
You need to store a transitioncolors.xml in your drawables folder with your transition definition:
<?xml version="1.0" encoding="UTF-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#color/green"/>
<item android:drawable="#color/blue"/>
</transition>
and then call:
ImageView view = (ImageView) findViewById(R.id.transitionId);
view.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
TransitionDrawable transition = (TransitionDrawable) v.getBackground();
transition.startTransition(3000);
});
where R.id.transitionId is:
<ImageView
android:id="#+id/transitionId"
android:background="#drawable/transitioncolors"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
I did some more research after I read Tanis.7x and Sevle's post, which put me in the right direction. For some reason the Object animator did not worked when I ran it and the Transition does not give me the flexibility I wanted. So I came across the ValueAnimator, which worked perfectly for me. I created my own class that extended the ValueAnimator and within the class I control how long each animation must be and also other aspects like running indefinitely until the user interrupts it on the screen. Thanks a lot guys, I really appreciate the help.
EDIT: tl;dr: WebView appears as white box, even though I appear to be setting it up correctly, and indeed it does work the first two times, but fails subsequently)
EDIT: Video showing the problem in action...
I have the following bit of code which inflates a view (Which contains a WebView) from the xml which defines it:
private void createCard(ViewGroup cvFrame, Card card) {
//... setup vairables...
cvFrame.clearDisappearingChildren();
cvFrame.clearAnimation();
try {
View cv = LayoutInflater.from(getBaseContext()).inflate(R.layout.card_back_view,
cvFrame, true);
cv.setBackgroundDrawable(Drawable.createFromStream(mngr.open(deckName + "_Card_back.png"), deckName));
TextView suit = (TextView)cv.findViewWithTag("card_back_suit");
//...setup text view for suit, this code works fine every time...
WebView title = (WebView)cv.findViewWithTag("card_back_title");
//This WebView doesn't appear to be the one which actually appears on screen (I can change settings till I'm blue in the face, with no effect)
if (title != null) {
title.setBackgroundColor(0x00000000);
title.loadData(titleText, "text/html", "UTF-8");
} else {
Log.e("CardView", "Error can't find title WebView");
}
} catch (IOException e) {
Log.e("CardView", "Error making cards: ", e);
}
}
When this method is called as part of the onCreate method in my Activity, the WebView contains the correct code, and is suitably transparent.
I have a gesture listener which replaces the contents of the ViewGroup with different content (It animates the top card off to the left, replaces the contents of the top card with card 2, puts the top card back, then replaces card 2 with card 3)
//Gesture listener event
ViewGroup cvFrame = (ViewGroup)findViewById(R.id.firstCard);
cardLoc++
cvFrame.startAnimation(slideLeft);
(onAnimationEnd code)
public void onAnimationEnd(Animation animation) {
if (animation == slideLeft) {
ViewGroup cvFrameOldFront = (ViewGroup)findViewById(R.id.firstCard);
ViewGroup cvFrameNewFront = (ViewGroup)findViewById(R.id.secondCard);
createCard(cvFrameOldFront, cards.get((cardLoc)%cards.size()));
createCard(cvFrameNewFront, cards.get((cardLoc+1)%cards.size()));
TranslateAnimation slideBack = new TranslateAnimation(0,0,0,0);
slideBack.setDuration(1);
slideBack.setFillAfter(true);
cvFrameOldFront.startAnimation(slideBack);
}
}
When the animation has happened and I replace the contents of the cards, the TextView suit is replaced fine and the code definitely passes through the code to replace the WebView contents, but for some reason I end up with a white rectangle the size and shape of the WebView, no content, no transparency.
If I change the WebView to a TextView, it's contents is replaced fine, so it's an issue that occurs only with the WebView control :S
Can anyone tell me why / suggest a fix?
It turns out the WebView doesn't get cleared down when using the LayoutInflater to replace the contents of a ViewGroup. The other controls all seem to get removed (or at least the findViewWithTag() returns the right reference for every other control). I've just added in the line cvFrame.removeAllViews() immediately before the LayoutInflater does it's stuff and that fixed the issue.
If anyone has any better explanation for this I'll throw the points their way otherwise they will just go into the ether...
By calling findViewById, you are getting a reference on the previously loaded webview do you ?
so the loadData call that fails is the second one you make on a single webview instance.
you may want to check this :
Android WebView - 1st LoadData() works fine, subsequent calls do not update display
It appears that loadData() won't load data twice... you may want to try WebView.loadDataWithBaseUri()
Hope that helps.
I had a similar problem loading several WebViews content.
It was because of a misusing of the pauseTimers function
The situation was : the first webView weren't needed anymore, conscientiously I wanted to pause it before to release it. Calling onPause() and pauseTimers()
pauseTimers being common to any web views, it broke every use of webviews occuring after that, there were displaying only white rectangles.
Maybe its not your problem here, but it's worth checking your not calling WebView.pauseTimers() somewhere.
To confirm your answer, the source code for LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot) does in fact internally calls root.addView() which attaches the newly inflated view at the end of the root's children instead of replacing them.
So the mystery now is why did your call to findViewWithTag() is returning the expected objects for your other widgets (which would be the top, most recently created instances), but for your WebView it was returning something else.
Is it possible that there is another object in your layout XML which shares the same "card_back_title" tag?
I was also wondering why you didn't use the more common findViewById() instead, but I am not sure whether it would make a difference.