TODOs: UI tests for TodoListActivity

Some of the code from MainActivityTest was extracted into TestUtil.

These tests cover the following interactions:
- Swipe left (to delete)
- Swipe right (to mark as done)
- Tap (to edit or delete)
- FAB (to add)
- Menu (to show done)
- Menu (to edit or delete the ListSpec)

The same strategy was used as the previous tests. The UI interaction
to Persistence was tested separately from the Listener call to UI
response.

Change-Id: I9b55edd6d34a0b78d23e961d7c85da91ff66865e
diff --git a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
index d3ebf20..c9b2dec 100644
--- a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
+++ b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
@@ -10,7 +10,6 @@
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.RecyclerView;
 import android.test.ActivityInstrumentationTestCase2;
-import android.widget.EditText;
 
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
@@ -18,23 +17,15 @@
 import io.v.todos.persistence.MainPersistence;
 
 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.swipeLeft;
-import static android.support.test.espresso.action.ViewActions.swipeRight;
+import static android.support.test.espresso.action.ViewActions.*;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.intent.Intents.intended;
 import static android.support.test.espresso.intent.matcher.IntentMatchers.hasComponent;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.support.test.espresso.matcher.ViewMatchers.*;
+import static io.v.todos.TestUtil.*;
 import static org.hamcrest.Matchers.not;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
 
 /**
  * Contains UI tests for the MainActivity that isolate the MainPersistence's behavior.
@@ -47,13 +38,6 @@
  * Created by alexfandrianto on 4/21/16.
  */
 public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
-    private static final String KEY1 = "asdlkjweriousdf";
-    private static final String KEY2 = "woeiuflskjeroius";
-    private static final String NAME1 = "weoislkjdflkejroif";
-    private static final String NAME2 = "weurlksdnoielrkmlsd";
-    private static final String NAME3 = "oisdlkwejrllisdfelkejr";
-    private static final long UI_DELAY = 500; // Tweak this value to adjust the test speed.
-
     private MainActivity mActivity;
 
     public MainActivityTest() {
@@ -80,44 +64,16 @@
         }
     }
 
-    private void pause() {
-        try {
-            Thread.sleep(UI_DELAY);
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-
     private MainPersistence mockPersistence() {
         MainPersistence mocked = mock(MainPersistence.class);
         mActivity.setMainPersistence(mocked);
         return mocked;
     }
 
-    private AlertDialog beginAddItem() {
-        onView(withId(R.id.fab)).perform(click());
-        pause();
-
-        final AlertDialog dialog = UIUtil.getLastDialog();
-
-        assertTrue(dialog.isShowing());
-
-        mActivity.runOnUiThread(new Runnable() {
-            public void run() {
-                EditText et = (EditText) dialog.getCurrentFocus();
-                et.setText("HELLO WORLD!");
-            }
-        });
-
-        pause();
-
-        return dialog;
-    }
-
     // Press the fab to attempt to add an item.
     public void testAttemptAddItem() {
         MainPersistence mocked = mockPersistence();
-        AlertDialog dialog = beginAddItem();
+        AlertDialog dialog = beginAddItem(mActivity);
 
         dialog.getButton(DialogInterface.BUTTON_POSITIVE).callOnClick();
 
@@ -133,7 +89,7 @@
     // Press the fab but don't actually add the item.
     public void testAttemptAddItemFail() {
         MainPersistence mocked = mockPersistence();
-        AlertDialog dialog = beginAddItem();
+        AlertDialog dialog = beginAddItem(mActivity);
 
         dialog.getButton(DialogInterface.BUTTON_NEGATIVE).callOnClick();
 
@@ -149,7 +105,7 @@
     // Press the fab but don't actually add the item.
     public void testAttemptAddItemCancel() {
         MainPersistence mocked = mockPersistence();
-        AlertDialog dialog = beginAddItem();
+        AlertDialog dialog = beginAddItem(mActivity);
 
         dialog.dismiss();
 
@@ -261,7 +217,8 @@
         pause();
 
         // DO UI INTERACTION
-        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, swipeRight()));
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1,
+                swipeRight()));
 
         pause();
 
@@ -278,7 +235,8 @@
         pause();
 
         // DO UI INTERACTION
-        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, swipeLeft()));
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1,
+                swipeLeft()));
 
         pause();
 
