/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.filemanager.test;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.Beta;
import android.support.test.rule.UiThreadTestRule;
import android.util.Log;
import static android.support.test.internal.util.Checks.checkNotNull;
/**
* This rule provides functional testing of a single activity. The activity under test will be
* launched before each test annotated with
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> and before
* methods annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>. It
* will be terminated after the test is completed and methods annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a> are
* finished. During the duration of the test you will be able to manipulate your Activity directly.
*
* @param <T> The activity to test
*/
@Beta
public class TestActivityTestRule<T extends Activity> extends UiThreadTestRule {
private static final String TAG = "TAInstrRule";
private final Class<T> mActivityClass;
private Instrumentation mInstrumentation;
private boolean mInitialTouchMode = false;
private boolean mLaunchActivity = false;
private T mActivity;
/**
* Similar to {@link #TestActivityTestRule(Class, boolean, boolean)} but with "touch mode" disabled.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @see TestActivityTestRule#TestActivityTestRule(Class, boolean, boolean)
*/
public TestActivityTestRule(Class<T> activityClass) {
this(activityClass, false);
}
/**
* Similar to {@link #TestActivityTestRule(Class, boolean, boolean)} but defaults to launch the
* activity under test once per
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html"><code>Test</code></a> method.
* It is launched before the first
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>
* method, and terminated after the last
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a>
* method.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
* @see TestActivityTestRule#TestActivityTestRule(Class, boolean, boolean)
*/
public TestActivityTestRule(Class<T> activityClass, boolean initialTouchMode) {
this(activityClass, initialTouchMode, true);
}
/**
* Creates an {@link TestActivityTestRule} for the Activity under test.
*
* @param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* @param initialTouchMode true if the Activity should be placed into "touch mode" when started
* @param launchActivity true if the Activity should be launched once per
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html">
* <code>Test</code></a> method. It will be launched before the first
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html">
* <code>Before</code></a> method, and terminated after the last
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html">
* <code>After</code></a> method.
*/
public TestActivityTestRule(Class<T> activityClass, boolean initialTouchMode,
boolean launchActivity) {
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
mLaunchActivity = launchActivity;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}
/**
* Override this method to set up Intent as if supplied to
* {@link android.content.Context#startActivity}.
* <p>
* The default Intent (if this method returns null or is not overwritten) is:
* action = {@link Intent#ACTION_MAIN}
* flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK}
* All other intent fields are null or empty.
*
* @return The Intent as if supplied to {@link android.content.Context#startActivity}.
*/
protected Intent getActivityIntent() {
return new Intent(Intent.ACTION_MAIN);
}
/**
* Override this method to execute any code that should run before your {@link Activity} is
* created and launched.
* This method is called before each test method, including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>.
*/
protected void beforeActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {@link Activity} is
* launched, but before any test code is run including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>.
* <p>
* Prefer
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>
* over this method. This method should usually not be overwritten directly in tests and only be
* used by subclasses of TestActivityTestRule to get notified when the activity is created and
* visible but test runs.
*/
protected void afterActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {@link Activity} is
* finished.
* This method is called after each test method, including any method annotated with
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a>.
*/
protected void afterActivityFinished() {
// empty by default
}
/**
* @return The activity under test.
*/
public T getActivity() {
if (mActivity == null) {
Log.w(TAG, "Activity wasn't created yet");
}
return mActivity;
}
@Override
public Statement apply(final Statement base, Description description) {
return new ActivityStatement(super.apply(base, description));
}
/**
* Launches the Activity under test.
* <p>
* Don't call this method directly, unless you explicitly requested not to launch the Activity
* manually using the launchActivity flag in
* {@link TestActivityTestRule#TestActivityTestRule(Class, boolean, boolean)}.
* <p>
* Usage:
* <pre>
* @Test
* public void customIntentToStartActivity() {
* Intent intent = new Intent(Intent.ACTION_PICK);
* mActivity = mActivityRule.launchActivity(intent);
* }
* </pre>
* @param startIntent The Intent that will be used to start the Activity under test. If
* {@code startIntent} is null, the Intent returned by
* {@link TestActivityTestRule#getActivityIntent()} is used.
* @return the Activity launched by this rule.
* @see TestActivityTestRule#getActivityIntent()
*/
public T launchActivity(@Nullable Intent startIntent) {
// set initial touch mode
mInstrumentation.setInTouchMode(mInitialTouchMode);
final String targetPackage = mInstrumentation.getContext().getPackageName();
// inject custom intent, if provided
if (null == startIntent) {
startIntent = getActivityIntent();
if (null == startIntent) {
Log.w(TAG, "getActivityIntent() returned null using default: " +
"Intent(Intent.ACTION_MAIN)");
startIntent = new Intent(Intent.ACTION_MAIN);
}
}
startIntent.setClassName(targetPackage, mActivityClass.getName());
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, String.format("Launching activity %s",
mActivityClass.getName()));
beforeActivityLaunched();
// The following cast is correct because the activity we're creating is of the same type as
// the one passed in
mInstrumentation.getContext().startActivity(startIntent);
mInstrumentation.waitForIdleSync();
afterActivityLaunched();
return mActivity;
}
// Visible for testing
void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = checkNotNull(instrumentation, "instrumentation cannot be null!");
}
void finishActivity() {
if (mActivity != null) {
mActivity.finish();
mActivity = null;
}
}
/**
* <a href="http://junit.org/apidocs/org/junit/runners/model/Statement.html">
* <code>Statement</code></a> that finishes the activity after the test was executed
*/
private class ActivityStatement extends Statement {
private final Statement mBase;
public ActivityStatement(Statement base) {
mBase = base;
}
@Override
public void evaluate() throws Throwable {
try {
if (mLaunchActivity) {
mActivity = launchActivity(getActivityIntent());
}
mBase.evaluate();
} finally {
finishActivity();
afterActivityFinished();
}
}
}
}