Skip to content

Commit

Permalink
Merge pull request #5166 from wikimedia/espresso-test-articles
Browse files Browse the repository at this point in the history
Espresso test articles
  • Loading branch information
cooltey authored Jan 11, 2025
2 parents ff4beb0 + 32bff8a commit 31a7c0d
Show file tree
Hide file tree
Showing 37 changed files with 1,678 additions and 66 deletions.
8 changes: 8 additions & 0 deletions app/src/androidTest/java/org/wikipedia/TestConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@ object TestConstants {
const val ON_THIS_DAY_CARD = "On this day"
const val RANDOM_CARD = "Random article"
const val SUGGESTED_EDITS = "Suggested edits"
const val SEARCH_TERM = "apple"
const val SEARCH_TERM2 = "orange"
const val SEARCH_HOPF_FIBRATION = "Hopf fibration"
const val SPECIAL_ARTICLE_VORTEX_SHEDDING = "Vortex shedding"
const val SPECIAL_ARTICLE_AVATAR_2009 = "Avatar 2009"
const val SPECIAL_ARTICLE_BILL_CLINTON = "Bill Clinton"
const val SPECIAL_ARTICLE_INDIA = "India"
const val SPECIAL_ARTICLE_USA = "USA"
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class AssertJavascriptAction(val script: String, val expectedResult: String) : V

override fun onReceiveValue(value: String) {
evaluateFinished.set(true)
if (value != expectedResult) {
val cleanValue = value.trim('"')
if (cleanValue != expectedResult) {
throw exception
.withCause(RuntimeException("Expected: $expectedResult, but got: $value"))
.build()
Expand Down
144 changes: 137 additions & 7 deletions app/src/androidTest/java/org/wikipedia/base/BaseRobot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package org.wikipedia.base

import android.app.Activity
import android.graphics.Rect
import android.os.SystemClock
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.ListView
Expand All @@ -12,14 +14,17 @@ import androidx.annotation.ColorRes
import androidx.annotation.IdRes
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.doubleClick
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.scrollTo
Expand Down Expand Up @@ -51,6 +56,8 @@ import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.anything
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.not
import org.hamcrest.TypeSafeMatcher
import org.wikipedia.R
Expand All @@ -59,7 +66,6 @@ import org.wikipedia.TestUtil.waitOnId
import java.util.concurrent.TimeUnit

abstract class BaseRobot {

protected fun clickOnViewWithIdAndContainsString(@IdRes viewId: Int, text: String) {
onView(allOf(
withId(viewId),
Expand Down Expand Up @@ -121,6 +127,15 @@ abstract class BaseRobot {
)
}

protected fun doubleClickOnViewWithId(@IdRes viewId: Int) {
onView(
allOf(
withId(viewId),
isDisplayed()
)
).perform(doubleClick())
}

protected fun scrollToView(@IdRes viewId: Int) {
onView(withId(viewId)).perform(scrollTo())
}
Expand Down Expand Up @@ -153,6 +168,11 @@ abstract class BaseRobot {
onView(withId(viewId)).check(matches(isDisplayed()))
}

protected fun checkPartialString(text: String) {
onView(withText(containsString(text)))
.check(matches(isDisplayed()))
}

protected fun isViewWithTextVisible(text: String): Boolean {
var isDisplayed = false
onView(withText(text)).check { view, noViewFoundException ->
Expand Down Expand Up @@ -221,23 +241,32 @@ abstract class BaseRobot {
.check(matches(matcher))
}

protected fun verifyMessageOfSnackbar(text: String) {
onView(
allOf(
withId(com.google.android.material.R.id.snackbar_text),
withText(text)
)).check(matches(isDisplayed()))
}

protected fun swipeDownOnTheWebView(@IdRes viewId: Int) {
onView(withId(viewId)).perform(TestUtil.swipeDownWebView())
delay(TestConfig.DELAY_LARGE)
}

protected fun performIfDialogShown(
protected fun clickIfDialogShown(
dialogText: String,
action: () -> Unit
errorString: String
) {
try {
onView(withText(dialogText))
.perform(waitForAsyncLoading())
.inRoot(isDialog())
.check(matches(isDisplayed()))
action()
.perform(click())
} catch (e: NoMatchingViewException) {
Log.e("BaseRobot", "$errorString")
} catch (e: Exception) {
// Dialog not shown or text not found
Log.e("error", "")
Log.e("BaseRobot", "Unexpected Error: ${e.message}")
}
}

Expand Down Expand Up @@ -436,6 +465,64 @@ abstract class BaseRobot {
.check(matches(atPosition(0, isLayoutDirectionRTL())))
}

protected fun clickXY(x: Int, y: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isDisplayed()
}

override fun getDescription(): String {
return "Click at coordinates: $x, $y"
}

override fun perform(uiController: UiController, view: View) {
uiController.injectMotionEvent(
MotionEvent.obtain(
SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_DOWN,
x.toFloat(),
y.toFloat(),
0
))

uiController.injectMotionEvent(
MotionEvent.obtain(
SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_UP,
x.toFloat(),
y.toFloat(),
0
))
}
}
}

protected fun clickOnListView(@IdRes viewId: Int, @IdRes childView: Int, position: Int) = apply {
onData(anything())
.inAdapterView(withId(viewId))
.atPosition(position)
.onChildView(withId(childView))
.perform(click())
}

protected fun waitForAsyncLoading(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isDisplayed()
}

override fun getDescription(): String {
return "wait for async loading"
}

override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadForAtLeast(2000)
}
}
}

private fun atPosition(position: Int, matcher: Matcher<View>) = object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position")
Expand Down Expand Up @@ -484,6 +571,49 @@ abstract class BaseRobot {
}))
}

protected fun checkImageIsVisibleInsideARecyclerView(@IdRes listId: Int,
@IdRes childItemId: Int,
position: Int) {
onView(withId(listId))
.check(matchesAtPosition(position, targetViewId = childItemId, assertion = { view ->
matches(isDisplayed())
}))
}

protected fun scrollToImageInWebView(imageIndex: Int): ViewAction {
val scrollScript = """
(function findContentImages() {
const contentImages = Array.from(document.querySelectorAll('img'))
.filter(img => img.complete && img.naturalWidth > 100 && img.naturalHeight > 100)
if (contentImages.length > $imageIndex) {
contentImages[$imageIndex].scrollIntoView({ behavior: 'smooth', block: 'center' })
return 'success'
}
return 'image not found'
})()
""".trimIndent()
return ExecuteJavascriptAction(scrollScript)
}

protected fun performActionIfSnackbarVisible(
text: String,
action: () -> Unit
) = apply {
try {
onView(
allOf(
withId(com.google.android.material.R.id.snackbar_text),
withText(text)
)
).check(matches(isDisplayed()))
action.invoke()
} catch (e: NoMatchingViewException) {
Log.e("BaseRobot", "No snackbar visible, skipping action")
} catch (e: Exception) {
Log.e("BaseRobot", "Unexpected error: ${e.message}")
}
}

private fun clickChildViewWithId(@IdRes id: Int) = object : ViewAction {
override fun getConstraints() = null

Expand Down
2 changes: 2 additions & 0 deletions app/src/androidTest/java/org/wikipedia/base/BaseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ data class DataInjector(
val overrideEditsContribution: Int? = null,
val intentBuilder: (Intent.() -> Unit)? = null,
val showOneTimeCustomizeToolbarTooltip: Boolean = false,
val readingListShareTooltipShown: Boolean = true
)

abstract class BaseTest<T : AppCompatActivity>(
Expand All @@ -54,6 +55,7 @@ abstract class BaseTest<T : AppCompatActivity>(
activityScenarioRule = ActivityScenarioRule(intent)
Prefs.isInitialOnboardingEnabled = dataInjector.isInitialOnboardingEnabled
Prefs.showOneTimeCustomizeToolbarTooltip = dataInjector.showOneTimeCustomizeToolbarTooltip
Prefs.readingListShareTooltipShown = dataInjector.readingListShareTooltipShown
dataInjector.overrideEditsContribution?.let {
Prefs.overrideSuggestedEditContribution = it
}
Expand Down
34 changes: 34 additions & 0 deletions app/src/androidTest/java/org/wikipedia/base/DrawableMatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.wikipedia.base

import android.content.res.Resources
import android.view.View
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description

object DrawableMatcher {
fun withDrawableId(@DrawableRes expectedId: Int) =
object : BoundedMatcher<View, ImageView>(ImageView::class.java) {
override fun describeTo(description: Description) {
description.appendText("with drawable from resource id: $expectedId")
}

override fun matchesSafely(imageView: ImageView): Boolean {
if (expectedId < 0) return false

val context = imageView.context
val resources = imageView.resources

try {
val expectedName = resources.getResourceEntryName(expectedId)
val expectedType = resources.getResourceTypeName(expectedId)

val foundId = resources.getIdentifier(expectedName, expectedType, context.packageName)
return foundId == expectedId
} catch (e: Resources.NotFoundException) {
return false
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.wikipedia.base

import android.view.View
import android.webkit.ValueCallback
import android.webkit.WebView
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.util.HumanReadables
import org.hamcrest.Matcher
import java.util.concurrent.atomic.AtomicBoolean

class ExecuteJavascriptAction(private val script: String) : ViewAction, ValueCallback<String> {
private val evaluateFinished = AtomicBoolean(false)

override fun getConstraints(): Matcher<View> {
return isAssignableFrom(WebView::class.java)
}

override fun getDescription(): String {
return "Execute Javascript Action"
}

override fun perform(uiController: UiController, view: View) {
uiController.loopMainThreadUntilIdle()

val webView = view as WebView
val exception = PerformException.Builder()
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(view))

webView.evaluateJavascript(script, this)

val maxTime = System.currentTimeMillis() + 5000
while (!evaluateFinished.get()) {
if (System.currentTimeMillis() > maxTime) {
throw exception
.withCause(RuntimeException("Evaluating Javascript timed out."))
.build()
}
uiController.loopMainThreadForAtLeast(50)
}

uiController.loopMainThreadForAtLeast(500)
}

override fun onReceiveValue(value: String?) {
evaluateFinished.set(true)
}
}
27 changes: 27 additions & 0 deletions app/src/androidTest/java/org/wikipedia/base/GifMatchers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.wikipedia.base

import android.view.View
import android.widget.ImageView
import androidx.test.espresso.matcher.BoundedMatcher
import com.bumptech.glide.load.resource.gif.GifDrawable
import org.hamcrest.Description
import org.hamcrest.Matcher

object GifMatchers {
fun hasGifDrawable(): Matcher<View> {
return object : BoundedMatcher<View, ImageView>(ImageView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has gif drawable")
}

override fun matchesSafely(imageView: ImageView): Boolean {
val drawable = imageView.drawable

return when (drawable) {
is GifDrawable -> true
else -> false
}
}
}
}
}
Loading

0 comments on commit 31a7c0d

Please sign in to comment.