diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java index 91417a656..c29fdfabc 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java @@ -2,20 +2,22 @@ package de.dennisguse.opentracks; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; -import static androidx.test.espresso.action.ViewActions.longClick; import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; +import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup; import static de.dennisguse.opentracks.util.ActivityTestUtils.waitForResumedActivity; -import static de.dennisguse.opentracks.util.EspressoUtils.waitForView; +import static de.dennisguse.opentracks.util.EspressoUtils.performClick; +import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor; +import static de.dennisguse.opentracks.util.EspressoUtils.performLongClick; +import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedView; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition; import static de.dennisguse.opentracks.util.EspressoUtils.withRecyclerViewItemCount; @@ -66,11 +68,10 @@ public class EspressoDeleteTrackTest { // TrackStoppedActivity: discard the just-recorded track directly onView(withId(R.id.discard_button)) - .perform(click()); - waitUntilDisplayedInCurrentRoot(withText(android.R.string.ok)); - + .perform(performClick()); onView(withText(android.R.string.ok)) - .perform(click()); + .inRoot(isDialog()) + .perform(performClick()); waitForResumedActivity(TrackListActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_list)); waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0); @@ -89,20 +90,19 @@ public class EspressoDeleteTrackTest { .perform(scrollTo(), replaceText(uniqueTrackName), closeSoftKeyboard()); onView(withId(R.id.finish_button)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackListActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_list)); waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName))); waitUntilRecyclerViewItemCount(withId(R.id.track_list), 1); onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName))) - .perform(longClick()); + .perform(performLongClickOnAncestor()); clickDeleteSelectedTrackAction(); - waitUntilDisplayedInCurrentRoot(withText(android.R.string.ok)); - onView(withText(android.R.string.ok)) - .perform(click()); + .inRoot(isDialog()) + .perform(performClick()); waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0); onView(withId(R.id.track_list)) @@ -112,14 +112,14 @@ public class EspressoDeleteTrackTest { private void recordTrackAndOpenTrackStoppedActivity() { // TrackListActivity: start recording onView(withId(R.id.track_list_fab_action)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackRecordingActivity.class, UI_TIMEOUT); waitForRecordingState(true, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_recording_fab_action)); // TrackRecordingActivity: stop recording and open TrackStoppedActivity onView(withId(R.id.track_recording_fab_action)) - .perform(longClick()); + .perform(performLongClick()); waitForResumedActivity(TrackStoppedActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_edit_name)); waitUntilDisplayed(withId(R.id.discard_button)); @@ -132,15 +132,15 @@ public class EspressoDeleteTrackTest { try { waitUntilDisplayed(withId(R.id.list_context_menu_delete)); onView(withId(R.id.list_context_menu_delete)) - .perform(click()); + .perform(performClick()); } catch (AssertionError ignored) { onView(allOf( withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description), isDescendantOfA(withId(androidx.appcompat.R.id.action_mode_bar)))) - .perform(click()); - waitUntilDisplayedInCurrentRoot(withText(R.string.menu_delete)); + .perform(performClick()); onView(withText(R.string.menu_delete)) - .perform(click()); + .inRoot(isPlatformPopup()) + .perform(performClickOnAncestor()); } } @@ -148,10 +148,6 @@ public class EspressoDeleteTrackTest { waitForResumedView(allOf(viewMatcher, isDisplayed()), UI_TIMEOUT); } - private void waitUntilDisplayedInCurrentRoot(Matcher viewMatcher) { - onView(isRoot()).perform(waitForView(allOf(viewMatcher, isDisplayed()), UI_TIMEOUT)); - } - private void waitUntilRecyclerViewItemCount(Matcher viewMatcher, int expectedCount) { waitForResumedViewCondition( viewMatcher, diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoEditTrackRecordingTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoEditTrackRecordingTest.java index a80b08ea9..fcfeabdd1 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoEditTrackRecordingTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoEditTrackRecordingTest.java @@ -2,18 +2,20 @@ package de.dennisguse.opentracks; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; -import static androidx.test.espresso.action.ViewActions.longClick; import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.dennisguse.opentracks.util.ActivityTestUtils.waitForResumedActivity; +import static de.dennisguse.opentracks.util.EspressoUtils.performClick; +import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor; +import static de.dennisguse.opentracks.util.EspressoUtils.performLongClick; import static de.dennisguse.opentracks.util.EspressoUtils.waitForView; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedView; import static de.dennisguse.opentracks.util.TrackRecordingServiceTestUtils.waitForRecordingState; @@ -67,7 +69,7 @@ public class EspressoEditTrackRecordingTest { { // TrackListActivity: start recording onView(withId(R.id.track_list_fab_action)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackRecordingActivity.class, UI_TIMEOUT); waitForRecordingState(true, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_recording_fab_action)); @@ -76,12 +78,15 @@ public class EspressoEditTrackRecordingTest { // TrackRecordingActivity // open menu - openContextualActionModeOverflowMenu(); + onView(allOf( + withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description), + isDescendantOfA(withId(R.id.bottom_app_bar)))) + .perform(performClick()); waitUntilDisplayedInCurrentRoot(withText(R.string.menu_edit)); // Click the item. onView(withText(R.string.menu_edit)) - .perform(click()); + .perform(performClickOnAncestor()); waitUntilDisplayed(withId(R.id.track_edit_name)); // change name for "New Name" @@ -93,12 +98,12 @@ public class EspressoEditTrackRecordingTest { // save edition onView(withId(R.id.track_edit_save)) - .perform(click()); + .perform(performClick()); waitUntilDisplayed(withId(R.id.track_recording_fab_action)); // stop; onView(withId(R.id.track_recording_fab_action)) - .perform(longClick()); + .perform(performLongClick()); waitForResumedActivity(TrackStoppedActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.resume_button)); waitUntilDisplayed(withId(R.id.finish_button)); diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java index fea4b26dc..b0206ac32 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java @@ -1,15 +1,16 @@ package de.dennisguse.opentracks; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; -import static androidx.test.espresso.action.ViewActions.longClick; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isClickable; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.dennisguse.opentracks.util.ActivityTestUtils.waitForResumedActivity; +import static de.dennisguse.opentracks.util.EspressoUtils.performClick; +import static de.dennisguse.opentracks.util.EspressoUtils.performLongClick; +import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor; import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedView; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition; @@ -34,6 +35,7 @@ import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; import org.junit.runner.RunWith; import java.time.Duration; @@ -50,11 +52,12 @@ public class EspressoUITest { private static final Duration UI_TIMEOUT = Duration.ofSeconds(10); - @Rule - public ActivityScenarioRule mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); + private final ActivityScenarioRule activityRule = new ActivityScenarioRule<>(TrackListActivity.class); + private final GrantPermissionRule grantPermissionRule = TestUtil.createGrantPermissionRule(); @Rule - public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule(); + public RuleChain rules = RuleChain.outerRule(grantPermissionRule) + .around(activityRule); @Before public void setUp() { @@ -69,7 +72,7 @@ public class EspressoUITest { { // TrackListActivity: start recording onView(withId(R.id.track_list_fab_action)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackRecordingActivity.class, UI_TIMEOUT); waitForRecordingState(true, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_recording_fab_action)); @@ -77,7 +80,7 @@ public class EspressoUITest { { // TrackRecordingActivity: stop onView(withId(R.id.track_recording_fab_action)) - .perform(longClick()); + .perform(performLongClick()); waitForResumedActivity(TrackStoppedActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_edit_name)); waitUntilDisplayed(withId(R.id.resume_button)); @@ -86,14 +89,14 @@ public class EspressoUITest { // TrackStoppedActivity: resume onView(allOf(withId(R.id.resume_button), isClickable())) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackRecordingActivity.class, UI_TIMEOUT); waitForRecordingState(true, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_recording_fab_action)); // TrackRecordingActivity: stop again onView(withId(R.id.track_recording_fab_action)) - .perform(longClick()); + .perform(performLongClick()); waitForResumedActivity(TrackStoppedActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_edit_name)); waitUntilDisplayed(withId(R.id.finish_button)); @@ -102,7 +105,7 @@ public class EspressoUITest { // TrackStoppedActivity onView(withId(R.id.finish_button)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackListActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_list)); } @@ -114,7 +117,7 @@ public class EspressoUITest { { // TrackListActivity: start recording onView(withId(R.id.track_list_fab_action)) - .perform(click()); + .perform(performClick()); waitForResumedActivity(TrackRecordingActivity.class, UI_TIMEOUT); waitForRecordingState(true, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.track_detail_activity_tablayout)); @@ -138,7 +141,7 @@ public class EspressoUITest { // stop onView(withId(R.id.track_recording_fab_action)) - .perform(longClick()); + .perform(performLongClick()); waitForResumedActivity(TrackStoppedActivity.class, UI_TIMEOUT); waitUntilDisplayed(withId(R.id.resume_button)); } @@ -152,11 +155,12 @@ public class EspressoUITest { TrackBuilder trackBuilder = new TrackBuilder(pair.first); trackBuilder.setName(uniqueTrackName); TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second); - mActivityTestRule.getScenario().recreate(); + activityRule.getScenario().recreate(); waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName))); onView(withId(R.id.track_list)).check(matches(isDisplayed())); - onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName))).perform(longClick()); + onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName))) + .perform(performLongClickOnAncestor()); } private void waitUntilDisplayed(Matcher viewMatcher) { diff --git a/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/EspressoAggregatedFilterTest.java b/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/EspressoAggregatedFilterTest.java index e6cee6626..355acbddc 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/EspressoAggregatedFilterTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/EspressoAggregatedFilterTest.java @@ -1,18 +1,21 @@ package de.dennisguse.opentracks.ui.aggregatedStatistics; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withParent; import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static de.dennisguse.opentracks.util.EspressoUtils.waitForView; +import static de.dennisguse.opentracks.util.EspressoUtils.performClick; import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedView; +import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition; +import static de.dennisguse.opentracks.util.EspressoUtils.withRecyclerViewItemCount; import static org.hamcrest.Matchers.allOf; +import android.content.Context; +import android.content.Intent; import android.util.Pair; import android.view.View; @@ -40,6 +43,7 @@ import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackBuilder; import de.dennisguse.opentracks.data.models.TrackPoint; +import de.dennisguse.opentracks.services.TrackRecordingService; @LargeTest @RunWith(AndroidJUnit4.class) @@ -56,8 +60,11 @@ public class EspressoAggregatedFilterTest { @Before public void setUp() { - ContentProviderUtils contentProviderUtils = new ContentProviderUtils(ApplicationProvider.getApplicationContext()); - contentProviderUtils.deleteAllTracks(ApplicationProvider.getApplicationContext()); + Context context = ApplicationProvider.getApplicationContext(); + context.stopService(new Intent(context, TrackRecordingService.class)); + + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + contentProviderUtils.deleteAllTracks(context); Pair> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20); TrackBuilder trackBuilder = new TrackBuilder(pair.first); @@ -69,6 +76,8 @@ public class EspressoAggregatedFilterTest { @After public void tearDown() { + Context context = ApplicationProvider.getApplicationContext(); + context.stopService(new Intent(context, TrackRecordingService.class)); if (scenario != null) { scenario.close(); } @@ -78,6 +87,8 @@ public class EspressoAggregatedFilterTest { public void espressoAggregatedFilterTest() { waitUntilDisplayed(withId(R.id.bottom_app_bar)); waitUntilDisplayed(withId(R.id.aggregated_statistics_filter)); + waitUntilRecyclerViewItemCount(withId(R.id.aggregated_stats_list), 1); + waitUntilDisplayed(allOf(withId(R.id.aggregated_stats_type_label), withText(ACTIVITY_TYPE_LOCALIZED))); // open FilterDialogFragment through the current bottom app bar action item onView(withId(R.id.bottom_app_bar)) @@ -85,23 +96,25 @@ public class EspressoAggregatedFilterTest { onView(withId(R.id.bottom_app_bar)) .check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter)))); onView(withId(R.id.aggregated_statistics_filter)) - .perform(click()); + .perform(performClick()); // check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text Matcher activityTypeFilter = allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED), withParent(allOf(withId(R.id.filter_items), withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))), isDisplayed()); - waitUntilDisplayedInCurrentRoot(activityTypeFilter); onView(activityTypeFilter) + .inRoot(isDialog()) .check(matches(isDisplayed())); // check there's an edit text for "from date" onView(withId(R.id.filter_date_edit_text_from)) + .inRoot(isDialog()) .check(matches(isDisplayed())); // check there's an edit text for "to date" onView(withId(R.id.filter_date_edit_text_to)) + .inRoot(isDialog()) .check(matches(isDisplayed())); } @@ -109,7 +122,11 @@ public class EspressoAggregatedFilterTest { waitForResumedView(allOf(viewMatcher, isDisplayed()), UI_TIMEOUT); } - private void waitUntilDisplayedInCurrentRoot(Matcher viewMatcher) { - onView(isRoot()).perform(waitForView(allOf(viewMatcher, isDisplayed()), UI_TIMEOUT)); + private void waitUntilRecyclerViewItemCount(Matcher viewMatcher, int expectedCount) { + waitForResumedViewCondition( + allOf(viewMatcher, isDisplayed()), + view -> withRecyclerViewItemCount(expectedCount).matches(view), + "RecyclerView item count to be " + expectedCount, + UI_TIMEOUT); } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java b/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java index 6c6d539cb..0ac4df014 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java @@ -8,6 +8,7 @@ import android.os.SystemClock; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.AdapterView; import android.widget.ListView; import androidx.recyclerview.widget.RecyclerView; @@ -221,4 +222,135 @@ public class EspressoUtils { }; } + public static ViewAction performClick() { + return new ViewAction() { + @Override + public String getDescription() { + return "invoke View.performClick()"; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, View view) { + if (!view.performClick()) { + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .build(); + } + uiController.loopMainThreadUntilIdle(); + } + }; + } + + public static ViewAction performClickOnAncestor() { + return new ViewAction() { + @Override + public String getDescription() { + return "invoke performClick() on the matched view or its ancestor"; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, View view) { + View current = view; + while (current != null) { + if (current.performClick()) { + uiController.loopMainThreadUntilIdle(); + return; + } + + ViewParent parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + + current = view; + while (current != null) { + ViewParent parent = current.getParent(); + if (parent instanceof AdapterView adapterView) { + int position = adapterView.getPositionForView(current); + if (position != AdapterView.INVALID_POSITION + && adapterView.performItemClick(current, position, adapterView.getAdapter().getItemId(position))) { + uiController.loopMainThreadUntilIdle(); + return; + } + } + + current = parent instanceof View ? (View) parent : null; + } + + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .build(); + } + }; + } + + public static ViewAction performLongClick() { + return new ViewAction() { + @Override + public String getDescription() { + return "invoke View.performLongClick()"; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, View view) { + if (!view.performLongClick()) { + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .build(); + } + uiController.loopMainThreadUntilIdle(); + } + }; + } + + public static ViewAction performLongClickOnAncestor() { + return new ViewAction() { + @Override + public String getDescription() { + return "invoke performLongClick() on the matched view or its ancestor"; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, View view) { + View current = view; + while (current != null) { + if (current.performLongClick()) { + uiController.loopMainThreadUntilIdle(); + return; + } + + ViewParent parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .build(); + } + }; + } + }