diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties new file mode 100644 index 000000000..29f2ebac6 --- /dev/null +++ b/.github/ci-gradle.properties @@ -0,0 +1,8 @@ + +org.gradle.daemon=false +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx5120m +org.gradle.workers.max=2 + +kotlin.incremental=false +kotlin.compiler.execution.strategy=in-process diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4c31c379..d954a96b5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,23 +9,47 @@ on: - master jobs: - tests: - runs-on: macOS-latest + build: + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - - uses: actions/checkout@v1 - name: Checkout - - uses: reactivecircus/android-emulator-runner@v2.11.0 - name: Run tests - with: - api-level: 28 - profile: Nexus 6 - headless: true - disable-animations: true - script: ./gradlew connectedCheck - - - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle- + - name: Checkout + uses: actions/checkout@v2 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Build project + run: ./gradlew assembleDebug + + test: + needs: build + runs-on: macOS-latest # enables hardware acceleration in the virtual machine + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Run instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 26 + arch: x86 + profile: pixel_2 + disable-animations: true + script: ./gradlew connectedCheck diff --git a/.travis.yml b/.travis.yml index 7f9cb2ac1..912f5df43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ jdk: oraclejdk8 dist: trusty branches: only: - - master - /^v\d+\.\d+\.\d+$/ env: global: diff --git a/README.md b/README.md index c3973c25f..3fca87081 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **The one who serves a great Espresso** [![Travis](https://img.shields.io/travis/rust-lang/rust.svg?label=Travis+CI)](https://travis-ci.org/github/AdevintaSpain/Barista) -[![Download](https://api.bintray.com/packages/schibstedspain/maven/barista/images/download.svg)](https://bintray.com/schibstedspain/maven/barista/_latestVersion) +[![CI](https://github.com/AdevintaSpain/Barista/actions/workflows/main.yml/badge.svg)](https://github.com/AdevintaSpain/Barista/actions/workflows/main.yml) [![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](LICENSE.md) @@ -27,7 +27,7 @@ Barista makes developing UI test faster, easier and more predictable. Built on t Import Barista as a testing dependency: ```gradle -androidTestImplementation('com.schibsted.spain:barista:3.7.0') { +androidTestImplementation('com.schibsted.spain:barista:3.9.0') { exclude group: 'org.jetbrains.kotlin' // Only if you already use Kotlin in your project } ``` @@ -141,7 +141,7 @@ setProgressToMax(R.id.seek_bar); #### Pull to refresh in SwipeRefreshLayout ```java refresh(R.id.swipe_refresh); -refresh(); // Id is optional! We'll find it for you :D +refresh(); // Id is optional. Barista will find it for you. ``` #### Close or press ime actions on the Keyboard @@ -272,8 +272,11 @@ assertHint(R.id.edittext, "Hint"); #### Check TextInputLayout and EditText's errors ```java -assertError(R.id.edittext, R.string.error); -assertError(R.id.edittext, "Error message"); +assertErrorDisplayed(R.id.edittext, R.string.error); +assertErrorDisplayed(R.id.edittext, "Error message"); + +assertNoErrorDisplayed(R.id.edittext, R.string.error); +assertNoErrorDisplayed(R.id.edittext, "Error message"); ``` #### Check TextInputLayout's assistive helper text diff --git a/build.gradle b/build.gradle index 97dc30a01..4d53c398a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,4 +28,4 @@ ext.compileSdkVersionDeclared = 30 ext.supportLibVersion = '27.1.1' ext.espressoVersion = '3.0.2' ext.uiAutomatorVersion = '2.1.3' -ext.baristaVersion = '3.7.0' +ext.baristaVersion = '3.10.0' diff --git a/library/src/main/java/com/schibsted/spain/barista/assertion/BaristaErrorAssertions.kt b/library/src/main/java/com/schibsted/spain/barista/assertion/BaristaErrorAssertions.kt index 0a18907c3..84d48e1b8 100644 --- a/library/src/main/java/com/schibsted/spain/barista/assertion/BaristaErrorAssertions.kt +++ b/library/src/main/java/com/schibsted/spain/barista/assertion/BaristaErrorAssertions.kt @@ -1,13 +1,13 @@ package com.schibsted.spain.barista.assertion import android.content.Context -import androidx.annotation.IdRes -import androidx.annotation.StringRes -import com.google.android.material.textfield.TextInputLayout -import androidx.test.espresso.matcher.ViewMatchers import android.view.View import android.widget.TextView +import androidx.annotation.IdRes +import androidx.annotation.StringRes import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.material.textfield.TextInputLayout import com.schibsted.spain.barista.internal.assertAny import org.hamcrest.Description import org.hamcrest.Matcher @@ -15,17 +15,46 @@ import org.hamcrest.TypeSafeMatcher object BaristaErrorAssertions { + @Deprecated( + message = "Use assertErrorDisplayed(id, text)", + replaceWith = ReplaceWith( + "assertErrorDisplayed(viewId, text)", + "com.schibsted.spain.barista.assertion.BaristaErrorAssertions.assertErrorDisplayed" + ) + ) @JvmStatic fun assertError(@IdRes viewId: Int, @StringRes text: Int) { - val resourceString = ApplicationProvider.getApplicationContext().resources.getString(text) - assertError(viewId, resourceString) + assertErrorDisplayed(viewId, text) } + @Deprecated( + message = "Use assertErrorDisplayed(id, text)", + replaceWith = ReplaceWith( + "assertErrorDisplayed(viewId, text)", + "com.schibsted.spain.barista.assertion.BaristaErrorAssertions.assertErrorDisplayed" + ) + ) @JvmStatic fun assertError(@IdRes viewId: Int, text: String) { + assertErrorDisplayed(viewId, text) + } + + @JvmStatic + fun assertErrorDisplayed(@IdRes viewId: Int, @StringRes text: Int) { + val resourceString = ApplicationProvider.getApplicationContext().resources.getString(text) + assertErrorDisplayed(viewId, resourceString) + } + + @JvmStatic + fun assertErrorDisplayed(@IdRes viewId: Int, text: String) { ViewMatchers.withId(viewId).assertAny(matchError(text)) } + @JvmStatic + fun assertNoErrorDisplayed(@IdRes viewId: Int) { + ViewMatchers.withId(viewId).assertAny(matchNoError()) + } + private fun matchError(expectedError: String): Matcher { return object : TypeSafeMatcher() { override fun describeTo(description: Description) { @@ -43,4 +72,22 @@ object BaristaErrorAssertions { } } } + + private fun matchNoError(): Matcher { + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("without error") + } + + override fun matchesSafely(item: View): Boolean { + return when (item) { + is TextView -> item.error.isNullOrEmpty() + is TextInputLayout -> item.error.isNullOrEmpty() + else -> { + throw UnsupportedOperationException("View of class ${item.javaClass.simpleName} not supported") + } + } + } + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/schibsted/spain/barista/internal/matcher/DrawableMatcher.kt b/library/src/main/java/com/schibsted/spain/barista/internal/matcher/DrawableMatcher.kt index 91b0d7bfe..043c2dab5 100644 --- a/library/src/main/java/com/schibsted/spain/barista/internal/matcher/DrawableMatcher.kt +++ b/library/src/main/java/com/schibsted/spain/barista/internal/matcher/DrawableMatcher.kt @@ -1,8 +1,13 @@ package com.schibsted.spain.barista.internal.matcher -import androidx.annotation.DrawableRes +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.core.graphics.drawable.DrawableCompat +import com.google.android.material.button.MaterialButton import com.schibsted.spain.barista.internal.util.BitmapComparator import com.schibsted.spain.barista.internal.util.DrawableToBitmapConverter import org.hamcrest.Description @@ -31,30 +36,26 @@ class DrawableMatcher private constructor(@DrawableRes private val expectedDrawa private var resourceName: String? = null override fun matchesSafely(target: View): Boolean { - if (target !is ImageView) { + val context = target.context + if (target !is ImageView && target !is MaterialButton) { return false } - val imageView = target + val drawable = target.getTargetDrawable() if (expectedDrawableRes == EMPTY) { - return imageView.drawable == null + return drawable == null } if (expectedDrawableRes == ANY) { - return imageView.drawable != null + return drawable != null } - if (imageView.drawable == null) { + if (drawable == null) { return false } - val resources = target.context.resources - val expectedDrawable = resources.getDrawable(expectedDrawableRes) - resourceName = resources.getResourceEntryName(expectedDrawableRes) + val resources = context.resources - if (expectedDrawable == null) { - return false - } + resourceName = resources.getResourceEntryName(expectedDrawableRes) - val viewBitmap = DrawableToBitmapConverter.getBitmap(imageView.drawable) - val expectedBitmap = DrawableToBitmapConverter.getBitmap(expectedDrawable) - return BitmapComparator.compare(viewBitmap, expectedBitmap) + val viewBitmap = DrawableToBitmapConverter.getBitmap(drawable) + return target.getExpectedBitmap()?.let { BitmapComparator.compare(viewBitmap, it) } ?: false } override fun describeTo(description: Description) { @@ -66,4 +67,28 @@ class DrawableMatcher private constructor(@DrawableRes private val expectedDrawa description.appendText("]") } } -} \ No newline at end of file + + private fun View.getTargetDrawable(): Drawable? { + return when (this) { + is MaterialButton -> this.icon + is ImageView -> this.drawable + else -> error("View not supported: $this") + } + } + + private fun View.getExpectedBitmap(): Bitmap? { + return resources.getDrawable(expectedDrawableRes)?.let { drawable -> + when (this) { + is MaterialButton -> DrawableToBitmapConverter.getBitmap(setTargetDrawableTint(drawable)) + is ImageView -> DrawableToBitmapConverter.getBitmap(drawable) + else -> error("") + } + } + } + + private fun MaterialButton.setTargetDrawableTint(drawable: Drawable): Drawable { + DrawableCompat.setTint(drawable, this.iconTint.getColorForState(icon.state, Color.BLACK)) + if (iconTintMode != null) DrawableCompat.setTintMode(drawable, iconTintMode) + return drawable + } +} diff --git a/library/src/main/java/com/schibsted/spain/barista/rule/flaky/FlakyTestRule.kt b/library/src/main/java/com/schibsted/spain/barista/rule/flaky/FlakyTestRule.kt index 635382782..335afbaab 100644 --- a/library/src/main/java/com/schibsted/spain/barista/rule/flaky/FlakyTestRule.kt +++ b/library/src/main/java/com/schibsted/spain/barista/rule/flaky/FlakyTestRule.kt @@ -29,10 +29,23 @@ class FlakyTestRule : TestRule { return this } + /** + * Utility method to use @[Repeat] by default in all test methods. + *

