package edu.vandy.view;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.os.SystemClock;
import android.support.annotation.StringRes;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.core.deps.guava.collect.Iterables;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import android.support.test.runner.lifecycle.Stage;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import edu.vandy.R;
import edu.vandy.common.Toaster;
import edu.vandy.common.Utils;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
@RunWith(AndroidJUnit4.class)
public class PalantiriActivityTest {
/**
* Logging tag.
*/
private static final String TAG = "PalantiriActivityTest";
/**
* Wait time constants.
*/
private final int CONFIG_TIMEOUT = 4000;
private final int SHUTDOWN_TIMEOUT = 6000;
/**
* Input values.
*/
private final int PALANTIRI = 4;
private final int BEINGS = 6;
private final int ITERATIONS = 5;
@Rule
public ActivityTestRule<PalantiriActivity> activityTestRule =
new ActivityTestRule<>(PalantiriActivity.class);
@Test
public void palantiriActivityTest() {
// Create and install a mock Toaster implementation.
MockToaster mockToaster = new MockToaster();
Utils.setMockToaster(mockToaster);
// Force config change.
setOrientationPortrait(CONFIG_TIMEOUT);
// Setup start and stop button view matchers which are used
// frequently throughout this test.
ViewInteraction startButton = onView(
allOf(withId(R.id.button_simulation),
withText(R.string.button_start_simulation),
isDisplayed()));
ViewInteraction stopButton = onView(
allOf(withId(R.id.button_simulation),
withText(R.string.button_stop_simulation),
isDisplayed()));
//
// Create a list of widget resource id / input value pairs
// to populate the starting activity values.
List<Pair<Integer, Integer>> pairs =
Stream.of(
Pair.create(R.id.edittext_number_of_palantiri,
PALANTIRI),
Pair.create(R.id.edittext_number_of_beings, BEINGS),
Pair.create(R.id.edittext_gazing_iterations, ITERATIONS)
).collect(Collectors.toList());
// Populate all the EditViews.
pairs.stream().forEach(
p -> onView(withId(p.first)).perform(
typeText(p.second.toString())));
// Check for expected values.
pairs.stream().forEach(
p -> onView(withId(p.first)).check(
matches(withText(
p.second.toString()))));
// Force a config change.
setOrientationLandscape(CONFIG_TIMEOUT);
// Make sure the fields still have the correct values.
pairs.stream().forEach(
p -> onView(withId(p.first)).check(
matches(withText(
p.second.toString()))));
// Start the simulation.
startButton.perform(click());
// Force a config change.
setOrientationLandscape(CONFIG_TIMEOUT);
// Check for a toast message that notifies the user that
// the simulation is being resumed after a config change
// (a timeout was already specified in the orientation call)
Assert.assertTrue(mockToaster.hasAnyMessageStartingWith(
R.string.toast_simulation_resume, 0));
// Force simulation to stop.
stopButton.perform(click());
// Check for expected stop simulation toast (should be immediate).
Assert.assertTrue(
mockToaster.hasAnyMessage(
String.format(activityTestRule.getActivity().getString(
R.string.toast_simulation_stopped, BEINGS),
0)));
// Now check for the toast that is displayed once the shutdown
// sequence has completed. Note that this can take a few seconds
// so we specify SHUTDOWN_TIMEOUT as the waiting period.
Assert.assertTrue(mockToaster.hasAnyMessageStartingWith(
R.string.toast_simulation_complete, SHUTDOWN_TIMEOUT));
// Clear the mock toast messages for the next test.
mockToaster.clear();
// Start a new simulation.
startButton.perform(click());
// Force a config change.
setOrientationPortrait(CONFIG_TIMEOUT);
// Check for a toast message that notifies the user that
// the simulation is being resumed after a config change
// (a timeout was already specified in the orientation call)
Assert.assertTrue(mockToaster.hasAnyMessageStartingWith(
R.string.toast_simulation_resume, 0));
// Force simulation to stop.
stopButton.perform(click());
// Check for expected stop simulation toast (should be immediate).
Assert.assertTrue(
mockToaster.hasAnyMessage(
String.format(activityTestRule.getActivity().getString(
R.string.toast_simulation_stopped, BEINGS),
0)));
// Now check for the toast that is displayed once the shutdown
// sequence has completed. Note that this can take a few seconds
// so we specify SHUTDOWN_TIMEOUT as the waiting period.
Assert.assertTrue(mockToaster.hasAnyMessageStartingWith(
R.string.toast_simulation_complete, SHUTDOWN_TIMEOUT));
// Clear the mock toast messages for the next test.
mockToaster.clear();
// Force a config change.
setOrientationLandscape(CONFIG_TIMEOUT);
// Return to previous activity.
pressBack();
// Ensure that the original EditView values are still the same.
pairs.stream().forEach(
p -> onView(withId(p.first)).check(
matches(withText(p.second.toString()))));
// Success!
Log.d(TAG, "The test was successful!");
}
public void setOrientationLandscape(int wait) {
Log.d(TAG, "palantiriActivityTest: setting orientation to LANDSCAPE");
setOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, wait);
}
public void setOrientationPortrait(int wait) {
Log.d(TAG, "palantiriActivityTest: setting orientation to PORTRAIT");
setOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, wait);
}
public void setOrientation(int orientation, int wait) {
try {
getCurrentActivity().setRequestedOrientation(orientation);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// Give the system app to settle.
SystemClock.sleep(wait);
}
private Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
final Activity[] activity = new Activity[1];
getInstrumentation().runOnMainSync(() -> {
java.util.Collection<Activity> activities =
ActivityLifecycleMonitorRegistry
.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = Iterables.getOnlyElement(activities);
});
return activity[0];
}
private class MockToaster implements Toaster {
/**
* Default sleep interval used while repeatedly checking for a toast
* message.
*/
private static final int WAIT_INTERVAL = 100;
/**
* List of toast messages received from the application since the the
* last clear() operation.
*/
final ArrayList<String> mMessages = new ArrayList<>();
/**
* Mock implementation simply adds passed toast message to an array.
*/
@Override
public void showToast(
Context context, String message, int duration) {
synchronized (mMessages) {
mMessages.add(message);
}
Toast.makeText(context, message, duration).show();
}
/**
* Returns true if the first and only received toast messages matches
* the passed message string within the specified time frame.
*/
boolean hasJustMessage(@StringRes int id, int waitTime) {
return hasJustMessage(
activityTestRule.getActivity().getString(id), waitTime);
}
/**
* Returns true if the first and only received toast messages matches
* the passed message string within the specified time frame.
*/
boolean hasJustMessage(String message, int waitTime) {
do {
synchronized (mMessages) {
if (mMessages.size() > 1) {
return false;
} else if (mMessages.size() == 1) {
return mMessages.contains(message);
}
}
int sleepTime = Math.min(WAIT_INTERVAL, waitTime);
SystemClock.sleep(sleepTime);
waitTime -= sleepTime;
} while (waitTime >= 0);
return false;
}
/**
* Returns true if the specified string exactly matches any posted toast
* messages. Non-matching toast messages that may also be received
* before or after the expected message.
*/
boolean hasAnyMessage(@StringRes int id, int waitTime) {
return hasAnyMessage(
activityTestRule.getActivity().getString(id),
waitTime);
}
/**
* Returns true if the specified string exactly matches any posted toast
* messages within the specified wait time. Ignores any additional
* non-matching toast messages that may also be received before or after
* the expected message.
*/
boolean hasAnyMessage(String message, int waitTime) {
while (waitTime >= 0) {
synchronized (mMessages) {
if (hasAnyMessage(message)) {
return true;
}
}
int sleepTime = Math.min(WAIT_INTERVAL, waitTime);
SystemClock.sleep(sleepTime);
waitTime -= sleepTime;
}
return false;
}
/**
* Returns true if the specified string has been displayed as a toast
* message. Ignores any additional non-matching toast messages that may
* also be received before.or after the expected message.
*/
boolean hasAnyMessage(String message) {
synchronized (mMessages) {
for (String msg : mMessages) {
if (msg.equals(message)) {
return true;
}
}
}
return false;
}
/**
* Returns true if the specified string resource matches an posted toast
* message withing the specified wait time. Ignores any additional
* non-matching toast messages that may also be received before or after
* the expected message.
*/
boolean hasAnyMessageStartingWith(@StringRes int id, int waitTime) {
return hasAnyMessageStartingWith(
activityTestRule.getActivity().getString(id),
waitTime);
}
/**
* Returns true if the specified string matches an posted toast message
* withing the specified wait time. Ignores any additional non-matching
* toast messages that may also be received before or after the expected
* message.
*/
boolean hasAnyMessageStartingWith(String message, int waitTime) {
while (waitTime >= 0) {
synchronized (mMessages) {
if (hasAnyMessageStartingWith(message)) {
return true;
}
}
int sleepTime = Math.min(WAIT_INTERVAL, waitTime);
SystemClock.sleep(sleepTime);
waitTime -= sleepTime;
}
return false;
}
/**
* Returns true if the specified string has already been posted. Ignores
* any additional non-matching toast messages that may also be received
* before or after the expected message.
*/
boolean hasAnyMessageStartingWith(String message) {
synchronized (mMessages) {
for (String msg : mMessages) {
if (msg.startsWith(message)) {
return true;
}
}
}
return false;
}
/**
* Clears any messages accumulated in the message array.
*/
void clear() {
synchronized (mMessages) {
mMessages.clear();
}
}
}
}