diff --git a/app/build.gradle b/app/build.gradle index cb85919a2..a1a1bb4bf 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 { @@ -15,6 +17,9 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } + debug { + testCoverageEnabled = true + } } lintOptions { @@ -27,5 +32,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..7c9a7e0a2 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java @@ -0,0 +1,130 @@ +/* + * 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.CoordinatesProvider; +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; +import java.util.Random; + +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); + } + } + }; + } + + public static ViewAction clickAt(final int x, final int y) + { + return new GeneralClickAction(Tap.SINGLE, new CoordinatesProvider() + { + @Override + public float[] calculateCoordinates(View view) + { + int[] locations = new int[2]; + view.getLocationOnScreen(locations); + + final float locationX = locations[0] + x; + final float locationY = locations[1] + y; + + return new float[]{locationX, locationY}; + } + }, Press.FINGER); + } + + public static ViewAction clickAtRandomLocations(final int count) + { + return new ViewAction() + { + @Override + public Matcher getConstraints() + { + return ViewMatchers.isDisplayed(); + } + + @Override + public String getDescription() + { + return "clickAtRandomLocations"; + } + + @Override + public void perform(UiController uiController, View view) + { + int width = view.getWidth(); + int height = view.getHeight(); + Random random = new Random(); + + for(int i = 0; i < count; i++) + { + int x = random.nextInt(width); + int y = random.nextInt(height); + + ViewAction action = clickAt(x, y); + action.perform(uiController, view); + } + } + }; + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java new file mode 100644 index 000000000..3ce26da37 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java @@ -0,0 +1,178 @@ +/* + * 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.content.Context; +import android.support.test.InstrumentationRegistry; + +import org.isoron.uhabits.models.Habit; + +import java.util.Collections; +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.openContextualActionModeOverflowMenu; +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.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +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.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; + +public class MainActivityActions +{ + public static String addHabit() + { + return addHabit(false); + } + + public static 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; + } + + public static 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)); + } + + public static void selectHabit(String name) + { + selectHabits(Collections.singletonList(name)); + } + + public static 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; + } + } + + public static void assertHabitsDontExist(List names) + { + for(String name : names) + onView(withId(R.id.listView)) + .check(matches(not(containsHabit(withName(name))))); + } + + public static void assertHabitExists(String name) + { + List names = new LinkedList<>(); + names.add(name); + assertHabitsExist(names); + } + + public static void assertHabitsExist(List names) + { + for(String name : names) + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .check(matches(isDisplayed())); + } + + public static void deleteHabit(String name) + { + deleteHabits(Collections.singletonList(name)); + } + + public static void deleteHabits(List names) + { + selectHabits(names); + clickActionModeMenuItem(R.string.delete); + onView(withText("OK")) + .perform(click()); + assertHabitsDontExist(names); + } + + public static void clickActionModeMenuItem(int stringId) + { + try + { + onView(withText(stringId)).perform(click()); + } + catch (Exception e1) + { + try + { + onView(withContentDescription(stringId)).perform(click()); + } + catch(Exception e2) + { + openContextualActionModeOverflowMenu(); + onView(withText(stringId)).perform(click()); + } + } + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/MainTest.java new file mode 100644 index 000000000..37bd87989 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/MainTest.java @@ -0,0 +1,201 @@ +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.scrollTo; +import static android.support.test.espresso.action.ViewActions.swipeLeft; +import static android.support.test.espresso.action.ViewActions.swipeRight; +import static android.support.test.espresso.action.ViewActions.swipeUp; +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.isRoot; +import static android.support.test.espresso.matcher.ViewMatchers.withClassName; +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.isoron.uhabits.HabitMatchers.withName; +import static org.isoron.uhabits.HabitViewActions.clickAtRandomLocations; +import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks; +import static org.isoron.uhabits.MainActivityActions.addHabit; +import static org.isoron.uhabits.MainActivityActions.assertHabitExists; +import static org.isoron.uhabits.MainActivityActions.assertHabitsDontExist; +import static org.isoron.uhabits.MainActivityActions.assertHabitsExist; +import static org.isoron.uhabits.MainActivityActions.clickActionModeMenuItem; +import static org.isoron.uhabits.MainActivityActions.deleteHabit; +import static org.isoron.uhabits.MainActivityActions.deleteHabits; +import static org.isoron.uhabits.MainActivityActions.selectHabit; +import static org.isoron.uhabits.MainActivityActions.selectHabits; +import static org.isoron.uhabits.MainActivityActions.typeHabitData; +import static org.isoron.uhabits.ShowHabitActivityActions.openHistoryEditor; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MainTest +{ + @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 + } + } + + @Test + public void testArchiveHabits() + { + List names = new LinkedList<>(); + Context context = InstrumentationRegistry.getTargetContext(); + + for(int i = 0; i < 3; i++) + names.add(addHabit()); + + selectHabits(names); + + clickActionModeMenuItem(R.string.archive); + assertHabitsDontExist(names); + + openActionBarOverflowOrOptionsMenu(context); + onView(withText(R.string.show_archived)) + .perform(click()); + + assertHabitsExist(names); + selectHabits(names); + clickActionModeMenuItem(R.string.unarchive); + + openActionBarOverflowOrOptionsMenu(context); + onView(withText(R.string.show_archived)) + .perform(click()); + + assertHabitsExist(names); + deleteHabits(names); + } + + @Test + public void testAddInvalidHabit() + { + onView(withId(R.id.action_add)) + .perform(click()); + + typeHabitData("", "", "15", "7"); + + onView(withId(R.id.buttonSave)).perform(click()); + onView(withId(R.id.input_name)).check(matches(isDisplayed())); + } + + @Test + public void testAddHabitAndViewStats() + { + String name = addHabit(true); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.llButtons)) + .perform(toggleAllCheckmarks()); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)) + .perform(click()); + + onView(withId(R.id.scoreView)) + .perform(swipeRight()); + + 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()); + + clickActionModeMenuItem(R.string.edit); + + String modifiedName = "Modified " + new Random().nextInt(10000); + typeHabitData(modifiedName, "", "1", "1"); + + onView(withId(R.id.buttonSave)) + .perform(click()); + + assertHabitExists(modifiedName); + + selectHabit(modifiedName); + clickActionModeMenuItem(R.string.color_picker_default_title); + pressBack(); + + deleteHabit(modifiedName); + } + + @Test + public void testEditHistory() + { + String name = addHabit(); + + onData(allOf(is(instanceOf(Habit.class)), withName(name))) + .onChildView(withId(R.id.label)) + .perform(click()); + + openHistoryEditor(); + onView(withClassName(endsWith("HabitHistoryView"))) + .perform(clickAtRandomLocations(20)); + + pressBack(); + onView(withId(R.id.historyView)) + .perform(scrollTo(), swipeRight(), swipeLeft()); + } + + @Test + public void testSettingsAndAbout() + { + Context context = InstrumentationRegistry.getContext(); + + openActionBarOverflowOrOptionsMenu(context); + onView(withText(R.string.settings)) + .perform(click()); + pressBack(); + + openActionBarOverflowOrOptionsMenu(context); + onView(withText(R.string.about)) + .perform(click()); + onView(isRoot()) + .perform(swipeUp()); + pressBack(); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java new file mode 100644 index 000000000..cacc9f21f --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java @@ -0,0 +1,34 @@ +/* + * 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 static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.scrollTo; +import static android.support.test.espresso.matcher.ViewMatchers.withId; + +public class ShowHabitActivityActions +{ + public static void openHistoryEditor() + { + onView(withId(R.id.btEditHistory)) + .perform(scrollTo(), click()); + } +} 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