@@ -295,7 +253,8 @@
         pause();
 
         // DO UI INTERACTION
-        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
+                ()));
 
         pause();
 
diff --git a/app/src/androidTestMock/java/io/v/todos/TestUtil.java b/app/src/androidTestMock/java/io/v/todos/TestUtil.java
new file mode 100644
index 0000000..212bfc7
--- /dev/null
+++ b/app/src/androidTestMock/java/io/v/todos/TestUtil.java
@@ -0,0 +1,72 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos;
+
+import android.app.Activity;
+import android.support.v7.app.AlertDialog;
+import android.widget.EditText;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static junit.framework.Assert.assertTrue;
+
+class TestUtil {
+    static final String KEY1 = "asdlkjweriousdf";
+    static final String KEY2 = "woeiuflskjeroius";
+    static final String NAME1 = "weoislkjdflkejroif";
+    static final String NAME2 = "weurlksdnoielrkmlsd";
+    static final String NAME3 = "oisdlkwejrllisdfelkejr";
+
+    static final String TEST_LIST_KEY = "list key";
+    static final String TEST_LIST_NAME = "list name";
+
+    private static final long UI_DELAY = 500; // Tweak this value to adjust the test speed.
+
+    static void pause() {
+        try {
+            Thread.sleep(UI_DELAY);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    static AlertDialog beginAddItem(Activity activity) {
+        onView(withId(R.id.fab)).perform(click());
+        pause();
+
+        final AlertDialog dialog = UIUtil.getLastDialog();
+
+        assertTrue(dialog.isShowing());
+
+        activity.runOnUiThread(new Runnable() {
+            public void run() {
+                EditText et = (EditText) dialog.getCurrentFocus();
+                et.setText("HELLO WORLD!");
+            }
+        });
+
+        pause();
+
+        return dialog;
+    }
+
+    static AlertDialog handleEditDialog(Activity activity) {
+        final AlertDialog dialog = UIUtil.getLastDialog();
+
+        assertTrue(dialog.isShowing());
+
+        activity.runOnUiThread(new Runnable() {
+            public void run() {
+                EditText et = (EditText) dialog.getCurrentFocus();
+                et.setText("HELLO WORLD!");
+            }
+        });
+
+        pause();
+
+        return dialog;
+    }
+}
diff --git a/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java b/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java
new file mode 100644
index 0000000..afc1446
--- /dev/null
+++ b/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java
@@ -0,0 +1,485 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.support.test.espresso.contrib.RecyclerViewActions;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.test.ActivityInstrumentationTestCase2;
+
+import io.v.todos.model.ListSpec;
+import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
+import io.v.todos.persistence.TodoListListener;
+import io.v.todos.persistence.TodoListPersistence;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
+import static android.support.test.espresso.action.ViewActions.*;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.*;
+import static io.v.todos.TestUtil.*;
+import static org.hamcrest.Matchers.not;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Contains UI tests for the TodoListActivity that isolate the TodoListPersistence's behavior.
+ * <p/>
+ * Note: These tests will fail if the device's screen is off.
+ * TODO(alexfandrianto): There do seem to be ways to force the screen to turn on though.
+ * <p/>
+ * Most sleeps are present in order to make the tests easier to follow when running.
+ */
+public class TodoListActivityTest extends ActivityInstrumentationTestCase2<TodoListActivity> {
+    private TodoListActivity mActivity;
+
+    public TodoListActivityTest() {
+        super(TodoListActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Intent i = new Intent();
+        i.putExtra(MainActivity.INTENT_SNACKOO_KEY, TEST_LIST_KEY);
+
+        // Certain methods must be called before getActivity. Surprise!
+        setActivityIntent(i);
+        setActivityInitialTouchMode(true);
+
+        mActivity = getActivity();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        if (mActivity != null) {
+            mActivity.finish();
+            mActivity = null;
+        }
+    }
+
+    private TodoListPersistence mockPersistence() {
+        TodoListPersistence mocked = mock(TodoListPersistence.class);
+        mActivity.setTodoListPersistence(mocked);
+        return mocked;
+    }
+
+    // Helper to verify that the mock was never actually called.
+    private void verifyMockPersistence(TodoListPersistence mocked) {
+        verifyZeroInteractions(mocked);
+    }
+
+    // Helper to verify the number of calls that occurred on the mock.
+    private void verifyMockPersistence(TodoListPersistence mocked, int updateTodoList, int
+            deleteTodoList, int addTask, int updateTask, int deleteTask, int setShowDone) {
+        verify(mocked, times(updateTodoList)).updateTodoList(any(ListSpec.class));
+        verify(mocked, times(deleteTodoList)).deleteTodoList();
+        verify(mocked, times(addTask)).addTask(any(TaskSpec.class));
+        verify(mocked, times(updateTask)).updateTask(any(Task.class));
+        verify(mocked, times(deleteTask)).deleteTask(anyString());
+        verify(mocked, times(setShowDone)).setShowDone(anyBoolean());
+    }
+
+    // Press the fab to attempt to add an item.
+    public void testAttemptAddItem() {
+        TodoListPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem(mActivity);
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).callOnClick();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked, 0, 0, 1, 0, 0, 0);
+    }
+
+    // Press the fab but don't actually add the item.
+    public void testAttemptAddItemFail() {
+        TodoListPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem(mActivity);
+
+        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).callOnClick();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+    }
+
+    // Press the fab but don't actually add the item.
+    public void testAttemptAddItemCancel() {
+        TodoListPersistence mocked = mockPersistence();
+        AlertDialog dialog = beginAddItem(mActivity);
+
+        dialog.dismiss();
+
+        pause();
+
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+    }
+
+    // Add some default items so that we can interact with them with swipes.
+    private void addInitialData() {
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                TodoListListener listener = mActivity.createTodoListListener();
+
+                Task data = new Task(KEY1, NAME1, 0, false); // not done item
+                listener.onItemAdd(data);
+
+                Task data2 = new Task(KEY2, NAME2, 1, true); // a done item
+                listener.onItemAdd(data2);
+            }
+        });
+    }
+
+    // This should set the title in the toolbar of the activity.
+    public void testDirectlyUpdateListSpec() {
+        final TodoListListener listener = mActivity.createTodoListListener();
+
+        // Set the title of this screen.
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                ListSpec data = new ListSpec(TEST_LIST_NAME, 0);
+                listener.onUpdate(data);
+            }
+        });
+
+        pause();
+
+        // CHECK THAT THE TITLE IS CORRECT!
+        onView(withId(R.id.toolbar)).check(matches(hasDescendant(withText(TEST_LIST_NAME))));
+    }
+
+    // This should kick us out of the activity.
+    public void testDirectlyDeleteListSpec() {
+        final TodoListListener listener = mActivity.createTodoListListener();
+
+        // Call onDelete to finish this activity.
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onDelete();
+            }
+        });
+
+        pause();
+
+        // CHECK THAT WE GOT KICKED OUT OF THE ACTIVITY!
+        assertTrue(mActivity.isFinishing() || mActivity.isDestroyed());
+    }
+
+    // This should toggle the recycler view's children.
+    public void testDirectlyToggleShowDone() {
+        addInitialData();
+
+        pause();
+
+        final TodoListListener listener = mActivity.createTodoListListener();
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+
+        // Check that the adapter thinks that there are 2 items.
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+
+        // Set the show done status to false.
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onUpdateShowDone(false);
+            }
+        });
+
+        pause();
+
+        // Check that the adapter only thinks there is 1 item now.
+        assertEquals(recycler.getAdapter().getItemCount(), 1);
+
+        // Set the show done status to true.
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onUpdateShowDone(true);
+            }
+        });
+
+        pause();
+
+        // Check that the adapter thinks there are 2 items again.
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+    }
+
+    // Call the listener directly to add an item.
+    public void testDirectlyAddItem() {
+        final TodoListListener listener = mActivity.createTodoListListener();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+        pause();
+
+        final Task data = new Task(KEY1, NAME1, 0, false);
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemAdd(data);
+            }
+        });
+
+        pause();
+        assertEquals(recycler.getAdapter().getItemCount(), 1);
+
+        final Task data2 = new Task(KEY2, NAME2, 1, true);
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemAdd(data2);
+            }
+        });
+
+        pause();
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+    }
+
+    public void testDirectlyEditItem() {
+        addInitialData();
+
+        pause();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+
+        final TodoListListener listener = mActivity.createTodoListListener();
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemUpdate(new Task(KEY1, NAME3, 0, false));
+            }
+        });
+
+        pause();
+
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME1)))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME3))));
+        assertEquals(recycler.getAdapter().getItemCount(), 2);
+    }
+
+    public void testDirectlyDeleteItem() {
+        addInitialData();
+
+        pause();
+
+        RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME2))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+
+        final TodoListListener listener = mActivity.createTodoListListener();
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                listener.onItemDelete(KEY2);
+            }
+        });
+
+        pause();
+
+        onView(withId(R.id.recycler)).check(matches(hasDescendant(withText(NAME1))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME2)))));
+        onView(withId(R.id.recycler)).check(matches(not(hasDescendant(withText(NAME3)))));
+        assertEquals(recycler.getAdapter().getItemCount(), 1);
+    }
+
+    // Swipe a task item to the right to toggle its done marking.
+    public void testAttemptCompleteAllTasks() {
+        TodoListPersistence mocked = mockPersistence();
+        addInitialData();
+
+        pause();
+
+        // DO UI INTERACTION
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1,
+                swipeRight()));
+
+        pause();
+
+        verifyMockPersistence(mocked, 0, 0, 0, 1, 0, 0);
+    }
+
+    // Swipe a task item to the left to attempt to delete it.
+    public void testAttemptDeleteItem() {
+        TodoListPersistence mocked = mockPersistence();
+        addInitialData();
+
+        pause();
+
+        // DO UI INTERACTION
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1,
+                swipeLeft()));
+
+        pause();
+
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 1, 0);
+    }
+
+    // Tap a todo list item to enter its edit dialog. Tests dismiss, cancel, save, and delete.
+    public void testTapItem() {
+        TodoListPersistence mocked = mockPersistence();
+        addInitialData();
+        AlertDialog dialog;
+
+        pause();
+
+        // 1. DO UI INTERACTION (AND THEN DISMISS)
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
+                ()));
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.dismiss();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+
+        // 2. DO UI INTERACTION (AND THEN CANCEL)
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
+                ()));
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+
+        // 3. DO UI INTERACTION (AND THEN SAVE!)
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
+                ()));
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked, 0, 0, 0, 1, 0, 0);
+
+        // 4. DO UI INTERACTION (AND THEN DELETE!)
+        onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
+                ()));
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_NEUTRAL).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked, 0, 0, 0, 1, 1, 0);
+    }
+
+    private void tapMenuItem(int stringId) {
+        openActionBarOverflowOrOptionsMenu(mActivity);
+
+        pause();
+
+        // The menu items don't know their own IDs, so we have to use their text to find them.
+        onView(withText(stringId)).perform(click());
+    }
+
+    public void testTapMenuEdit() {
+        TodoListPersistence mocked = mockPersistence();
+        AlertDialog dialog;
+
+        // SETUP: We must have a title (or TaskSpec), otherwise this edit dialog doesn't make sense.
+        final TodoListListener listener = mActivity.createTodoListListener();
+
+        // Set the title of this screen.
+        mActivity.runOnUiThread(new Runnable() {
+            public void run() {
+                ListSpec data = new ListSpec(TEST_LIST_NAME, 0);
+                listener.onUpdate(data);
+            }
+        });
+
+        pause();
+
+        // 1. DISMISS THE DIALOG
+        tapMenuItem(R.string.action_edit);
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.dismiss();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+
+        // 2. CANCEL THE DIALOG
+        tapMenuItem(R.string.action_edit);
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked);
+
+        // 3. PRESS SAVE
+        tapMenuItem(R.string.action_edit);
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked, 1, 0, 0, 0, 0, 0);
+
+        // 4. PRESS DELETE
+        tapMenuItem(R.string.action_edit);
+
+        pause();
+
+        dialog = handleEditDialog(mActivity);
+        dialog.getButton(DialogInterface.BUTTON_NEUTRAL).callOnClick();
+
+        pause();
+        assertFalse(dialog.isShowing());
+
+        verifyMockPersistence(mocked, 1, 1, 0, 0, 0, 0);
+    }
+
+    public void testTapMenuShowDone() {
+        TodoListPersistence mocked = mockPersistence();
+
+        tapMenuItem(R.string.show_done);
+
+        pause();
+
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 0, 1);
+    }
+}
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index a5c603b..454884d 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -7,6 +7,7 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
 import android.view.Menu;
