diff --git a/README.md b/README.md index 69bcbc36..2d34c781 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ clearText(R.id.edittext) ```java clickListItem(R.id.list, 4); clickListItemChild(R.id.list, 3, R.id.row_button); +doOnListItemChild(R.id.list, 5, R.id.edittext, replaceText("Yet another great text")); scrollListToPosition(R.id.list, 4); clickSpinnerItem(R.id.spinner, 1); diff --git a/library/src/main/java/com/adevinta/android/barista/interaction/BaristaListInteractions.kt b/library/src/main/java/com/adevinta/android/barista/interaction/BaristaListInteractions.kt index 3c76631b..84e27675 100644 --- a/library/src/main/java/com/adevinta/android/barista/interaction/BaristaListInteractions.kt +++ b/library/src/main/java/com/adevinta/android/barista/interaction/BaristaListInteractions.kt @@ -13,6 +13,7 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.FailureHandler import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition @@ -23,8 +24,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import com.adevinta.android.barista.internal.failurehandler.SpyFailureHandler import com.adevinta.android.barista.internal.failurehandler.description import com.adevinta.android.barista.internal.failurehandler.withFailureHandler -import com.adevinta.android.barista.internal.viewaction.ClickChildAction.clickChildWithId import com.adevinta.android.barista.internal.viewaction.PerformClickAction.clickUsingPerformClick +import com.adevinta.android.barista.internal.viewaction.ChildActions.onChildWithId import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anything @@ -53,9 +54,15 @@ object BaristaListInteractions { @JvmStatic @JvmOverloads fun clickListItemChild(@IdRes id: Int? = null, position: Int, @IdRes childId: Int) { + doOnListItemChild(id, position, childId, click()) + } + + @JvmStatic + @JvmOverloads + fun doOnListItemChild(@IdRes id: Int? = null, position: Int, @IdRes childId: Int, viewAction: ViewAction) { performMagicAction(id, position, - recyclerAction = actionOnItemAtPosition(position, clickChildWithId(childId)), - listViewAction = clickChildWithId(childId) + recyclerAction = actionOnItemAtPosition(position, onChildWithId(childId, viewAction)), + listViewAction = onChildWithId(childId, viewAction) ) } diff --git a/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ChildActions.kt b/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ChildActions.kt new file mode 100644 index 00000000..f70342aa --- /dev/null +++ b/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ChildActions.kt @@ -0,0 +1,56 @@ +package com.adevinta.android.barista.internal.viewaction + +import android.view.View +import androidx.annotation.IdRes +import androidx.test.espresso.PerformException +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.util.HumanReadables +import com.adevinta.android.barista.internal.failurehandler.description +import com.adevinta.android.barista.internal.util.ViewTreeAnalyzer +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf + +object ChildActions { + + @JvmStatic + fun onChildWithId(@IdRes childId: Int, viewAction: ViewAction): ViewAction { + return onChildWithMatcher(withId(childId), viewAction) + } + + @JvmStatic + fun onChildWithMatcher(childMatcher: Matcher, viewAction: ViewAction): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher { + return allOf(isDisplayed(), hasDescendant(childMatcher)) + } + + override fun getDescription(): String { + return "Perform " + viewAction.description + " on a child view " + childMatcher.description() + } + + override fun perform(uiController: UiController, view: View) { + var foundTarget = false + + for (child in ViewTreeAnalyzer.getAllChildren(view)) { + if (childMatcher.matches(child)) { + foundTarget = true + viewAction.perform(uiController, child) + break + } + } + + if (!foundTarget) { + throw PerformException.Builder() + .withActionDescription(description) + .withViewDescription(HumanReadables.describe(view)) + .withCause(IllegalArgumentException("Didn't find any view " + childMatcher.description())) + .build() + } + } + } + } +} diff --git a/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ClickChildAction.java b/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ClickChildAction.java deleted file mode 100644 index e5d545d9..00000000 --- a/library/src/main/java/com/adevinta/android/barista/internal/viewaction/ClickChildAction.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.adevinta.android.barista.internal.viewaction; - -import android.view.View; -import android.widget.TextView; -import androidx.annotation.IdRes; -import androidx.test.espresso.PerformException; -import androidx.test.espresso.UiController; -import androidx.test.espresso.ViewAction; -import androidx.test.espresso.util.HumanReadables; -import com.adevinta.android.barista.internal.failurehandler.HelperFunctionsKt; -import com.adevinta.android.barista.internal.util.ViewTreeAnalyzer; -import org.hamcrest.Matcher; -import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static com.adevinta.android.barista.internal.matcher.TextMatcherKt.withCompatText; -import static org.hamcrest.Matchers.allOf; - -public class ClickChildAction { - - public static ViewAction clickChildWithId(@IdRes final int childId) { - - final Matcher childMatcher = withId(childId); - - return new ViewAction() { - @Override - public Matcher getConstraints() { - return allOf(isDisplayed(), hasDescendant(childMatcher)); - } - - @Override - public String getDescription() { - return "Click on a child view " + HelperFunctionsKt.description(childMatcher); - } - - @Override - public void perform(UiController uiController, View view) { - View child = view.findViewById(childId); - if (child != null) { - child.performClick(); - } else { - throw new PerformException.Builder() - .withActionDescription(getDescription()) - .withViewDescription(HumanReadables.describe(view)) - .withCause(new IllegalArgumentException("Didn't find any view " + HelperFunctionsKt.description(childMatcher))) - .build(); - } - } - }; - } - - public static ViewAction clickChildwithCompatText(final String text) { - return new ViewAction() { - @Override - public Matcher getConstraints() { - return allOf(isDisplayed(), hasDescendant(withCompatText(text))); - } - - @Override - public String getDescription() { - return "Click on a child View with specified text"; - } - - @Override - public void perform(UiController uiController, View view) { - for (View child : ViewTreeAnalyzer.getAllChildren(view)) { - if (child instanceof TextView) { - TextView textView = (TextView) child; - String label = textView.getText().toString(); - if (text.equalsIgnoreCase(label)) { - textView.performClick(); - return; - } - } - } - } - }; - } -} \ No newline at end of file diff --git a/sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildClickTest.java b/sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildTest.java similarity index 64% rename from sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildClickTest.java rename to sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildTest.java index a7270c39..5160cf2c 100644 --- a/sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildClickTest.java +++ b/sample/src/androidTest/java/com/adevinta/android/barista/sample/ListsChildTest.java @@ -11,16 +11,18 @@ import org.junit.runner.RunWith; import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.adevinta.android.barista.interaction.BaristaKeyboardInteractions.closeKeyboard; import static com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItemChild; -import static com.adevinta.android.barista.sample.ListsActivity.IntentBuilder; +import static com.adevinta.android.barista.interaction.BaristaListInteractions.doOnListItemChild; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @RunWith(AndroidJUnit4.class) -public class ListsChildClickTest { +public class ListsChildTest { @Rule public ActivityTestRule activity = new ActivityTestRule<>(ListsActivity.class, true, false); @@ -36,7 +38,7 @@ public void clickRecyclerItemChild() { clickListItemChild(20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -48,7 +50,7 @@ public void clickListViewItemChild() { clickListItemChild(20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -60,7 +62,7 @@ public void clickGridViewItemChild() { clickListItemChild(20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -72,7 +74,7 @@ public void clickMultipleRecyclerItemChild_byId() { clickListItemChild(R.id.recycler, 20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -84,7 +86,7 @@ public void clickMultipleListViewItemChild_byId() { clickListItemChild(R.id.listview, 20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -96,7 +98,46 @@ public void clickMultipleGridViewItemChild_byId() { clickListItemChild(R.id.gridview, 20, R.id.yes); - assertResult("yes"); + assertClickResult("yes"); + spyFailureHandlerRule.assertNoEspressoFailures(); + } + + @Test + public void actionMultipleRecyclerItemChild_byId() { + openActivity(ListsActivity.buildIntent() + .withRecyclers(R.id.recycler, R.id.recycler2) + ); + + String text = "It works"; + doOnListItemChild(R.id.recycler, 20, R.id.edittext, replaceText(text)); + + assertPerformActionResult(text); + spyFailureHandlerRule.assertNoEspressoFailures(); + } + + @Test + public void actionMultipleListViewItemChild_byId() { + openActivity(ListsActivity.buildIntent() + .withComplexLists(R.id.listview, R.id.listview2) + ); + + String text = "It works"; + doOnListItemChild(R.id.listview, 20, R.id.edittext, replaceText(text)); + + assertPerformActionResult(text); + spyFailureHandlerRule.assertNoEspressoFailures(); + } + + @Test + public void actionMultipleGridViewItemChild_byId() { + openActivity(ListsActivity.buildIntent() + .withComplexGrids(R.id.gridview, R.id.gridview2) + ); + + String text = "It works"; + doOnListItemChild(R.id.gridview, 20, R.id.edittext, replaceText(text)); + + assertPerformActionResult(text); spyFailureHandlerRule.assertNoEspressoFailures(); } @@ -110,7 +151,8 @@ public void fails_whenRecyclerChildNotExist() { spyFailureHandlerRule.assertEspressoFailures(1); assertThat(thrown).isInstanceOf(BaristaException.class) - .hasMessageContaining("Could not perform action (actionOnItemAtPosition performing ViewAction: Click on a child view ") + .hasMessageContaining( + "Could not perform action (actionOnItemAtPosition performing ViewAction: Perform single click on a child view with id: ") .hasMessageContaining("on item at position: 20) on RecyclerView") .hasCauseInstanceOf(PerformException.class) .hasStackTraceContaining("Didn't find any view with id"); @@ -126,16 +168,21 @@ public void fails_whenListViewChildNotExist() { spyFailureHandlerRule.assertEspressoFailures(1); assertThat(thrown).isInstanceOf(BaristaException.class) - .hasMessageContaining("Could not perform action (Click on a child view ") + .hasMessageContaining("Could not perform action (Perform single click on a child view with id: ") .hasMessageContaining("on ListView") .hasCauseInstanceOf(PerformException.class); } - private void openActivity(IntentBuilder intentBuilder) { + private void openActivity(ListsActivity.IntentBuilder intentBuilder) { activity.launchActivity(intentBuilder.build(ApplicationProvider.getApplicationContext())); + closeKeyboard(); } - private void assertResult(String text) { + private void assertClickResult(String text) { onView(withId(R.id.clicked_text_result)).check(matches(withText(text))); } + + private void assertPerformActionResult(String text) { + onView(withId(R.id.typed_text_result)).check(matches(withText(text))); + } } diff --git a/sample/src/main/java/com/adevinta/android/barista/sample/ListsActivity.java b/sample/src/main/java/com/adevinta/android/barista/sample/ListsActivity.java index 3031e8fd..745b77a3 100644 --- a/sample/src/main/java/com/adevinta/android/barista/sample/ListsActivity.java +++ b/sample/src/main/java/com/adevinta/android/barista/sample/ListsActivity.java @@ -3,6 +3,8 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -14,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.jetbrains.annotations.NotNull; /** * This Activity helps testing a variety of scenarios with different ListView and RecyclerView. @@ -61,6 +64,7 @@ public static IntentBuilder buildIntent() { private LinearLayout listsContainer; private TextView clickedResult; + private TextView typedResult; @Override protected void onCreate(Bundle savedInstanceState) { @@ -68,6 +72,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_lists); listsContainer = (LinearLayout) findViewById(R.id.multi_list_container); clickedResult = (TextView) findViewById(R.id.clicked_text_result); + typedResult = (TextView) findViewById(R.id.typed_text_result); for (int id : getIntent().getIntArrayExtra(EXTRA_SIMPLE_LISTS)) { addSimpleListView(id); @@ -104,7 +109,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) private void addComplexListView(int id) { ListView listView = new ListView(this); listView.setId(id); - listView.setAdapter(new TextListViewAdapter(this, FRUITS, clickedResult)); + listView.setAdapter(new TextListViewAdapter(this, FRUITS, clickedResult, createTypedResultTextWatcher())); addList(listView); } @@ -115,7 +120,7 @@ private void addRecyclerView(int id) { LinearLayoutManager mLayoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(mLayoutManager); - recyclerView.setAdapter(new TextRecyclerViewAdapter(FRUITS, clickedResult)); + recyclerView.setAdapter(new TextRecyclerViewAdapter(FRUITS, clickedResult, createTypedResultTextWatcher())); addList(recyclerView); } @@ -135,7 +140,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) private void addComplexGridView(int id) { GridView gridView = new GridView(this); gridView.setId(id); - gridView.setAdapter(new TextListViewAdapter(this, FRUITS, clickedResult)); + gridView.setAdapter(new TextListViewAdapter(this, FRUITS, clickedResult, createTypedResultTextWatcher())); addList(gridView); } @@ -148,6 +153,24 @@ private void addList(View listView) { )); } + @NotNull + private TextWatcher createTypedResultTextWatcher() { + return new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + typedResult.setText(s); + } + }; + } + public static class IntentBuilder { private int[] simpleListViewIds = {}; private int[] complexListViewIds = {}; diff --git a/sample/src/main/java/com/adevinta/android/barista/sample/TextListViewAdapter.java b/sample/src/main/java/com/adevinta/android/barista/sample/TextListViewAdapter.java index ec9a560e..77d9101a 100644 --- a/sample/src/main/java/com/adevinta/android/barista/sample/TextListViewAdapter.java +++ b/sample/src/main/java/com/adevinta/android/barista/sample/TextListViewAdapter.java @@ -2,10 +2,12 @@ import android.app.Activity; import android.content.Context; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; @@ -13,11 +15,13 @@ public class TextListViewAdapter extends BaseAdapter { private Activity activity; private String[] items; private final TextView clickedResult; + private final TextWatcher textWatcher; - TextListViewAdapter(Activity activity, String[] items, TextView clickedResult) { + TextListViewAdapter(Activity activity, String[] items, TextView clickedResult, TextWatcher textWatcher) { this.activity = activity; this.items = items.clone(); this.clickedResult = clickedResult; + this.textWatcher = textWatcher; } @Override @@ -47,6 +51,8 @@ public View getView(final int position, View convertView, ViewGroup parent) { TextView textView = (TextView) rowView.findViewById(R.id.textview); View yesButton = rowView.findViewById(R.id.yes); View noButton = rowView.findViewById(R.id.no); + EditText editText = rowView.findViewById(R.id.edittext); + textView.setText(items[position]); ImageView imageView = rowView.findViewById(R.id.imageview); @@ -75,6 +81,10 @@ public void onClick(View view) { } }); + // Avoiding adding TextWatcher multiple times + editText.removeTextChangedListener(textWatcher); + editText.addTextChangedListener(textWatcher); + return rowView; } } diff --git a/sample/src/main/java/com/adevinta/android/barista/sample/TextRecyclerViewAdapter.java b/sample/src/main/java/com/adevinta/android/barista/sample/TextRecyclerViewAdapter.java index 97fd42f6..7fca3daa 100644 --- a/sample/src/main/java/com/adevinta/android/barista/sample/TextRecyclerViewAdapter.java +++ b/sample/src/main/java/com/adevinta/android/barista/sample/TextRecyclerViewAdapter.java @@ -1,8 +1,10 @@ package com.adevinta.android.barista.sample; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; @@ -11,10 +13,12 @@ public class TextRecyclerViewAdapter extends RecyclerView.Adapter { private final String[] items; private final TextView clickedResult; + private final TextWatcher textWatcher; - TextRecyclerViewAdapter(String[] items, TextView clickedResult) { + TextRecyclerViewAdapter(String[] items, TextView clickedResult, TextWatcher textWatcher) { this.items = items.clone(); this.clickedResult = clickedResult; + this.textWatcher = textWatcher; } public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @@ -22,7 +26,8 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { TextView textView = (TextView) root.findViewById(R.id.textview); View yesButton = root.findViewById(R.id.yes); View noButton = root.findViewById(R.id.no); - return new ViewHolder(root, textView, yesButton, noButton); + EditText editText = root.findViewById(R.id.edittext); + return new ViewHolder(root, textView, yesButton, noButton, editText, textWatcher); } public void onBindViewHolder(final ViewHolder holder, final int position) { @@ -55,12 +60,18 @@ static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; View yesButton; View noButton; + EditText editText; - ViewHolder(View root, TextView textView, View yesButton, View noButton) { + ViewHolder(View root, TextView textView, View yesButton, View noButton, EditText editText, TextWatcher textWatcher) { super(root); this.textView = textView; this.yesButton = yesButton; this.noButton = noButton; + this.editText = editText; + + // Avoiding adding TextWatcher multiple times + this.editText.removeTextChangedListener(textWatcher); + this.editText.addTextChangedListener(textWatcher); } } } diff --git a/sample/src/main/res/layout/activity_lists.xml b/sample/src/main/res/layout/activity_lists.xml index 20f74f0b..66e2dc0f 100644 --- a/sample/src/main/res/layout/activity_lists.xml +++ b/sample/src/main/res/layout/activity_lists.xml @@ -11,6 +11,12 @@ android:layout_height="wrap_content" /> + + + +