Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/proguard-rules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@
-keep class org.acra.** { *; }
-keepattributes SourceFile,LineNumberTable
-keepattributes *Annotation*

# --- /recycler view ---
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be the opening comment, without /, and this one would go at the end of the recycler view - related block

-keep class androidx.recyclerview.widget.RecyclerView {
public androidx.recyclerview.widget.RecyclerView$ViewHolder findViewHolderForPosition(int);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you actually verify this proguard config? I think the inner class you mention in the declaration that it's a class and the method should have a return type.

Examples: https://www.guardsquare.com/en/products/proguard/manual/examples

}
217 changes: 179 additions & 38 deletions app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
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.intended
import androidx.test.espresso.intent.Intents.intending
Expand All @@ -24,6 +25,8 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4
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 org.hamcrest.core.AllOf.allOf
import org.junit.After
Expand Down Expand Up @@ -65,19 +68,175 @@ class UploadTest {
}
UITestHelper.skipWelcome()
UITestHelper.loginUser()
saveToInternalStorage()
}

@After
fun teardown() {
Intents.release()
}

private fun saveToInternalStorage() {
@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)
dismissWarningDialog()

onView(allOf<View>(withId(R.id.description_item_edit_text), withParent(withParent(withId(R.id.image_title_container)))))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For code readability it can be a good idea to split long lines. many code repos have a particular length limit, 80 or 100 characters are common.

.perform(replaceText(commonsFileName))

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

dismissWarningDialog()
dismissWarningDialog()

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

chooseCategoryAndLicense()

val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to verify that the file upload request is triggered with the right arguments? Without making sure that the inputs are properly read, I'm not sure what exactly this is testing, besides that buttons can be clcked without crashing

}

@Test
fun testUploadWithDescription() {
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>(withId(R.id.description_item_edit_text), withParent(withParent(withId(R.id.image_title_container)))))
.perform(replaceText(commonsFileName))

onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

dismissWarningDialog()
dismissWarningDialog()

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

chooseCategoryAndLicense()

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>(withId(R.id.description_item_edit_text), withParent(withParent(withId(R.id.image_title_container)))))
.perform(replaceText(commonsFileName))

onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))

onView(withId(R.id.bottom_card_add_desc))
.perform(click())

onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(2,
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))

onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(2,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

dismissWarningDialog()
dismissWarningDialog()

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)

chooseCategoryAndLicense()

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(), "image.jpg")
val mypath = File(Environment.getExternalStorageDirectory(), imageName)

Timber.d("Filepath: %s", mypath.path)

Expand All @@ -100,15 +259,10 @@ class UploadTest {
}
}

@Test
fun uploadTest() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}

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/image.jpg")
val imageUri = Uri.parse("file://mnt/sdcard/$imageName")

// Build a result to return from the Camera app
val intent = Intent()
Expand All @@ -118,37 +272,18 @@ class UploadTest {
// 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
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)
private fun dismissWarningDialog() {
try {
onView(withText("Yes"))
.check(matches(isDisplayed()))
.perform(click())
} catch (ignored: NoMatchingViewException) {}

onView(allOf<View>(withId(R.id.description_item_edit_text), withParent(withParent(withId(R.id.image_title_container)))))
.perform(replaceText(commonsFileName))

onView(withId(R.id.bottom_card_next))
.perform(click())

UITestHelper.sleep(1000)
} catch (ignored: NoMatchingViewException) {
}
}

private fun chooseCategoryAndLicense() {
onView(withId(R.id.category_search))
.perform(replaceText("Uploaded with Mobile/Android Tests"))

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all these sleep() statements actually needed?

Espresso is supposed to already make the steps synchronous and not require it for successions of UI actions: https://developer.android.com/training/testing/espresso/index.html#sync

Additionally, sleep makes tests last a looooong time, so you don't run them very often, and might cause them to timeout when you do.

There are utilities to wait on non-ui stuff to finish, if necessary maybe refactor to use this?https://developer.android.com/training/testing/espresso/idling-resource

The no-sleep changes might be out of scope of this patch, but in that case please file a new issue to change this, it doesn't look like a good idea to have all these around to me.

Expand All @@ -166,9 +301,15 @@ class UploadTest {
.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 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())
}
}
47 changes: 47 additions & 0 deletions app/src/androidTest/java/fr/free/nrw/commons/util/MyViewAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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."
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be useful for debugging to print the actual id here

}

override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as EditText
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findViewById can take care of casting the returned view to the desired type, so give it EditText as generic type instead of casting after the fact

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)
}
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import io.reactivex.subjects.Subject;
import timber.log.Timber;

class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {

private Title title;
private List<Description> descriptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import android.app.Activity;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;

/**
* Created by Ilgaz Er on 8/7/2018.
*/
Expand Down