package org.edx.mobile.view; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.AnimRes; import android.support.v4.app.FragmentManager; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.TextView; import org.edx.mobile.R; import org.edx.mobile.base.BaseFragmentActivity; import org.junit.Test; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowActivity; import org.robolectric.shadows.ShadowApplication; import org.robolectric.util.ActivityController; import org.robolectric.util.Scheduler; import static org.assertj.android.api.Assertions.assertThat; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.junit.Assume.assumeThat; import static org.junit.Assume.assumeTrue; // TODO: Test network connectivity change events too, after we manage to mock them public abstract class BaseFragmentActivityTest extends UiTest { /** * Method for defining the subclass of {@link BaseFragmentActivity} that * is being tested. Should be overridden by subclasses. * * @return The {@link BaseFragmentActivity} subclass that is being tested */ protected Class<? extends BaseFragmentActivity> getActivityClass() { return BaseFragmentActivity.class; } /** * Method for constructing the {link Intent} to be used to start the * {link Activity} instance. Should be overridden by subclasses to * attach any additional data to be passed. * * @return The {@link Intent} used to start the {link Activity} */ protected Intent getIntent() { return new Intent(RuntimeEnvironment.application, getActivityClass()); } /** * Method for defining whether the activity has a drawer configured * * @return true if the drawer is configured */ protected boolean hasDrawer() { return false; } /** * Method for defining whether the transition animation is overridden * with custom animation on restart * * @return true if the transition animation is overridden on restart */ protected boolean appliesPrevTransitionOnRestart() { return false; } /** * Testing window content overlay hack for API level 18 */ @Test @Config(sdk = Build.VERSION_CODES.JELLY_BEAN_MR2) public void updateActionBarShadowTest() { BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).create().get(); // Get the content view View contentView = activity.findViewById(android.R.id.content); // Make sure it's a valid instance of a FrameLayout assumeThat(contentView, instanceOf(FrameLayout.class)); TypedValue tv = new TypedValue(); // Get the windowContentOverlay value of the current theme assumeTrue(activity.getTheme().resolveAttribute( android.R.attr.windowContentOverlay, tv, true)); // If it's a valid resource, confirm that is has been set as // the foreground drawable for the content view assumeTrue(tv.resourceId != 0); Drawable contentForeground = ((FrameLayout) contentView).getForeground(); assertEquals(tv.resourceId, Shadows.shadowOf( contentForeground).getCreatedFromResId()); } /** * Generic method for asserting pending transition animation * * @param shadowActivity The shadow activity * @param enterAnim The enter animation resource * @param exitAnim The exit animation resource */ private static void assertOverridePendingTransition(ShadowActivity shadowActivity, @AnimRes int enterAnim, @AnimRes int exitAnim) { assertEquals(enterAnim, shadowActivity .getPendingTransitionEnterAnimationResourceId()); assertEquals(exitAnim, shadowActivity .getPendingTransitionExitAnimationResourceId()); } /** * Testing overall lifecycle and setup */ @Test public void lifecycleTest() { ActivityController<? extends BaseFragmentActivity> controller = Robolectric.buildActivity(getActivityClass()).withIntent(getIntent()); BaseFragmentActivity activity = controller.get(); ShadowActivity shadowActivity = Shadows.shadowOf(activity); controller.create().start(); // Action bar state initialization ActionBar bar = activity.getSupportActionBar(); if (bar != null) { int displayOptions = bar.getDisplayOptions(); assertTrue((displayOptions & ActionBar.DISPLAY_HOME_AS_UP) > 0); assertTrue((displayOptions & ActionBar.DISPLAY_SHOW_HOME) > 0); assertTrue(null == activity.findViewById(android.R.id.home)); } controller.postCreate(null).resume().postResume().visible(); DrawerLayout mDrawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout); boolean hasDrawer = hasDrawer(); if (mDrawerLayout != null) { // NavigationFragment initialization FragmentManager fragmentManager = activity.getSupportFragmentManager(); fragmentManager.executePendingTransactions(); assert fragmentManager.findFragmentByTag("NavigationFragment") != null; if (hasDrawer) { CharSequence contentDescription = activity.findViewById( android.R.id.home).getContentDescription(); assertFalse(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertEquals(activity.getText(R.string.label_close), contentDescription); mDrawerLayout.openDrawer(GravityCompat.START); assertTrue(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertEquals(activity.getText(R.string.label_close), contentDescription); activity.closeDrawer(); assertFalse(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertEquals(activity.getText(R.string.label_close), contentDescription); mDrawerLayout.openDrawer(GravityCompat.START); assertTrue(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertEquals(activity.getText(R.string.label_close), contentDescription); Configuration config = activity.getResources().getConfiguration(); assertNotEquals(Configuration.ORIENTATION_LANDSCAPE, config.orientation); config.orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; activity.onConfigurationChanged(config); assertTrue(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertEquals(activity.getText(R.string.label_close), contentDescription); } } // Action bar home button assertTrue(shadowActivity.clickMenuItem(android.R.id.home)); if (hasDrawer) { assertNotNull(mDrawerLayout); assertTrue(mDrawerLayout.isDrawerOpen(GravityCompat.START)); assertTrue(shadowActivity.clickMenuItem(android.R.id.home)); assertFalse(mDrawerLayout.isDrawerOpen(GravityCompat.START)); activity.finish(); } assertThat(activity).isFinishing(); } /** * Testing options menu initialization */ @Test public void initializeOptionsMenuTest() { BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).setup().get(); Menu menu = Shadows.shadowOf(activity).getOptionsMenu(); assertNotNull(menu); MenuItem offlineItem = menu.findItem(R.id.offline); assertNotNull(offlineItem); assertThat(offlineItem).hasTitle(activity.getText(R.string.offline_text)); } /** * Generic method for asserting title setup * * @param activity The activity instance * @param title The expected title */ protected void assertTitle(BaseFragmentActivity activity, CharSequence title) { ActionBar bar = activity.getSupportActionBar(); assumeNotNull(bar); assumeNotNull(title); Typeface type = Typeface.createFromAsset( activity.getAssets(), "fonts/OpenSans-Semibold.ttf"); int titleId = activity.getResources().getIdentifier( "action_bar_title", "id", "android"); TextView titleTextView = (TextView) activity.findViewById(titleId); assumeNotNull(titleTextView); assertThat(titleTextView).hasCurrentTextColor( activity.getResources().getColor(R.color.edx_white)); assertEquals(type, titleTextView.getTypeface()); assertEquals(bar.getTitle(), title); } /** * Testing title setup */ @Test public void setTitleTest() { BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).create().get(); CharSequence title = "test"; activity.setTitle(title); assertTitle(activity, title); } /** * Generic method for asserting view animation method functionality * * @param view The animated view * @param trigger A {@link Runnable} that triggers the animation */ protected void assertAnimateLayouts(View view, Runnable trigger) { // The foreground scheduler needs to be paused so that the // temporary visibility of the animated View can be verified. Scheduler foregroundScheduler = ShadowApplication.getInstance() .getForegroundThreadScheduler(); boolean wasPaused = foregroundScheduler.isPaused(); if (!wasPaused) { foregroundScheduler.pause(); } assertThat(view).isGone(); trigger.run(); assertThat(view).isVisible(); Animation animation = view.getAnimation(); assertNotNull(animation); assertThat(animation.getStartTime()) .isLessThanOrEqualTo(AnimationUtils.currentAnimationTimeMillis()); assertThat(animation).hasStartOffset(0); foregroundScheduler.unPause(); assertThat(view).isGone(); if (wasPaused) { foregroundScheduler.pause(); } } /** * Testing view animation methods */ @Test public void animateLayoutsTest() { final BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).setup().get(); final View view = new View(activity); view.setVisibility(View.GONE); activity.addContentView(view, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); assertAnimateLayouts(view, new Runnable() { @Override public void run() { activity.animateLayouts(view); } }); activity.stopAnimation(view); assertThat(view).hasAnimation(null); } /** * Generic message for asserting show info method functionality * * @param activity The activity instance * @param message The message that is expected to be displayed * @param trigger A {@link Runnable} that triggers the showing of the * message */ protected void assertShowInfoMessage(final BaseFragmentActivity activity, final String message, final Runnable trigger) { final TextView messageView = (TextView) activity.findViewById(R.id.flying_message); assumeNotNull(messageView); assertAnimateLayouts(messageView, new Runnable() { @Override public void run() { trigger.run(); assertThat(messageView).hasText(message); } }); } /** * Testing show info method */ @Test public void showInfoMessageTest() { final BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).setup().get(); TextView messageView = new TextView(activity); messageView.setId(R.id.flying_message); messageView.setVisibility(View.GONE); activity.addContentView(messageView, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); assertThat(messageView).hasText(""); final String message = "test"; assertShowInfoMessage(activity, message, new Runnable() { @Override public void run() { assumeTrue(activity.showInfoMessage(message)); } }); } /** * Testing show info method */ @Test public void showOfflineAccessMessage() { final BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).setup().get(); View offlineView = new View(activity); offlineView.setId(R.id.offline_access_panel); offlineView.setVisibility(View.GONE); activity.addContentView(offlineView, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); assertAnimateLayouts(offlineView, new Runnable() { @Override public void run() { activity.showOfflineAccessMessage(); } }); } /** * Generic method for asserting next started activity along with * the custom transition animation override * * @param currentActivity The current activity * @param nextActivityClass The class of the newly started activity */ protected Intent assertNextStartedActivity(BaseFragmentActivity currentActivity, Class<? extends Activity> nextActivityClass) { ShadowActivity shadowActivity = Shadows.shadowOf(currentActivity); Intent intent = shadowActivity.getNextStartedActivity(); assertNotNull(intent); assertThat(intent).hasComponent(currentActivity, nextActivityClass); return intent; } /** * Generic method for asserting next started activity along with * the custom transition animation override * * @param currentActivity The current activity * @param nextActivityClass The class of the newly started activity * @param requestCode The request code */ protected Intent assertNextStartedActivityForResult( BaseFragmentActivity currentActivity, Class<? extends Activity> nextActivityClass, int requestCode) { ShadowActivity shadowActivity = Shadows.shadowOf(currentActivity); ShadowActivity.IntentForResult intentForResult = shadowActivity.getNextStartedActivityForResult(); assertNotNull(intentForResult); assertThat(intentForResult.intent).hasComponent( currentActivity, nextActivityClass); assertEquals(requestCode, intentForResult.requestCode); return intentForResult.intent; } /** * Testing method for enabling and disabling UI interaction */ @Test public void tryToSetUIInteractionTest() { BaseFragmentActivity activity = Robolectric.buildActivity(getActivityClass()) .withIntent(getIntent()).setup().get(); assertFalse(activity.tryToSetUIInteraction(true)); assertFalse(activity.tryToSetUIInteraction(false)); } }