/***************************************************************************
* Copyright 2006-2016 by Christian Ihle *
* contact@kouchat.net *
* *
* This file is part of KouChat. *
* *
* KouChat is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* KouChat is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with KouChat. *
* If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
package net.usikkert.kouchat.android.util;
import static junit.framework.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import net.usikkert.kouchat.android.R;
import net.usikkert.kouchat.android.chatwindow.AndroidUserInterface;
import net.usikkert.kouchat.android.controller.MainChatController;
import net.usikkert.kouchat.android.controller.PrivateChatController;
import net.usikkert.kouchat.misc.User;
import net.usikkert.kouchat.testclient.TestUtils;
import net.usikkert.kouchat.util.Tools;
import com.robotium.solo.Solo;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.test.ActivityInstrumentationTestCase2;
import android.test.InstrumentationTestCase;
import android.text.Layout;
import android.text.TextPaint;
import android.util.DisplayMetrics;
import android.view.KeyEvent;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* Utilities for tests.
*
* @author Christian Ihle
*/
public final class RobotiumTestUtils {
private RobotiumTestUtils() {
}
/**
* Quits the application by using the quit menu item. Works from any activity.
*
* @param solo The solo tester.
*/
public static void quit(final Solo solo) {
goHome(solo);
openMenu(solo);
solo.clickOnText("Quit");
}
/**
* Returns to the main chat.
*
* @param solo The solo tester.
*/
public static void goHome(final Solo solo) {
solo.sleep(500);
solo.goBackToActivity(MainChatController.class.getSimpleName());
solo.sleep(500);
}
/**
* Opens the overflow menu in the action bar.
*
* @param solo The solo tester.
*/
public static void openMenu(final Solo solo) {
solo.clickOnView(solo.getView(R.id.mainChatMenu));
}
/**
* Adds a line of text to the first edittext field, and presses enter.
*
* @param solo The solo tester.
* @param text The line of text to write.
*/
public static void writeLine(final Solo solo, final String text) {
solo.enterText(0, text);
solo.sendKey(KeyEvent.KEYCODE_ENTER);
}
/**
* Writes the text to the field with current focus.
*
* <p>This can be used when it's important not to change the state of the view with focus.
* Using the regular method to enter text will put the view in a strange state where the cursor
* disappears, and pressing 'enter' changes focus to the next view instead of adding a new line.</p>
*
* @param instrumentation The test instrumentation.
* @param text The text to write.
*/
public static void writeText(final Instrumentation instrumentation, final String text) {
// Send one character at the time, with some sleep, to hack around some weird issue where sometimes
// the characters end up in the wrong order
for (int i = 0; i < text.length(); i++) {
final String character = String.valueOf(text.charAt(i));
instrumentation.sendStringSync(character);
Tools.sleep(15);
}
}
/**
* Gets a textview containing the given text.
*
* @param solo The solo tester.
* @param text The text to look for in textviews.
* @return The textview with the text.
* @throws IllegalArgumentException If no textview was found with the given text.
*/
public static TextView getTextViewWithText(final Solo solo, final String text) {
final List<TextView> currentTextViews = solo.getCurrentViews(TextView.class);
for (final TextView currentTextView : currentTextViews) {
if (currentTextView.getClass().equals(TextView.class) &&
currentTextView.getText().toString().contains(text)) {
return currentTextView;
}
}
throw new IllegalArgumentException("Could not find TextView with text: " + text);
}
/**
* Gets all the lines of text from a textview.
*
* @param fullText The full text from the textview. This is a separate parameter to avoid memory issues
* with repeated calls to {@link TextView#getText()}.
* @param textView The textview to get all the lines of text from.
* @return All the lines if text in the textview. Each list item is one line.
*/
public static List<String> getAllLinesOfText(final String fullText, final TextView textView) {
final List<String> allLines = new ArrayList<String>();
final Layout layout = textView.getLayout();
final int lineCount = layout.getLineCount();
for (int currentLineNumber = 0; currentLineNumber < lineCount; currentLineNumber++) {
allLines.add(getLineOfText(fullText, currentLineNumber, layout));
}
return allLines;
}
/**
* Gets the text on the given line from the full text of a textview.
*
* @param fullText The full text from a textview.
* @param lineNumber The line number in the textview to get the text from.
* @param layout The layout of the textview.
* @return The text found on the given line.
*/
public static String getLineOfText(final String fullText, final int lineNumber, final Layout layout) {
final int lineStart = layout.getLineStart(lineNumber);
final int lineEnd = layout.getLineEnd(lineNumber);
return fullText.substring(lineStart, lineEnd);
}
/**
* Clicks on the given text. If the text spans multiple lines, the last line gets clicked.
*
* <p>Added because {@link Solo#clickOnText(String)} has issues with locating the text.
* It will often click the wrong place. Use this method instead.</p>
*
* @param solo The solo tester.
* @param textViewId Id of the textview with the text to click.
* @param scrollViewId Id of the scrollview that contains the textview.
* @param textToClick The text to click.
* @throws IllegalArgumentException If the text is not visible or not found.
*/
public static void clickOnText(final Solo solo, final int textViewId, final int scrollViewId, final String textToClick) {
final Point coordinatesForLine = getCoordinatesForText(solo, textViewId, scrollViewId, textToClick);
solo.clickOnScreen(coordinatesForLine.x, coordinatesForLine.y);
}
/**
* Long clicks on the given text. If the text spans multiple lines, the last line gets clicked.
*
* <p>Added because {@link Solo#clickLongOnText(String)} has issues with locating the text.
* It will often click the wrong place. Use this method instead.</p>
*
* @param solo The solo tester.
* @param textViewId Id of the textview with the text to long click.
* @param scrollViewId Id of the scrollview that contains the textview.
* @param textToClick The text to long click.
* @throws IllegalArgumentException If the text is not visible or not found.
*/
public static void clickLongOnText(final Solo solo, final int textViewId, final int scrollViewId, final String textToClick) {
final Point coordinatesForLine = getCoordinatesForText(solo, textViewId, scrollViewId, textToClick);
solo.clickLongOnScreen(coordinatesForLine.x, coordinatesForLine.y);
}
/**
* Searches for a textview with the given text.
*
* <p>Added because {@link Solo#searchText(String)} looks for text in everything that inherits from
* textview, and that includes edittext. If it's important to look only in textviews, use this method.</p>
*
* @param solo The solo tester.
* @param text The text to search for.
* @return If the text was found in any textviews.
*/
public static boolean searchText(final Solo solo, final String text) {
try {
getTextViewWithText(solo, text);
return true;
} catch (final IllegalArgumentException e) {
return false;
}
}
/**
* Goes back to the previous activity, hiding the software keyboard first if necessary.
*
* @param solo The solo tester.
*/
public static void goBack(final Solo solo) {
final Activity currentActivity = solo.getCurrentActivity();
solo.goBack();
// No change in activity by going back. Assuming it's because the software keyboard was visible.
// Need to go back once more to actually "go back".
if (currentActivity == solo.getCurrentActivity()) {
solo.goBack();
}
solo.sleep(500);
}
/**
* Clicks on the "up" button in the action bar. The up button is the icon with the left arrow.
*
* @param solo The solo tester.
*/
public static void goUp(final Solo solo) {
solo.clickOnActionBarHomeButton();
}
/**
* Switches the orientation between landscape and portrait.
*
* @param solo The solo tester.
*/
public static void switchOrientation(final Solo solo) {
if (getCurrentOrientation(solo) == Configuration.ORIENTATION_LANDSCAPE) {
solo.setActivityOrientation(Solo.PORTRAIT);
} else {
solo.setActivityOrientation(Solo.LANDSCAPE);
}
solo.sleep(500);
}
/**
* Sets the orientation to the requested new orientation.
*
* @param solo The solo tester.
* @param newOrientation The new orientation to set,
*/
public static void setOrientation(final Solo solo, final int newOrientation) {
if (newOrientation == Configuration.ORIENTATION_LANDSCAPE) {
solo.setActivityOrientation(Solo.LANDSCAPE);
} else {
solo.setActivityOrientation(Solo.PORTRAIT);
}
solo.sleep(500);
}
/**
* Gets the orientation of the activity.
*
* @param solo The solo tester.
* @return The current orientation.
*/
public static int getCurrentOrientation(final Solo solo) {
return solo.getCurrentActivity().getResources().getConfiguration().orientation;
}
/**
* Switches the entire user interface to {@link Locale#US}.
*
* @param activity The activity to change the language of.
*/
public static void switchUserInterfaceToEnglish(final Activity activity) {
Locale.setDefault(Locale.US); // Switches the decimal separators, time format and so on
final Resources resources = activity.getResources();
final DisplayMetrics displayMetrics = resources.getDisplayMetrics();
final Configuration configuration = resources.getConfiguration();
configuration.locale = Locale.US; // Switches the actual language shown in the user interface
resources.updateConfiguration(configuration, displayMetrics);
}
/**
* Goes to the settings in the menu.
*
* @param solo The solo tester.
*/
public static void openSettings(final Solo solo) {
openMenu(solo);
solo.clickOnText("Settings");
}
/**
* Goes to the settings in the menu, and selects the option to change the nick name.
*
* @param solo The solo tester.
*/
public static void clickOnChangeNickNameInTheSettings(final Solo solo) {
openSettings(solo);
solo.clickOnText("Set nick name");
}
/**
* Changes the nick name, if already in the correct menu.
*
* Use {@link #clickOnChangeNickNameInTheSettings(Solo)} first.
*
* @param solo The solo tester.
* @param nickName The nick name to change to.
*/
public static void changeNickNameTo(final Solo solo, final String nickName) {
solo.hideSoftKeyboard();
solo.clearEditText(0);
solo.enterText(0, nickName);
solo.clickOnButton("OK");
}
/**
* Opens the menu, clicks on "Topic", sets the specified topic, and clicks OK.
*
* @param solo The solo tester.
* @param instrumentation The test instrumentation.
* @param topic The topic to set.
*/
public static void changeTopicTo(final Solo solo, final Instrumentation instrumentation, final String topic) {
solo.sleep(100);
openMenu(solo);
solo.clickOnText("Topic");
solo.sleep(400);
writeText(instrumentation, topic);
solo.sleep(200);
solo.clickOnText("OK");
solo.sleep(200);
}
/**
* Opens the menu, clicks on "Away", sets the specified away message, and clicks OK.
*
* @param solo The solo tester.
* @param instrumentation The test instrumentation.
* @param awayMessage The away message to set.
*/
public static void goAway(final Solo solo, final Instrumentation instrumentation, final String awayMessage) {
solo.sleep(100);
openMenu(solo);
solo.clickOnText("Away");
solo.sleep(400);
writeText(instrumentation, awayMessage);
solo.sleep(200);
solo.clickOnText("OK");
solo.sleep(200);
}
/**
* Extracts "me" from the android user interface in an activity.
*
* @param activity An activity with an android user interface object.
* @return The application user.
*/
public static User getMe(final Activity activity) {
final AndroidUserInterface androidUserInterface = getAndroidUserInterface(activity);
return TestUtils.getFieldValue(androidUserInterface, User.class, "me");
}
/**
* Extracts the android user interface from an activity.
*
* @param activity An activity with an android user interface object.
* @return The android user interface.
*/
public static AndroidUserInterface getAndroidUserInterface(final Activity activity) {
return TestUtils.getFieldValue(activity, AndroidUserInterface.class, "androidUserInterface");
}
/**
* Launches the main chat.
*
* <p>Use {@link #goBack(Solo)} or {@link #goHome(Solo)} to navigate to an already opened main chat.
* Use this method if the main chat has been finished and closed.</p>
*
* @param testCase The test that needs to launch the main chat.
*/
public static void launchMainChat(final InstrumentationTestCase testCase) {
final String packageName = testCase.getInstrumentation().getTargetContext().getPackageName();
testCase.launchActivity(packageName, MainChatController.class, null);
}
/**
* Closes the main chat, by finishing it.
*
* @param testCase The test that needs to close the main chat.
*/
public static void closeMainChat(final ActivityInstrumentationTestCase2 testCase) {
final Activity activity = testCase.getActivity();
assertEquals(MainChatController.class, activity.getClass());
activity.finish();
}
/**
* Opens a private chat with the specified user.
*
* <p>Expects that the user is not away.</p>
*
* @param solo The solo tester.
* @param instrumentation The instrumentation instance.
* @param numberOfUsers Number of users to expect in the main chat.
* @param userNumber The number to expect the specified user to be in the list.
* @param userName Expected user name of the user to open the private chat with.
*/
public static void openPrivateChat(final Solo solo, final Instrumentation instrumentation,
final int numberOfUsers, final int userNumber,
final String userName) {
openPrivateChat(solo, instrumentation, numberOfUsers, userNumber, userName, null);
}
/**
* Opens a private chat with the specified user.
*
* <p>Supports a user that is away.</p>
*
* @param solo The solo tester.
* @param instrumentation The instrumentation instance.
* @param numberOfUsers Number of users to expect in the main chat.
* @param userNumber The number to expect the specified user to be in the list.
* @param userName Expected user name of the user to open the private chat with.
* @param awayMessage Expected away message of the user to open the private chat with.
*/
public static void openPrivateChat(final Solo solo, final Instrumentation instrumentation,
final int numberOfUsers, final int userNumber,
final String userName, final String awayMessage) {
solo.sleep(500);
assertEquals(numberOfUsers, solo.getCurrentViews(ListView.class).get(0).getCount());
solo.clickInList(userNumber);
solo.sleep(500);
solo.assertCurrentActivity("Should have opened the private chat", PrivateChatController.class);
instrumentation.waitForIdleSync();
solo.sleep(500);
// To be sure we are chatting with the right user
final AppCompatActivity currentActivity = (AppCompatActivity) solo.getCurrentActivity();
final ActionBar actionBar = currentActivity.getSupportActionBar();
assertEquals(userName, actionBar.getTitle());
assertEquals(awayMessage, actionBar.getSubtitle());
}
/**
* Checks if the text is currently visible in the scrollview.
*
* @param solo The solo tester.
* @param textViewId Id of the textview with the text to check.
* @param scrollViewId Id of the scrollview that contains the textview.
* @param textToFind The text to check if it's visible.
* @return If the text is currently visible.
* @throws IllegalArgumentException If the text is not not found.
*/
public static boolean textIsVisible(final Solo solo, final int textViewId, final int scrollViewId, final String textToFind) {
final TextView textView = (TextView) solo.getView(textViewId);
final ScrollView scrollView = (ScrollView) solo.getView(scrollViewId);
final Rect visibleScrollArea = getVisibleScrollArea(scrollView);
final String fullText = textView.getText().toString();
final List<String> allLinesOfText = getAllLinesOfText(fullText, textView);
final List<Line> matchingLinesOfText = getMatchingLinesOfText(fullText, allLinesOfText, textToFind);
for (final Line matchingLine : matchingLinesOfText) {
final Point coordinatesForLine = getCoordinatesForLine(textView, matchingLine.getLineText(),
matchingLine.getLineNumber(), allLinesOfText.get(matchingLine.getLineNumber()));
if (!visibleScrollArea.contains(coordinatesForLine.x, coordinatesForLine.y)) {
return false;
}
}
return true;
}
/**
* Gets all the lines of text matching text to find.
*
* @param fullText The full text to search in.
* @param allLinesOfText The full text, split in lines.
* @param textToFind The text to find the lines for.
* @return List containing the text to find, with the exact part of the text found on each line,
* and which line number that part of the text was found at.
* @throws IllegalArgumentException If the text to find is not located in the full text.
*/
public static List<Line> getMatchingLinesOfText(final String fullText, final List<String> allLinesOfText, final String textToFind) {
final int textToFindIndex = fullText.lastIndexOf(textToFind);
if (textToFindIndex < 0) {
throw new IllegalArgumentException("Could not find: " + textToFind);
}
final int startLine = findStartLine(allLinesOfText, textToFindIndex);
final List<Line> matchingLines = new ArrayList<Line>();
final List<String> wordsFromTextToFind = splitOnBoundaries(textToFind);
removeEmptyFirstWord(wordsFromTextToFind);
for (int currentLineNumber = startLine; currentLineNumber < allLinesOfText.size(); currentLineNumber++) {
addMatchingLine(allLinesOfText.get(currentLineNumber), currentLineNumber, wordsFromTextToFind, matchingLines);
if (wordsFromTextToFind.isEmpty()) {
break;
}
}
return matchingLines;
}
private static int findStartLine(final List<String> allLinesOfText, final int textToFindIndex) {
int startLine = 0;
int currentIndex = 0;
for (final String line : allLinesOfText) {
if (currentIndex + line.length() >= textToFindIndex) {
break;
}
startLine++;
currentIndex += line.length();
}
return startLine;
}
private static List<String> splitOnBoundaries(final String text) {
return new ArrayList<String>(Arrays.asList(text.split("\\b")));
}
private static void removeEmptyFirstWord(final List<String> words) {
if (words.get(0).equals("")) {
words.remove(0);
}
}
private static void addMatchingLine(final String currentLine, final int currentLineNumber,
final List<String> wordsFromTextToFind, final List<Line> matchingLines) {
String wordFromTextToFind = wordsFromTextToFind.get(0);
if (!currentLine.contains(wordFromTextToFind)) {
return;
}
final String currentLineStartingAtWord = currentLine.substring(currentLine.indexOf(wordFromTextToFind));
final List<String> wordsFromCurrentLine = splitOnBoundaries(currentLineStartingAtWord);
removeEmptyFirstWord(wordsFromCurrentLine);
String wordFromCurrentLine = wordsFromCurrentLine.remove(0);
wordFromTextToFind = wordsFromTextToFind.remove(0);
final StringBuilder matchingLine = new StringBuilder();
while (wordFromTextToFind.equals(wordFromCurrentLine)) {
matchingLine.append(wordFromCurrentLine);
if (wordsFromTextToFind.isEmpty() || wordsFromCurrentLine.isEmpty()) {
break;
}
wordFromTextToFind = wordsFromTextToFind.remove(0);
wordFromCurrentLine = wordsFromCurrentLine.remove(0);
}
matchingLines.add(new Line(currentLineNumber, matchingLine.toString()));
}
private static Rect getVisibleScrollArea(final ScrollView scrollView) {
final int[] locationOnScreen = new int[2];
scrollView.getLocationOnScreen(locationOnScreen);
return new Rect(
locationOnScreen[0], // left position
locationOnScreen[1], // top position
locationOnScreen[0] + scrollView.getWidth(), // right position
locationOnScreen[1] + scrollView.getHeight()); // bottom position
}
private static Point getCoordinatesForText(final Solo solo, final int textViewId, final int scrollViewId, final String textToFind) {
final TextView textView = (TextView) solo.getView(textViewId);
final ScrollView scrollView = (ScrollView) solo.getView(scrollViewId);
final Rect visibleScrollArea = getVisibleScrollArea(scrollView);
final String fullText = textView.getText().toString();
final List<String> allLinesOfText = getAllLinesOfText(fullText, textView);
final List<Line> matchingLinesOfText = getMatchingLinesOfText(fullText, allLinesOfText, textToFind);
final Line lastMatchingLine = matchingLinesOfText.get(matchingLinesOfText.size() - 1);
final Point coordinatesForLine = getCoordinatesForLine(textView, lastMatchingLine.getLineText(),
lastMatchingLine.getLineNumber(), allLinesOfText.get(lastMatchingLine.getLineNumber()));
if (!visibleScrollArea.contains(coordinatesForLine.x, coordinatesForLine.y)) {
throw new IllegalArgumentException("Text to find is not visible: " + textToFind);
}
return coordinatesForLine;
}
private static Point getCoordinatesForLine(final TextView textView, final String textToFind,
final int lineNumber, final String fullLine) {
final Layout layout = textView.getLayout();
final TextPaint paint = textView.getPaint();
final int textIndex = fullLine.indexOf(textToFind);
final String preText = fullLine.substring(0, textIndex);
final int textWidth = (int) Layout.getDesiredWidth(textToFind, paint);
final int preTextWidth = (int) Layout.getDesiredWidth(preText, paint);
final int[] textViewXYLocation = new int[2];
textView.getLocationOnScreen(textViewXYLocation);
// Width: in the middle of the text
final int xPosition = preTextWidth + (textWidth / 2);
// Height: in the middle of the given line, plus the text view position from the top, minus the amount scrolled
final int yPosition = layout.getLineBaseline(lineNumber) + textViewXYLocation[1] - textView.getScrollY();
return new Point(xPosition, yPosition);
}
}