mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Relations | Fix | Filter create option for tags that already exist (#2237)
* Relations | Fix | Filter create option for tags that already exist * Tech | Fix | Change compile android tests task * Tech | Fix | add build folders ignore to global .gitignore * Tech | Fix | Extract MockDataFactory.kt from test-utils * Tech | Fix | Move return to the same line as checkNotNull * Tech | Fix | Delete unused analytics property * Relations | Fix | Add AddObjectRelationValueViewModelTest.kt test query with tags behaviour * Tech | Fix | Rename :test:utils to :test:android-utils * Tech | Enhancement | implement fake providers for AddObjectRelationViewModerl
This commit is contained in:
parent
8e678c95d4
commit
58da3badfe
37 changed files with 770 additions and 469 deletions
30
test/android-utils/build.gradle
Normal file
30
test/android-utils/build.gradle
Normal file
|
@ -0,0 +1,30 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion compile_sdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion min_sdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation mainApplication.appcompat
|
||||
implementation mainApplication.kotlin
|
||||
implementation mainApplication.coroutinesAndroid
|
||||
implementation mainApplication.androidxCore
|
||||
implementation acceptanceTesting.espressoCore
|
||||
|
||||
implementation mainApplication.design
|
||||
implementation mainApplication.recyclerView
|
||||
}
|
1
test/android-utils/src/main/AndroidManifest.xml
Normal file
1
test/android-utils/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1 @@
|
|||
<manifest package="com.anytypeio.anytype.test_utils" />
|
|
@ -0,0 +1,17 @@
|
|||
package com.anytypeio.anytype.test_utils
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
class TestFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.blank, container, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package com.anytypeio.anytype.test_utils.utils
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.ViewInteraction
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import com.anytypeio.anytype.test_utils.utils.TestUtils.withRecyclerView
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.HasChildViewWithText
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildViewWithText
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.WithBackgroundColor
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.WithChildViewCount
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.WithTextColor
|
||||
import com.anytypeio.anytype.test_utils.utils.espresso.WithoutBackgroundColor
|
||||
import org.hamcrest.Matchers.not
|
||||
|
||||
fun Int.findItemAt(position: Int, layoutId: Int): ViewInteraction {
|
||||
return onView(withRecyclerView(this).atPositionOnView(position, layoutId))
|
||||
}
|
||||
|
||||
fun ViewInteraction.performClick(): ViewInteraction = perform(ViewActions.click())
|
||||
fun Int.matchView(): ViewInteraction = onView(withId(this))
|
||||
fun Int.performClick(): ViewInteraction = matchView().performClick()
|
||||
fun Int.type(text: String) = matchView().perform(click(), typeText(text))
|
||||
|
||||
fun ViewInteraction.checkHasText(text: String) {
|
||||
check(matches(ViewMatchers.withText(text)))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsSelected() {
|
||||
check(matches(ViewMatchers.isSelected()))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsFocused() {
|
||||
check(matches(ViewMatchers.isFocused()))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsNotFocused() {
|
||||
check(matches(not(ViewMatchers.isFocused())))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsDisplayed() {
|
||||
check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsNotDisplayed() {
|
||||
check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkIsNotSelected() {
|
||||
check(matches(not(ViewMatchers.isSelected())))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasText(resId: Int) {
|
||||
check(matches(ViewMatchers.withText(resId)))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasTextColor(color: Int) {
|
||||
check(matches(WithTextColor(color)))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasBackgroundColor(color: Int) {
|
||||
check(matches(WithBackgroundColor(color)))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasNoBackground() {
|
||||
check(matches(WithoutBackgroundColor()))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasChildViewCount(count: Int) : ViewInteraction {
|
||||
return check(matches(WithChildViewCount(count)))
|
||||
}
|
||||
|
||||
fun Int.rVMatcher(): RecyclerViewMatcher =
|
||||
RecyclerViewMatcher(this)
|
||||
|
||||
fun Int.checkRecyclerItemCount(expected: Int) = matchView().check(RecyclerViewItemCountAssertion(expected))
|
||||
|
||||
fun RecyclerViewMatcher.onItemView(pos: Int, target: Int): ViewInteraction {
|
||||
return onView(atPositionOnView(pos, target))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasChildViewWithText(
|
||||
pos: Int,
|
||||
text: String,
|
||||
target: Int
|
||||
) : ViewInteraction {
|
||||
return check(matches(HasChildViewWithText(pos, text, target)))
|
||||
}
|
||||
|
||||
fun ViewInteraction.checkHasViewGroupChildWithText(
|
||||
pos: Int,
|
||||
text: String
|
||||
) : ViewInteraction {
|
||||
return check(matches(HasViewGroupChildViewWithText(pos, text)))
|
||||
}
|
||||
|
||||
fun RecyclerViewMatcher.onItem(pos: Int): ViewInteraction {
|
||||
return onView(atPosition(pos))
|
||||
}
|
||||
|
||||
fun RecyclerViewMatcher.checkIsRecyclerSize(expected: Int) {
|
||||
recyclerViewId.matchView().check(RecyclerViewItemCountAssertion(expected))
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.anytypeio.anytype.test_utils.utils
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.ViewAssertion
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import org.hamcrest.CoreMatchers
|
||||
|
||||
class RecyclerViewItemCountAssertion(val expected: Int) : ViewAssertion {
|
||||
override fun check(view: View?, noViewFoundException: NoMatchingViewException?) {
|
||||
if (noViewFoundException != null) throw noViewFoundException
|
||||
val recycler = view as RecyclerView
|
||||
val adapter = recycler.adapter
|
||||
checkNotNull(adapter) { "Adapter wasn't set" }
|
||||
ViewMatchers.assertThat(adapter.itemCount, CoreMatchers.`is`(expected))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.anytypeio.anytype.test_utils.utils;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
public class RecyclerViewMatcher {
|
||||
|
||||
final int recyclerViewId;
|
||||
|
||||
public RecyclerViewMatcher(int recyclerViewId) {
|
||||
this.recyclerViewId = recyclerViewId;
|
||||
}
|
||||
|
||||
public Matcher<View> atPosition(final int position) {
|
||||
return atPositionOnView(position, -1);
|
||||
}
|
||||
|
||||
public Matcher<View> atPositionOnView(final int position, final int targetViewId) {
|
||||
|
||||
return new TypeSafeMatcher<View>() {
|
||||
Resources resources = null;
|
||||
View childView;
|
||||
|
||||
public void describeTo(Description description) {
|
||||
String idRecyclerDescription = Integer.toString(recyclerViewId);
|
||||
String idTargetViewDescription = Integer.toString(targetViewId);
|
||||
if (this.resources != null) {
|
||||
try {
|
||||
idRecyclerDescription = this.resources.getResourceName(recyclerViewId);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
idRecyclerDescription = String.format("%s (resource name not found)", recyclerViewId);
|
||||
}
|
||||
try {
|
||||
idTargetViewDescription = this.resources.getResourceName(targetViewId);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
idTargetViewDescription = String.format("%s (resource name not found)", targetViewId);
|
||||
}
|
||||
}
|
||||
description.appendText("\nwith id: [" + idTargetViewDescription + "] inside: [" + idRecyclerDescription + "]");
|
||||
}
|
||||
|
||||
public boolean matchesSafely(View view) {
|
||||
|
||||
this.resources = view.getResources();
|
||||
|
||||
if (childView == null) {
|
||||
RecyclerView recyclerView =
|
||||
view.getRootView().findViewById(recyclerViewId);
|
||||
if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
|
||||
RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(position);
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException(
|
||||
"No view holder found at position: " + position +
|
||||
". Actual child count: " + recyclerView.getChildCount()
|
||||
);
|
||||
}
|
||||
childView = holder.itemView;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetViewId == -1) {
|
||||
return view == childView;
|
||||
} else {
|
||||
View targetView = childView.findViewById(targetViewId);
|
||||
return view == targetView;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.anytypeio.anytype.test_utils.utils;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.matcher.ViewMatchers;
|
||||
import androidx.test.espresso.util.HumanReadables;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
public class TestUtils {
|
||||
|
||||
|
||||
public static <VH extends RecyclerView.ViewHolder> ViewAction actionOnItemViewAtPosition(int position,
|
||||
@IdRes
|
||||
int viewId,
|
||||
ViewAction viewAction) {
|
||||
return new ActionOnItemViewAtPositionViewAction(position, viewId, viewAction);
|
||||
}
|
||||
|
||||
private static final class ActionOnItemViewAtPositionViewAction<VH extends RecyclerView
|
||||
.ViewHolder>
|
||||
implements
|
||||
|
||||
ViewAction {
|
||||
private final int position;
|
||||
private final ViewAction viewAction;
|
||||
private final int viewId;
|
||||
|
||||
private ActionOnItemViewAtPositionViewAction(int position,
|
||||
@IdRes int viewId,
|
||||
ViewAction viewAction) {
|
||||
this.position = position;
|
||||
this.viewAction = viewAction;
|
||||
this.viewId = viewId;
|
||||
}
|
||||
|
||||
public Matcher<View> getConstraints() {
|
||||
return Matchers.allOf(new Matcher[]{
|
||||
ViewMatchers.isAssignableFrom(RecyclerView.class), ViewMatchers.isDisplayed()
|
||||
});
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return "actionOnItemAtPosition performing ViewAction: "
|
||||
+ this.viewAction.getDescription()
|
||||
+ " on item at position: "
|
||||
+ this.position;
|
||||
}
|
||||
|
||||
public void perform(UiController uiController, View view) {
|
||||
RecyclerView recyclerView = (RecyclerView) view;
|
||||
(new ScrollToPositionViewAction(this.position)).perform(uiController, view);
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
|
||||
View targetView = recyclerView.getChildAt(this.position).findViewById(this.viewId);
|
||||
|
||||
if (targetView == null) {
|
||||
throw (new PerformException.Builder()).withActionDescription(this.toString())
|
||||
.withViewDescription(
|
||||
|
||||
HumanReadables.describe(view))
|
||||
.withCause(new IllegalStateException(
|
||||
"No toView with id "
|
||||
+ this.viewId
|
||||
+ " found at position: "
|
||||
+ this.position))
|
||||
.build();
|
||||
} else {
|
||||
this.viewAction.perform(uiController, targetView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScrollToPositionViewAction implements ViewAction {
|
||||
private final int position;
|
||||
|
||||
private ScrollToPositionViewAction(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public Matcher<View> getConstraints() {
|
||||
return Matchers.allOf(new Matcher[]{
|
||||
ViewMatchers.isAssignableFrom(RecyclerView.class), ViewMatchers.isDisplayed()
|
||||
});
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return "scroll RecyclerView to position: " + this.position;
|
||||
}
|
||||
|
||||
public void perform(UiController uiController, View view) {
|
||||
RecyclerView recyclerView = (RecyclerView) view;
|
||||
recyclerView.scrollToPosition(this.position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
|
||||
return new RecyclerViewMatcher(recyclerViewId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package com.anytypeio.anytype.test_utils.utils.espresso
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import androidx.test.espresso.matcher.BoundedMatcher
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
|
||||
|
||||
class WithTextColor(private val expectedColor: Int) : BoundedMatcher<View, TextView>(TextView::class.java) {
|
||||
override fun matchesSafely(item: TextView) = item.currentTextColor == expectedColor
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with text color:")
|
||||
description.appendValue(expectedColor)
|
||||
}
|
||||
}
|
||||
|
||||
class WithBackgroundColor(private val expected: Int) : BoundedMatcher<View, View>(View::class.java) {
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with background color:")
|
||||
description.appendValue(expected)
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View): Boolean {
|
||||
val actual = (item.background as ColorDrawable).color
|
||||
return actual == expected
|
||||
}
|
||||
}
|
||||
|
||||
class WithoutBackgroundColor : BoundedMatcher<View, View>(View::class.java) {
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with background color:")
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View): Boolean {
|
||||
return item.background == null
|
||||
}
|
||||
}
|
||||
|
||||
class WithChildViewCount(private val expectedCount: Int) : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
|
||||
override fun matchesSafely(item: ViewGroup): Boolean = item.childCount == expectedCount
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("ViewGroup with child-count = $expectedCount");
|
||||
}
|
||||
}
|
||||
|
||||
class HasViewGroupChildViewWithText(private val pos: Int, val text: String) : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
|
||||
|
||||
private var actual: String? = null
|
||||
|
||||
override fun matchesSafely(item: ViewGroup): Boolean {
|
||||
val child = item.getChildAt(pos)
|
||||
checkNotNull(child) { throw IllegalStateException("No view child at position: $pos") }
|
||||
check(child is TextView) { throw IllegalStateException("Child view is not text view at position: $pos, but: ${child::class.java.canonicalName}") }
|
||||
actual = child.text.toString()
|
||||
return actual == text
|
||||
}
|
||||
override fun describeTo(description: Description) {
|
||||
if (actual != null) {
|
||||
description.appendText("Should have text [$text] at position: $pos but was: $actual");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HasChildViewWithText(private val pos: Int, val text: String, val target: Int) : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
|
||||
|
||||
private var actual: String? = null
|
||||
|
||||
override fun matchesSafely(item: RecyclerView): Boolean {
|
||||
val holder = item.findViewHolderForLayoutPosition(pos)
|
||||
checkNotNull(holder) { throw IllegalStateException("No holder at position: $pos") }
|
||||
val target = holder.itemView.findViewById<TextView>(target)
|
||||
actual = target.text.toString()
|
||||
return actual == text
|
||||
}
|
||||
override fun describeTo(description: Description) {
|
||||
if (actual != null) {
|
||||
description.appendText("Should have text [$text] at position: $pos but was: $actual");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WithTextColorRes(private val expectedColorRes: Int) : BoundedMatcher<View, TextView>(TextView::class.java) {
|
||||
override fun matchesSafely(item: TextView): Boolean {
|
||||
val color = ContextCompat.getColor(item.context, expectedColorRes)
|
||||
return item.currentTextColor == color
|
||||
}
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with text color:")
|
||||
description.appendValue(expectedColorRes)
|
||||
}
|
||||
}
|
||||
|
||||
class TextLineCountMatcher(private val lines: Int) : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description?) {
|
||||
description?.appendText("isTextInLines")
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View?): Boolean {
|
||||
return (item as TextView).lineCount == lines
|
||||
}
|
||||
}
|
||||
|
||||
class SetEditTextSelectionAction(private val selection: Int) : ViewAction {
|
||||
|
||||
override fun getConstraints(): Matcher<View> {
|
||||
return allOf(isDisplayed(), isAssignableFrom(EditText::class.java))
|
||||
}
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "set selection to $selection"
|
||||
}
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
(view as EditText).setSelection(selection)
|
||||
}
|
||||
}
|
11
test/android-utils/src/main/res/layout/blank.xml
Normal file
11
test/android-utils/src/main/res/layout/blank.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
Loading…
Add table
Add a link
Reference in a new issue