[WIP] Implemented Espresso tests for upload with multilingual descriptions (#2830)

* With more upload tests

* Fix tests

* Fix tests
This commit is contained in:
Vivek Maskara 2020-03-10 12:01:22 -07:00 committed by GitHub
parent 99c6f5f105
commit bd668182b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 301 additions and 76 deletions

View file

@ -76,6 +76,10 @@
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-keepattributes *Annotation* -keepattributes *Annotation*
# --- /recycler view ---
-keep class androidx.recyclerview.widget.RecyclerView {
public androidx.recyclerview.widget.RecyclerView$ViewHolder findViewHolderForPosition(int);
}
# --- Parcelable --- # --- Parcelable ---
-keepclassmembers class * implements android.os.Parcelable { -keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR; static ** CREATOR;

View file

@ -9,8 +9,12 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import timber.log.Timber import timber.log.Timber
class UITestHelper { class UITestHelper {
companion object { companion object {
fun skipWelcome() { fun skipWelcome() {
@ -34,7 +38,7 @@ class UITestHelper {
closeSoftKeyboard() closeSoftKeyboard()
onView(ViewMatchers.withId(R.id.login_button)) onView(ViewMatchers.withId(R.id.login_button))
.perform(ViewActions.click()) .perform(ViewActions.click())
sleep(5000) sleep(10000)
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
} }
@ -68,5 +72,22 @@ class UITestHelper {
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
} }
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun matches(item: Any): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
}
return false
}
override fun describeTo(description: Description) {
description.appendText("should return first matching item")
}
}
}
} }
} }

View file

@ -8,11 +8,13 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.Intents.intending
@ -24,6 +26,8 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.auth.LoginActivity import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.upload.DescriptionsAdapter
import fr.free.nrw.commons.util.MyViewAction
import fr.free.nrw.commons.utils.ConfigUtils import fr.free.nrw.commons.utils.ConfigUtils
import org.hamcrest.core.AllOf.allOf import org.hamcrest.core.AllOf.allOf
import org.junit.After import org.junit.After
@ -65,7 +69,6 @@ class UploadTest {
} }
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
UITestHelper.loginUser() UITestHelper.loginUser()
saveToInternalStorage()
} }
@After @After
@ -73,59 +76,15 @@ class UploadTest {
Intents.release() Intents.release()
} }
private fun saveToInternalStorage() {
val bitmapImage = randomBitmap
// path to /data/data/yourapp/app_data/imageDir
val mypath = File(Environment.getExternalStorageDirectory(), "image.jpg")
Timber.d("Filepath: %s", mypath.path)
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(mypath)
// Use the compress method on the BitMap object to write image to the OutputStream
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
fos?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
@Test @Test
fun uploadTest() { fun testUploadWithDescription() {
if (!ConfigUtils.isBetaFlavour()) { if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!") throw Error("This test should only be run in Beta!")
} }
// Uri to return by our mock gallery selector setupSingleUpload("image.jpg")
// Requires file 'image.jpg' to be placed at root of file structure
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
// Build a result to return from the Camera app openGallery()
val intent = Intent()
intent.data = imageUri
val result = ActivityResult(Activity.RESULT_OK, intent)
// Stub out the File picker. When an intent is sent to the File picker, this tells
// Espresso to respond with the ActivityResult we just created
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
// Open FAB
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
.perform(click())
// Click gallery
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
.perform(click())
// Validate that an intent to get an image is sent // Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))) intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
@ -158,7 +117,7 @@ class UploadTest {
UITestHelper.sleep(3000) UITestHelper.sleep(3000)
try { try {
onView(allOf(isDisplayed(), withParent(withId(R.id.rv_categories)))) onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click()) .perform(click())
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
} }
@ -188,4 +147,206 @@ class UploadTest {
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
} }
} }
@Test
fun testUploadWithoutDescription() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}
setupSingleUpload("image.jpg")
openGallery()
// Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
// Create filename with the current time (to prevent overwrites)
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
val commonsFileName = "MobileTest " + dateFormat.format(Date())
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
.perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
UITestHelper.sleep(10000)
dismissWarning("Yes")
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
@Test
fun testUploadWithMultilingualDescription() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}
setupSingleUpload("image.jpg")
openGallery()
// Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
// Create filename with the current time (to prevent overwrites)
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
val commonsFileName = "MobileTest " + dateFormat.format(Date())
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarningDialog()
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
.perform(replaceText(commonsFileName))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
onView(withId(R.id.btn_add_description))
.perform(click())
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
UITestHelper.sleep(5000)
dismissWarning("Yes")
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
private fun setupSingleUpload(imageName: String) {
saveToInternalStorage(imageName)
singleImageIntent(imageName)
}
private fun saveToInternalStorage(imageName: String) {
val bitmapImage = randomBitmap
// path to /data/data/yourapp/app_data/imageDir
val mypath = File(Environment.getExternalStorageDirectory(), imageName)
Timber.d("Filepath: %s", mypath.path)
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(mypath)
// Use the compress method on the BitMap object to write image to the OutputStream
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
fos?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun singleImageIntent(imageName: String) {
// Uri to return by our mock gallery selector
// Requires file 'image.jpg' to be placed at root of file structure
val imageUri = Uri.parse("file://mnt/sdcard/$imageName")
// Build a result to return from the Camera app
val intent = Intent()
intent.data = imageUri
val result = ActivityResult(Activity.RESULT_OK, intent)
// Stub out the File picker. When an intent is sent to the File picker, this tells
// Espresso to respond with the ActivityResult we just created
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
}
private fun dismissWarningDialog() {
try {
onView(withText("Yes"))
.check(matches(isDisplayed()))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
}
private fun openGallery() {
// Open FAB
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
.perform(click())
// Click gallery
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
.perform(click())
}
} }

View file

@ -1,25 +0,0 @@
package fr.free.nrw.commons
import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.hamcrest.Matcher
object ViewActions {
fun clickChildViewWithId(id: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
}
}
}

View file

@ -0,0 +1,64 @@
package fr.free.nrw.commons.util
import android.view.View
import android.widget.EditText
import androidx.appcompat.widget.AppCompatSpinner
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.hamcrest.Matcher
class MyViewAction {
companion object {
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as EditText
v.setText(textToBeTyped)
}
}
}
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as AppCompatSpinner
v.setSelection(position)
}
}
}
fun clickItemWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as View
v.performClick()
}
}
}
}
}