From 51e8c2f11152ec912c3a84d2ec10caa01841c333 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 11 Mar 2016 10:35:52 -0500 Subject: [PATCH] Implement basic user interface tests --- app/build.gradle | 8 + .../org/isoron/uhabits/HabitMatchers.java | 79 ++++++ .../org/isoron/uhabits/HabitViewActions.java | 75 ++++++ .../org/isoron/uhabits/MainActivityTest.java | 252 ++++++++++++++++++ .../uhabits/fragments/HabitListAdapter.java | 4 +- 5 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/MainActivityTest.java diff --git a/app/build.gradle b/app/build.gradle index cb85919a2..4f56050af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,8 @@ android { applicationId "org.isoron.uhabits" minSdkVersion 15 targetSdkVersion 23 + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -27,5 +29,11 @@ dependencies { compile 'com.github.paolorotolo:appintro:3.4.0' compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') + + androidTestCompile 'com.android.support:support-annotations:23.1.1' + androidTestCompile 'com.android.support.test:runner:0.4.1' + androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' + androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' } diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java b/app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java new file mode 100644 index 000000000..c96b24c16 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits; + +import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.isoron.uhabits.models.Habit; + +public class HabitMatchers +{ + public static Matcher withName(final String name) + { + return new TypeSafeMatcher() + { + @Override + public boolean matchesSafely(Habit habit) + { + return habit.name.equals(name); + } + + @Override + public void describeTo(Description description) + { + description.appendText("name should be ").appendText(name); + } + + @Override + public void describeMismatchSafely(Habit habit, Description description) + { + description.appendText("was ").appendText(habit.name); + } + }; + } + + public static Matcher containsHabit(final Matcher matcher) + { + return new TypeSafeMatcher() + { + @Override + protected boolean matchesSafely(View view) + { + Adapter adapter = ((AdapterView) view).getAdapter(); + for (int i = 0; i < adapter.getCount(); i++) + if (matcher.matches(adapter.getItem(i))) return true; + + return false; + } + + @Override + public void describeTo(Description description) + { + description.appendText("with class name: "); + matcher.describeTo(description); + } + }; + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java b/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java new file mode 100644 index 000000000..5dd6f29a9 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits; + +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.GeneralClickAction; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Tap; +import android.support.test.espresso.matcher.ViewMatchers; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.hamcrest.Matcher; + +import java.security.InvalidParameterException; + +public class HabitViewActions +{ + public static ViewAction toggleAllCheckmarks() + { + final GeneralClickAction clickAction = + new GeneralClickAction(Tap.LONG, GeneralLocation.CENTER, Press.FINGER); + + return new ViewAction() + { + @Override + public Matcher getConstraints() + { + return ViewMatchers.isDisplayed(); + } + + @Override + public String getDescription() + { + return "toggleAllCheckmarks"; + } + + @Override + public void perform(UiController uiController, View view) + { + if (view.getId() != R.id.llButtons) + throw new InvalidParameterException("View must have id llButtons"); + + LinearLayout llButtons = (LinearLayout) view; + int count = llButtons.getChildCount(); + + for (int i = 0; i < count; i++) + { + TextView tvButton = (TextView) llButtons.getChildAt(i); + clickAction.perform(uiController, tvButton); + } + } + }; + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/MainActivityTest.java b/app/src/androidTest/java/org/isoron/uhabits/MainActivityTest.java new file mode 100644 index 000000000..93d8451b4 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/MainActivityTest.java @@ -0,0 +1,252 @@ +package org.isoron.uhabits; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.NoMatchingViewException; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; + +import org.isoron.uhabits.models.Habit; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +import static android.support.test.espresso.Espresso.onData; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +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.longClick; +import static android.support.test.espresso.action.ViewActions.replaceText; +import static android.support.test.espresso.action.ViewActions.scrollTo; +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.withClassName; +import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.isoron.uhabits.HabitMatchers.containsHabit; +import static org.isoron.uhabits.HabitMatchers.withName; +import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MainActivityTest +{ + @Rule + public IntentsTestRule activityRule = new IntentsTestRule<>( + MainActivity.class); + + @Before + public void skipTutorial() + { + try + { + for (int i = 0; i < 10; i++) + onView(allOf(withClassName(endsWith("AppCompatImageButton")), + isDisplayed())).perform(click()); + } + catch (NoMatchingViewException e) + { + // ignored + } + } + + public String addHabit() + { + return addHabit(false); + } + + public String addHabit(boolean openDialogs) + { + String name = "New Habit " + new Random().nextInt(1000000); + String description = "Did you perform your new habit today?"; + String num = "4"; + String den = "8"; + + onView(withId(R.id.action_add)) + .perform(click()); + + typeHabitData(name, description, num, den); + + if(openDialogs) + { + onView(withId(R.id.buttonPickColor)) + .perform(click()); + pressBack(); + onView(withId(R.id.inputReminderTime)) + .perform(click()); + onView(withText("Done")) + .perform(click()); + onView(withId(R.id.inputReminderDays)) + .perform(click()); + onView(withText("OK")) + .perform(click()); + } + + onView(withId(R.id.buttonSave)) + .perform(click()); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)); + + return name; + } + + private void typeHabitData(String name, String description, String num, String den) + { + onView(withId(R.id.input_name)) + .perform(replaceText(name)); + onView(withId(R.id.input_description)) + .perform(replaceText(description)); + onView(withId(R.id.input_freq_num)) + .perform(replaceText(num)); + onView(withId(R.id.input_freq_den)) + .perform(replaceText(den)); + } + + private void selectHabits(List names) + { + boolean first = true; + for(String name : names) + { + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)) + .perform(first ? longClick() : click()); + + first = false; + } + } + + private void assertHabitsDontExist(List names) + { + for(String name : names) + onView(withId(R.id.listView)) + .check(matches(not(containsHabit(withName(name))))); + } + + private void assertHabitExists(String name) + { + List names = new LinkedList<>(); + names.add(name); + assertHabitsExist(names); + } + + private void assertHabitsExist(List names) + { + for(String name : names) + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .check(matches(isDisplayed())); + } + + private void deleteHabit(String name) + { + LinkedList names = new LinkedList<>(); + names.add(name); + deleteHabits(names); + } + + private void deleteHabits(List names) + { + Context context = InstrumentationRegistry.getTargetContext(); + + selectHabits(names); + + openActionBarOverflowOrOptionsMenu(context); + + onView(withText(R.string.delete)) + .perform(click()); + onView(withText("OK")) + .perform(click()); + + assertHabitsDontExist(names); + } + + @Test + public void testArchiveHabits() + { + List names = new LinkedList<>(); + Context context = InstrumentationRegistry.getTargetContext(); + + for(int i = 0; i < 3; i++) + names.add(addHabit()); + + selectHabits(names); + onView(withContentDescription(R.string.archive)) + .perform(click()); + assertHabitsDontExist(names); + + openActionBarOverflowOrOptionsMenu(context); + onView(withText(R.string.show_archived)) + .perform(click()); + + assertHabitsExist(names); + deleteHabits(names); + } + + @Test + public void testAddInvalidHabit() + { + typeHabitData("", "", "15", "7"); + onView(withId(R.id.buttonSave)).perform(click()); + onView(withId(R.id.input_name)).check(matches(isDisplayed())); + } + + @Test + public void testToggleCheckmarks() + { + String name = addHabit(); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.llButtons)) + .perform(toggleAllCheckmarks()); + + deleteHabit(name); + } + + @Test + public void testAddHabit() + { + String name = addHabit(true); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)) + .perform(click()); + + onView(withId(R.id.punchcardView)) + .perform(scrollTo()); + } + + @Test + public void testEditHabit() + { + String name = addHabit(); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)) + .perform(longClick()); + + onView(withContentDescription(R.string.edit)) + .perform(click()); + + String modifiedName = "Modified " + new Random().nextInt(10000); + typeHabitData(modifiedName, "", "1", "1"); + + onView(withId(R.id.buttonSave)) + .perform(click()); + + assertHabitExists(modifiedName); + deleteHabit(modifiedName); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java b/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java index 432582701..3ef39713e 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java @@ -59,7 +59,7 @@ class HabitListAdapter extends BaseAdapter } @Override - public Object getItem(int position) + public Habit getItem(int position) { return loader.habitsList.get(position); } @@ -67,7 +67,7 @@ class HabitListAdapter extends BaseAdapter @Override public long getItemId(int position) { - return ((Habit) getItem(position)).getId(); + return (getItem(position)).getId(); } @Override