@@ -18,6 +19,7 @@
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
 import io.v.todos.model.TaskSpec;
+import io.v.todos.persistence.MainPersistence;
 import io.v.todos.persistence.PersistenceFactory;
 import io.v.todos.persistence.TodoListListener;
 import io.v.todos.persistence.TodoListPersistence;
@@ -95,59 +97,7 @@
             @Override
             protected TodoListPersistence initPersistence() throws Exception {
                 return PersistenceFactory.getTodoListPersistence(mActivity, snackooKey,
-                        new TodoListListener() {
-                            @Override
-                            public void onUpdate(ListSpec value) {
-                                snackoo = value;
-                                getActionBar().setTitle(snackoo.getName());
-                            }
-
-                            @Override
-                            public void onDelete() {
-                                finish();
-                            }
-
-                            @Override
-                            public void onUpdateShowDone(boolean showDone) {
-                                if (mShowDoneMenuItem != null) {
-                                    // Only interact with mShowDoneMenu if it has been inflated.
-                                    mShowDoneMenuItem.setChecked(showDone);
-                                }
-
-                                int oldSize = adapter.getItemCount();
-                                adapter.setShowDone(showDone);
-                                int newSize = adapter.getItemCount();
-                                if (newSize > oldSize) {
-                                    adapter.notifyItemRangeInserted(oldSize, newSize - oldSize);
-                                } else {
-                                    adapter.notifyItemRangeRemoved(newSize, oldSize - newSize);
-                                }
-                                setEmptyVisiblity();
-                            }
-
-                            @Override
-                            public void onItemAdd(Task item) {
-                                int position = snackoosList.insertInOrder(item);
-                                adapter.notifyItemInserted(position);
-                                setEmptyVisiblity();
-                            }
-
-                            @Override
-                            public void onItemUpdate(Task item) {
-                                int start = snackoosList.findIndexByKey(item.key);
-                                int end = snackoosList.updateInOrder(item);
-                                adapter.notifyItemMoved(start, end);
-                                adapter.notifyItemChanged(end);
-                                setEmptyVisiblity();
-                            }
-
-                            @Override
-                            public void onItemDelete(String key) {
-                                int position = snackoosList.removeByKey(key);
-                                adapter.notifyItemRemoved(position);
-                                setEmptyVisiblity();
-                            }
-                        });
+                        createTodoListListener());
             }
 
             protected void onSuccess(TodoListPersistence persistence) {
@@ -156,6 +106,71 @@
         }.execute(PersistenceFactory.mightGetTodoListPersistenceBlock());
     }
 