+ * Use this method when constructing the Rule to apply a default behavior of @[Repeat] without having to add the annotation to + * each test. This can help you to find flaky tests. + *

+ * The default behavior can be overridden with [Repeat] or [AllowFlaky]. + */ + fun repeatAttemptsByDefault(defaultAttempts: Int): FlakyTestRule { + flakyStatementBuilder.setRepeatAttemptsByDefault(defaultAttempts) + return this + } + override fun apply(base: Statement, description: Description): Statement { return flakyStatementBuilder .setBase(base) .setDescription(description) .build() } -} \ No newline at end of file +} diff --git a/library/src/main/java/com/schibsted/spain/barista/rule/flaky/internal/FlakyStatementBuilder.java b/library/src/main/java/com/schibsted/spain/barista/rule/flaky/internal/FlakyStatementBuilder.java index 51d83759e..1fd6d3fef 100644 --- a/library/src/main/java/com/schibsted/spain/barista/rule/flaky/internal/FlakyStatementBuilder.java +++ b/library/src/main/java/com/schibsted/spain/barista/rule/flaky/internal/FlakyStatementBuilder.java @@ -11,6 +11,8 @@ public class FlakyStatementBuilder { private Description description; private boolean useAllowFlakyByDefault = false; private int defaultAllowFlakyAttempts = 0; + private int defaultRepeatAttempts = 1; + private boolean useRepeatByDefault = false; public FlakyStatementBuilder setBase(Statement base) { this.base = base; @@ -22,8 +24,16 @@ public FlakyStatementBuilder setDescription(Description description) { return this; } + public FlakyStatementBuilder setRepeatAttemptsByDefault(int attempts) { + useAllowFlakyByDefault = false; + useRepeatByDefault = true; + defaultRepeatAttempts = attempts; + return this; + } + public FlakyStatementBuilder allowFlakyAttemptsByDefault(int attempts) { useAllowFlakyByDefault = true; + useRepeatByDefault = false; defaultAllowFlakyAttempts = attempts; return this; } @@ -46,6 +56,8 @@ public Statement build() { return new AllowFlakyStatement(attempts, base); } else if (useAllowFlakyByDefault) { return new AllowFlakyStatement(defaultAllowFlakyAttempts, base); + } else if (useRepeatByDefault) { + return new RepeatStatement(defaultRepeatAttempts, base); } else { return base; } diff --git a/library/src/test/java/com/schibsted/spain/barista/rule/flaky/FlakyStatementBuilderTest.java b/library/src/test/java/com/schibsted/spain/barista/rule/flaky/FlakyStatementBuilderTest.java index ade35e4f5..1e7816bae 100644 --- a/library/src/test/java/com/schibsted/spain/barista/rule/flaky/FlakyStatementBuilderTest.java +++ b/library/src/test/java/com/schibsted/spain/barista/rule/flaky/FlakyStatementBuilderTest.java @@ -44,6 +44,16 @@ public void repeatStatementReturnedWhenRepeatAnnotationFound() throws Exception assertTrue(resultStatement instanceof RepeatStatement); } + @Test + public void repeatStatementReturnedWhenSettingDefaultRepeatAttempts() throws Exception { + Statement baseStatement = new SomeStatement(); + Description description = Description.EMPTY; + + Statement resultStatement = createStatementWithRepeatAttemptsByDefault(baseStatement, description); + + assertTrue(resultStatement instanceof RepeatStatement); + } + @Test public void allowFlakyStatementReturnedWhenAllowFlakyAnnotationFound() throws Exception { Statement baseStatement = new SomeStatement(); @@ -59,13 +69,36 @@ public void allowFlakyStatementReturnedWhenNoAnnotationsFoundButUsesDefault() th Statement baseStatement = new SomeStatement(); Description description = Description.EMPTY; - Statement resultStatement = new FlakyStatementBuilder() + Statement resultStatement = createStatementWithAllowFlakyByDefault(baseStatement, description); + + assertTrue(resultStatement instanceof AllowFlakyStatement); + } + + @Test + public void lastStatementReturnedWhenDefaultRepeatAttemptsAndAllowFlakyStatementUsedAtTheSameTime() throws Exception { + Statement baseStatement = new SomeStatement(); + Description description = Description.EMPTY; + + Statement resultStatement = createStatementWithFlakyAndReturn(baseStatement, description); + + assertTrue(resultStatement instanceof RepeatStatement); + } + + private Statement createStatementWithFlakyAndReturn(Statement baseStatement, Description description) { + return new FlakyStatementBuilder() .setBase(baseStatement) .setDescription(description) .allowFlakyAttemptsByDefault(5) + .setRepeatAttemptsByDefault(3) .build(); + } - assertTrue(resultStatement instanceof AllowFlakyStatement); + private Statement createStatementWithAllowFlakyByDefault(Statement baseStatement, Description description) { + return new FlakyStatementBuilder() + .setBase(baseStatement) + .setDescription(description) + .allowFlakyAttemptsByDefault(5) + .build(); } //region Shortcut methods @@ -76,6 +109,14 @@ private Statement createStatement(Statement baseStatement, Description descripti .build(); } + private Statement createStatementWithRepeatAttemptsByDefault(Statement baseStatement, Description description) { + return new FlakyStatementBuilder() + .setBase(baseStatement) + .setDescription(description) + .setRepeatAttemptsByDefault(3) + .build(); + } + private Description withAnnotations(Annotation... annotations) { return Description.createTestDescription(CLASS, TEST, annotations); } @@ -115,4 +156,4 @@ public void evaluate() throws Throwable { } } //endregion -} \ No newline at end of file +} diff --git a/sample/src/androidTest/java/com/schibsted/spain/barista/sample/AssertionsTest.java b/sample/src/androidTest/java/com/schibsted/spain/barista/sample/AssertionsTest.java index 40ba9db48..5c201cbd7 100644 --- a/sample/src/androidTest/java/com/schibsted/spain/barista/sample/AssertionsTest.java +++ b/sample/src/androidTest/java/com/schibsted/spain/barista/sample/AssertionsTest.java @@ -259,6 +259,11 @@ public void checkDrawable_withId() throws Exception { assertHasDrawable(R.id.image_view, R.drawable.ic_barista); } + @Test + public void checkMaterialButtonDrawable_withId() throws Exception { + assertHasDrawable(R.id.material_button_view, R.drawable.ic_barista); + } + @Test public void checkVectorDrawable_withId() throws Exception { assertHasDrawable(R.id.vector_image_view, R.drawable.barista_logo_vector); diff --git a/sample/src/androidTest/java/com/schibsted/spain/barista/sample/HintAndErrorTest.java b/sample/src/androidTest/java/com/schibsted/spain/barista/sample/HintAndErrorTest.java index fda84732e..0304d2989 100644 --- a/sample/src/androidTest/java/com/schibsted/spain/barista/sample/HintAndErrorTest.java +++ b/sample/src/androidTest/java/com/schibsted/spain/barista/sample/HintAndErrorTest.java @@ -2,12 +2,15 @@ import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; +import com.schibsted.spain.barista.internal.failurehandler.BaristaException; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import static com.schibsted.spain.barista.assertion.BaristaErrorAssertions.assertError; +import static com.schibsted.spain.barista.assertion.BaristaErrorAssertions.assertErrorDisplayed; +import static com.schibsted.spain.barista.assertion.BaristaErrorAssertions.assertNoErrorDisplayed; import static com.schibsted.spain.barista.assertion.BaristaHintAssertions.assertHint; +import static com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn; @RunWith(AndroidJUnit4.class) public class HintAndErrorTest { @@ -31,15 +34,47 @@ public void assertHintByResource() { @Test public void assertErrorByString() { - assertError(R.id.hintanderror_inputlayout, "TextInputLayout error"); - assertError(R.id.hintanderror_inputedittext, "TextInputEditText error"); - assertError(R.id.hintanderror_edittext, "EditText error"); + clickOn(R.id.showErrors); + assertErrorDisplayed(R.id.hintanderror_inputlayout, "TextInputLayout error"); + assertErrorDisplayed(R.id.hintanderror_inputlayout, "TextInputLayout error"); + assertErrorDisplayed(R.id.hintanderror_inputedittext, "TextInputEditText error"); + assertErrorDisplayed(R.id.hintanderror_edittext, "EditText error"); } @Test public void assertErrorByResource() { - assertError(R.id.hintanderror_inputlayout, R.string.hintanderror_inputlayout_error); - assertError(R.id.hintanderror_inputedittext, R.string.hintanderror_inputedittext_error); - assertError(R.id.hintanderror_edittext, R.string.hintanderror_edittext_error); + clickOn(R.id.showErrors); + assertErrorDisplayed(R.id.hintanderror_inputlayout, R.string.hintanderror_inputlayout_error); + assertErrorDisplayed(R.id.hintanderror_inputedittext, R.string.hintanderror_inputedittext_error); + assertErrorDisplayed(R.id.hintanderror_edittext, R.string.hintanderror_edittext_error); + } + + @Test(expected = BaristaException.class) + public void assertErrorByStringFails() { + assertErrorDisplayed(R.id.hintanderror_inputlayout, "TextInputLayout error"); + assertErrorDisplayed(R.id.hintanderror_inputedittext, "TextInputEditText error"); + assertErrorDisplayed(R.id.hintanderror_edittext, "EditText error"); + } + + @Test(expected = BaristaException.class) + public void assertErrorByResourceFails() { + assertErrorDisplayed(R.id.hintanderror_inputlayout, R.string.hintanderror_inputlayout_error); + assertErrorDisplayed(R.id.hintanderror_inputedittext, R.string.hintanderror_inputedittext_error); + assertErrorDisplayed(R.id.hintanderror_edittext, R.string.hintanderror_edittext_error); + } + + @Test + public void assertNoErrorByResource() { + assertNoErrorDisplayed(R.id.hintanderror_inputlayout); + assertNoErrorDisplayed(R.id.hintanderror_inputedittext); + assertNoErrorDisplayed(R.id.hintanderror_edittext); + } + + @Test(expected = BaristaException.class) + public void assertNoErrorByResourceFails() { + clickOn(R.id.showErrors); + assertNoErrorDisplayed(R.id.hintanderror_inputlayout); + assertNoErrorDisplayed(R.id.hintanderror_inputedittext); + assertNoErrorDisplayed(R.id.hintanderror_edittext); } } diff --git a/sample/src/main/java/com/schibsted/spain/barista/sample/HintAndErrorActivity.java b/sample/src/main/java/com/schibsted/spain/barista/sample/HintAndErrorActivity.java index b9c2be06b..f5109b751 100644 --- a/sample/src/main/java/com/schibsted/spain/barista/sample/HintAndErrorActivity.java +++ b/sample/src/main/java/com/schibsted/spain/barista/sample/HintAndErrorActivity.java @@ -1,11 +1,11 @@ package com.schibsted.spain.barista.sample; import android.os.Bundle; +import android.widget.EditText; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; -import androidx.appcompat.app.AppCompatActivity; -import android.widget.EditText; public class HintAndErrorActivity extends AppCompatActivity { @Override @@ -13,7 +13,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_hintanderrortext); - showError(); + findViewById(R.id.showErrors).setOnClickListener(v -> showError()); } private void showError() { diff --git a/sample/src/main/res/layout/activity_hintanderrortext.xml b/sample/src/main/res/layout/activity_hintanderrortext.xml index f3d69934e..77ce0ab19 100644 --- a/sample/src/main/res/layout/activity_hintanderrortext.xml +++ b/sample/src/main/res/layout/activity_hintanderrortext.xml @@ -7,6 +7,13 @@ android:padding="16dp" > + + @@ -155,4 +156,11 @@ android:layout_width="48dp" android:layout_height="48dp" /> + +