-
-
Save nbarraille/03e8910dc1d415ed9740 to your computer and use it in GitHub Desktop.
/* | |
* The MIT License (MIT) | |
* | |
* Copyright (c) 2015 - Nathan Barraille | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
* | |
*/ | |
package net.slideshare.mobile.test.util; | |
import android.app.Activity; | |
import android.content.pm.ActivityInfo; | |
import android.support.test.espresso.UiController; | |
import android.support.test.espresso.ViewAction; | |
import android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorRegistry; | |
import android.support.test.runner.lifecycle.Stage; | |
import android.view.View; | |
import org.hamcrest.Matcher; | |
import java.util.Collection; | |
import static android.support.test.espresso.matcher.ViewMatchers.isRoot; | |
/** | |
* An Espresso ViewAction that changes the orientation of the screen | |
*/ | |
public class OrientationChangeAction implements ViewAction { | |
private final int orientation; | |
private OrientationChangeAction(int orientation) { | |
this.orientation = orientation; | |
} | |
@Override | |
public Matcher<View> getConstraints() { | |
return isRoot(); | |
} | |
@Override | |
public String getDescription() { | |
return "change orientation to " + orientation; | |
} | |
@Override | |
public void perform(UiController uiController, View view) { | |
uiController.loopMainThreadUntilIdle(); | |
final Activity activity = (Activity) view.getContext(); | |
activity.setRequestedOrientation(orientation); | |
Collection<Activity> resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED); | |
if (resumedActivities.isEmpty()) { | |
throw new RuntimeException("Could not change orientation"); | |
} | |
} | |
public static ViewAction orientationLandscape() { | |
return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); | |
} | |
public static ViewAction orientationPortrait() { | |
return new OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); | |
} | |
} |
I updated the Gist with MIT license, feel free to use it in your projects
This changes the orientation of a screenshot captured by spoon, but it doesn't actually change the orientation of the UI to match. So I'm getting my portrait layout in a landscape screenshot. I'm not registering for configChanges in my manifest either. :(
I found this to actually work: mActivityRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
SystemClock.sleep(100);
mandracheck i tried your method and am getting the following error
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.app.ActionBarDrawerToggle.onConfigurationChanged(android.content.res.Configuration)' on a null object reference
at com.netiq.mobileaccessforandroid.MainActivity.onConfigurationChanged(MainActivity.java:1034)
at android.app.ActivityThread.performConfigurationChanged(ActivityThread.java:4174)
at android.app.ActivityThread.handleConfigurationChanged(ActivityThread.java:4247)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1454)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Test running failed: Instrumentation run failed due to 'java.lang.NullPointerException'
On importing your code Android Studio is not finding ActivityLifecycleMonitorRegistry. I'm pretty sure it has to do with my build.gradle file, but I haven't been able to reconcile it so that it imports "android.support.test.internal.runner.lifecycle.ActivityLifecycleMonitorRegistry;"
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:support-v4:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.google.code.gson:gson:2.4'
compile 'com.google.protobuf:protobuf-java:2.4.1'
compile files('libs/google-http-client-1.17.0-rc.jar')
compile files('libs/google-http-client-android-1.17.0-rc.jar')
compile files('libs/google-http-client-jackson-1.17.0-rc.jar')
compile files('libs/google-oauth-client-1.17.0-rc.jar')
compile files('libs/httpclient-4.0.1.jar')
compile files('libs/jackson-core-2.1.3.jar')
compile files('libs/jsr305-1.3.9.jar')
compile files('libs/httpcore-4.0.1.jar')
compile files('libs/commons-logging-1.1.1.jar')
compile files('libs/jackson-core-asl-1.9.11.jar')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') {
// Necessary if your app targets Marshmallow (since Espresso
// hasn't moved to Marshmallow yet)
exclude module: 'support-annotations'
}
androidTestCompile('com.android.support.test:runner:0.4.1') {
// Necessary if your app targets Marshmallow (since the test runner
// hasn't moved to Marshmallow yet)
exclude module: 'support-annotations'
}
androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.1') {
// Necessary if your app targets Marshmallow (since Espresso
// hasn't moved to Marshmallow yet)
exclude module: 'support-annotations'
}
On importing your code Android Studio is not finding ActivityLifecycleMonitorRegistry.
Same here. Works great without the additional check, though. ;)
I'm getting java.lang.ClassCastException: android.view.ContextThemeWrapper cannot be cast to android.app.Activity
when calling it with onView(isRoot()).perform(orientationLandscape());
The root view's Context doesn't seem to be an Activity…
The ClassCastException
can occur when using AppCompatActivity
- this tends to wrap the Context
in a ContextWrapper
. You may also see a TintContextWrapper
dependent on what you are doing. To get the Activity
you need to unwrap the context using ContextWrapper.getBaseContext()
until you get to an Activity
. NOTE: Activity
itself extends ContextThemeWrapper
This uses recursion which is not obviously ideal but should give you an idea:
public static Activity getActivity(Context context) {
if (context instanceof Activity) {
return (Activity) context;
}
if (context instanceof ContextWrapper) {
return getActivity(((ContextWrapper) context).getBaseContext());
}
return null;
}
Is there an advantage using this instead of e.g. simply activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); ?
Although it works, it was very slow in my case. It does not wait for the orientation to be changed. Same goes for
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
@mattmook there's nothing keeping you from replacing recursion with a loop:
public static Activity getActivity(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
Thank you very much for so beautiful code. But how to perform Espresso tests on the second Activity? It seems that my tests are still applied on the old instance.
I can use SystemClock.sleep(1000); But is there a way to avoid it? :)
Thank you very much for your great snippet! :)
In case, this doesn't work for you because of (since Android 7.0?) com.android.internal.policy.DecorContext
's baseContext is the Application not an Activity, try out a variation of the following code.
activity = getActivity(view.getContext());
if (activity == null && view instanceof ViewGroup) {
ViewGroup v = (ViewGroup)view;
int c = v.getChildCount();
for (int i = 0; i < c && activity == null; ++i) {
activity = getActivity(v.getChildAt(i).getContext());
}
}
Building upon @TWiStErRob and @simonracz answers I've created a fork of this ViewAction which works fine with Android 7.0 and lower.
Kotlin version:
import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import org.hamcrest.Matcher
import android.content.pm.ActivityInfo
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import android.app.Activity
import androidx.test.runner.lifecycle.Stage
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 - Nathan Barraille
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
class OrientationChangeAction(private val orientation: Int): ViewAction {
companion object {
fun orientationLandscape(): ViewAction = OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
fun orientationPortrait(): ViewAction = OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
override fun getDescription(): String = "change orientation to $orientation"
override fun getConstraints(): Matcher<View> = isRoot()
override fun perform(uiController: UiController, view: View) {
uiController.loopMainThreadUntilIdle()
var activity = getActivity(view.context)
if (activity == null && view is ViewGroup) {
val c = view.childCount
var i = 0
while (i < c && activity == null) {
activity = getActivity(view.getChildAt(i).context)
++i
}
}
activity!!.requestedOrientation = orientation
}
private fun getActivity(context: Context): Activity? {
var context = context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = (context as ContextWrapper).baseContext
}
return null
}
}
@nbarraille Does this work when we use config annotation?
I tried to run Espresso test case, it worked, but not with Robolectric. I guess I might facing robolectric issue!!
@Config(
qualifiers = "port-xxhdpi"
)
please do add a license