+    // Creates a listener for this activity. Visible to tests to allow them to invoke the listener
+    // methods directly.
+    @VisibleForTesting
+    TodoListListener createTodoListListener() {
+        return new TodoListListener() {
+            @Override
+            public void onUpdate(ListSpec value) {
+                snackoo = value;
+                getActionBar().setTitle(snackoo.getName());
+            }
+
+            @Override
+            public void onDelete() {
+                finish();
+            }
+
+            @Override
+            public void onUpdateShowDone(boolean showDone) {
+                if (mShowDoneMenuItem != null) {
+                    // Only interact with mShowDoneMenu if it has been inflated.
+                    mShowDoneMenuItem.setChecked(showDone);
+                }
+
+                int oldSize = adapter.getItemCount();
+                adapter.setShowDone(showDone);
+                int newSize = adapter.getItemCount();
+                if (newSize > oldSize) {
+                    adapter.notifyItemRangeInserted(oldSize, newSize - oldSize);
+                } else {
+                    adapter.notifyItemRangeRemoved(newSize, oldSize - newSize);
+                }
+                setEmptyVisiblity();
+            }
+
+            @Override
+            public void onItemAdd(Task item) {
+                int position = snackoosList.insertInOrder(item);
+                adapter.notifyItemInserted(position);
+                setEmptyVisiblity();
+            }
+
+            @Override
+            public void onItemUpdate(Task item) {
+                int start = snackoosList.findIndexByKey(item.key);
+                int end = snackoosList.updateInOrder(item);
+                adapter.notifyItemMoved(start, end);
+                adapter.notifyItemChanged(end);
+                setEmptyVisiblity();
+            }
+
+            @Override
+            public void onItemDelete(String key) {
+                int position = snackoosList.removeByKey(key);
+                adapter.notifyItemRemoved(position);
+                setEmptyVisiblity();
+            }
+        };
+    }
+
+    // Allow the tests to mock out the main persistence.
+    @VisibleForTesting
+    void setTodoListPersistence(TodoListPersistence p) {
+        mPersistence = p;
+    }
+
     // Set the visibility based on what the adapter thinks is the visible item count.
     private void setEmptyVisiblity() {
         View v = findViewById(R.id.empty);
diff --git a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
index 5ceaba7..9210238 100644
--- a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
@@ -12,7 +12,8 @@
 import io.v.todos.model.TaskSpec;
 
 public final class PersistenceFactory {
-    private PersistenceFactory(){}
+    private PersistenceFactory() {
+    }
 
     /**
      * Indicates whether {@link #getMainPersistence(Activity, ListEventListener)} may block. This
@@ -49,38 +50,49 @@
 
     static class MockMainPersistence implements MainPersistence {
         @Override
-        public void addTodoList(ListSpec listSpec) {}
+        public void addTodoList(ListSpec listSpec) {
+        }
 
         @Override
-        public void deleteTodoList(String key) {}
+        public void deleteTodoList(String key) {
+        }
 
         @Override
-        public void setCompletion(ListMetadata listMetadata, boolean done) {}
+        public void setCompletion(ListMetadata listMetadata, boolean done) {
+        }
 
         @Override
-        public void close() {}
+        public void close() {
+        }
     }
 
     static class MockTodoListPersistence implements TodoListPersistence {
         @Override
-        public void updateTodoList(ListSpec listSpec) {}
+        public void updateTodoList(ListSpec listSpec) {
+        }
 
         @Override
-        public void deleteTodoList() {}
+        public void deleteTodoList() {
+        }
 
         @Override
-        public void addTask(TaskSpec task) {}
+        public void addTask(TaskSpec task) {
+        }
 
         @Override
-        public void updateTask(Task task) {}
+        public void updateTask(Task task) {
+        }
 
         @Override
-        public void deleteTask(String key) {}
+        public void deleteTask(String key) {
+        }
 
         @Override
-        public void setShowDone(boolean showDone) {}
+        public void setShowDone(boolean showDone) {
+        }
 
         @Override
-        public void close() {}
+        public void close() {
+        }
     }
 }