Skip to content

Commit bd66818

Browse files
authored
[WIP] Implemented Espresso tests for upload with multilingual descriptions (commons-app#2830)
* With more upload tests * Fix tests * Fix tests
1 parent 99c6f5f commit bd66818

File tree

5 files changed

+293
-68
lines changed

5 files changed

+293
-68
lines changed

app/proguard-rules.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@
7676
-keepattributes SourceFile,LineNumberTable
7777
-keepattributes *Annotation*
7878

79+
# --- /recycler view ---
80+
-keep class androidx.recyclerview.widget.RecyclerView {
81+
public androidx.recyclerview.widget.RecyclerView$ViewHolder findViewHolderForPosition(int);
82+
}
7983
# --- Parcelable ---
8084
-keepclassmembers class * implements android.os.Parcelable {
8185
static ** CREATOR;
82-
}
86+
}

app/src/androidTest/java/fr/free/nrw/commons/UITestHelper.kt

+22-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import androidx.test.espresso.action.ViewActions
99
import androidx.test.espresso.matcher.ViewMatchers
1010
import androidx.test.rule.ActivityTestRule
1111
import org.apache.commons.lang3.StringUtils
12+
import org.hamcrest.BaseMatcher
13+
import org.hamcrest.Description
14+
import org.hamcrest.Matcher
1215
import timber.log.Timber
1316

17+
1418
class UITestHelper {
1519
companion object {
1620
fun skipWelcome() {
@@ -34,7 +38,7 @@ class UITestHelper {
3438
closeSoftKeyboard()
3539
onView(ViewMatchers.withId(R.id.login_button))
3640
.perform(ViewActions.click())
37-
sleep(5000)
41+
sleep(10000)
3842
} catch (ignored: NoMatchingViewException) {
3943
}
4044

@@ -68,5 +72,22 @@ class UITestHelper {
6872
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
6973
assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
7074
}
75+
76+
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
77+
return object : BaseMatcher<T>() {
78+
var isFirst = true
79+
override fun matches(item: Any): Boolean {
80+
if (isFirst && matcher.matches(item)) {
81+
isFirst = false
82+
return true
83+
}
84+
return false
85+
}
86+
87+
override fun describeTo(description: Description) {
88+
description.appendText("should return first matching item")
89+
}
90+
}
91+
}
7192
}
7293
}

app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt

+202-41
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import android.graphics.Bitmap
88
import android.net.Uri
99
import android.os.Environment
1010
import android.view.View
11+
import androidx.recyclerview.widget.RecyclerView
1112
import androidx.test.espresso.Espresso.onView
1213
import androidx.test.espresso.NoMatchingViewException
1314
import androidx.test.espresso.action.ViewActions.click
1415
import androidx.test.espresso.action.ViewActions.replaceText
1516
import androidx.test.espresso.assertion.ViewAssertions.matches
17+
import androidx.test.espresso.contrib.RecyclerViewActions
1618
import androidx.test.espresso.intent.Intents
1719
import androidx.test.espresso.intent.Intents.intended
1820
import androidx.test.espresso.intent.Intents.intending
@@ -24,6 +26,8 @@ import androidx.test.rule.ActivityTestRule
2426
import androidx.test.rule.GrantPermissionRule
2527
import androidx.test.runner.AndroidJUnit4
2628
import fr.free.nrw.commons.auth.LoginActivity
29+
import fr.free.nrw.commons.upload.DescriptionsAdapter
30+
import fr.free.nrw.commons.util.MyViewAction
2731
import fr.free.nrw.commons.utils.ConfigUtils
2832
import org.hamcrest.core.AllOf.allOf
2933
import org.junit.After
@@ -65,68 +69,154 @@ class UploadTest {
6569
}
6670
UITestHelper.skipWelcome()
6771
UITestHelper.loginUser()
68-
saveToInternalStorage()
6972
}
7073

7174
@After
7275
fun teardown() {
7376
Intents.release()
7477
}
7578

76-
private fun saveToInternalStorage() {
77-
val bitmapImage = randomBitmap
79+
@Test
80+
fun testUploadWithDescription() {
81+
if (!ConfigUtils.isBetaFlavour()) {
82+
throw Error("This test should only be run in Beta!")
83+
}
7884

79-
// path to /data/data/yourapp/app_data/imageDir
80-
val mypath = File(Environment.getExternalStorageDirectory(), "image.jpg")
85+
setupSingleUpload("image.jpg")
8186

82-
Timber.d("Filepath: %s", mypath.path)
87+
openGallery()
8388

84-
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
89+
// Validate that an intent to get an image is sent
90+
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
91+
92+
// Create filename with the current time (to prevent overwrites)
93+
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
94+
val commonsFileName = "MobileTest " + dateFormat.format(Date())
95+
96+
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
97+
dismissWarning("Yes")
98+
99+
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
100+
.perform(replaceText(commonsFileName))
101+
102+
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
103+
.perform(replaceText(commonsFileName))
104+
105+
106+
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
107+
.perform(click())
108+
109+
UITestHelper.sleep(5000)
110+
dismissWarning("Yes")
111+
112+
UITestHelper.sleep(3000)
113+
114+
onView(allOf(isDisplayed(), withId(R.id.et_search)))
115+
.perform(replaceText("Uploaded with Mobile/Android Tests"))
116+
117+
UITestHelper.sleep(3000)
85118

86-
var fos: FileOutputStream? = null
87119
try {
88-
fos = FileOutputStream(mypath)
89-
// Use the compress method on the BitMap object to write image to the OutputStream
90-
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
91-
} catch (e: Exception) {
92-
e.printStackTrace()
93-
} finally {
94-
try {
95-
fos?.close()
96-
} catch (e: IOException) {
97-
e.printStackTrace()
98-
}
120+
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
121+
.perform(click())
122+
} catch (ignored: NoMatchingViewException) {
123+
}
124+
125+
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
126+
.perform(click())
127+
128+
dismissWarning("Yes, Submit")
129+
130+
UITestHelper.sleep(500)
131+
132+
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
133+
.perform(click())
99134

135+
UITestHelper.sleep(10000)
136+
137+
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
138+
commonsFileName.replace(' ', '_') + ".jpg"
139+
Timber.i("File should be uploaded to $fileUrl")
140+
}
141+
142+
private fun dismissWarning(warningText: String) {
143+
try {
144+
onView(withText(warningText))
145+
.check(matches(isDisplayed()))
146+
.perform(click())
147+
} catch (ignored: NoMatchingViewException) {
100148
}
101149
}
102150

103151
@Test
104-
fun uploadTest() {
152+
fun testUploadWithoutDescription() {
105153
if (!ConfigUtils.isBetaFlavour()) {
106154
throw Error("This test should only be run in Beta!")
107155
}
108156

109-
// Uri to return by our mock gallery selector
110-
// Requires file 'image.jpg' to be placed at root of file structure
111-
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
157+
setupSingleUpload("image.jpg")
112158

113-
// Build a result to return from the Camera app
114-
val intent = Intent()
115-
intent.data = imageUri
116-
val result = ActivityResult(Activity.RESULT_OK, intent)
159+
openGallery()
117160

118-
// Stub out the File picker. When an intent is sent to the File picker, this tells
119-
// Espresso to respond with the ActivityResult we just created
120-
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
161+
// Validate that an intent to get an image is sent
162+
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
121163

122-
// Open FAB
123-
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
164+
// Create filename with the current time (to prevent overwrites)
165+
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
166+
val commonsFileName = "MobileTest " + dateFormat.format(Date())
167+
168+
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
169+
dismissWarning("Yes")
170+
171+
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
172+
.perform(replaceText(commonsFileName))
173+
174+
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
124175
.perform(click())
125176

126-
// Click gallery
127-
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
177+
UITestHelper.sleep(10000)
178+
dismissWarning("Yes")
179+
180+
UITestHelper.sleep(3000)
181+
182+
onView(allOf(isDisplayed(), withId(R.id.et_search)))
183+
.perform(replaceText("Test"))
184+
185+
UITestHelper.sleep(3000)
186+
187+
try {
188+
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
189+
.perform(click())
190+
} catch (ignored: NoMatchingViewException) {
191+
}
192+
193+
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
194+
.perform(click())
195+
196+
dismissWarning("Yes, Submit")
197+
198+
UITestHelper.sleep(500)
199+
200+
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
128201
.perform(click())
129202

203+
UITestHelper.sleep(10000)
204+
205+
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
206+
commonsFileName.replace(' ', '_') + ".jpg"
207+
Timber.i("File should be uploaded to $fileUrl")
208+
}
209+
210+
@Test
211+
fun testUploadWithMultilingualDescription() {
212+
if (!ConfigUtils.isBetaFlavour()) {
213+
throw Error("This test should only be run in Beta!")
214+
}
215+
216+
setupSingleUpload("image.jpg")
217+
218+
openGallery()
219+
130220
// Validate that an intent to get an image is sent
131221
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
132222

@@ -135,14 +225,28 @@ class UploadTest {
135225
val commonsFileName = "MobileTest " + dateFormat.format(Date())
136226

137227
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
138-
dismissWarning("Yes")
228+
dismissWarningDialog()
139229

140230
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
141231
.perform(replaceText(commonsFileName))
142232

143-
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
144-
.perform(replaceText(commonsFileName))
233+
onView(withId(R.id.rv_descriptions)).perform(
234+
RecyclerViewActions
235+
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(0,
236+
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
237+
238+
onView(withId(R.id.btn_add_description))
239+
.perform(click())
240+
241+
onView(withId(R.id.rv_descriptions)).perform(
242+
RecyclerViewActions
243+
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
244+
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
145245

246+
onView(withId(R.id.rv_descriptions)).perform(
247+
RecyclerViewActions
248+
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
249+
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
146250

147251
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
148252
.perform(click())
@@ -153,12 +257,12 @@ class UploadTest {
153257
UITestHelper.sleep(3000)
154258

155259
onView(allOf(isDisplayed(), withId(R.id.et_search)))
156-
.perform(replaceText("Uploaded with Mobile/Android Tests"))
260+
.perform(replaceText("Test"))
157261

158262
UITestHelper.sleep(3000)
159263

160264
try {
161-
onView(allOf(isDisplayed(), withParent(withId(R.id.rv_categories))))
265+
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
162266
.perform(click())
163267
} catch (ignored: NoMatchingViewException) {
164268
}
@@ -180,12 +284,69 @@ class UploadTest {
180284
Timber.i("File should be uploaded to $fileUrl")
181285
}
182286

183-
private fun dismissWarning(warningText: String) {
287+
private fun setupSingleUpload(imageName: String) {
288+
saveToInternalStorage(imageName)
289+
singleImageIntent(imageName)
290+
}
291+
292+
private fun saveToInternalStorage(imageName: String) {
293+
val bitmapImage = randomBitmap
294+
295+
// path to /data/data/yourapp/app_data/imageDir
296+
val mypath = File(Environment.getExternalStorageDirectory(), imageName)
297+
298+
Timber.d("Filepath: %s", mypath.path)
299+
300+
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
301+
302+
var fos: FileOutputStream? = null
184303
try {
185-
onView(withText(warningText))
304+
fos = FileOutputStream(mypath)
305+
// Use the compress method on the BitMap object to write image to the OutputStream
306+
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
307+
} catch (e: Exception) {
308+
e.printStackTrace()
309+
} finally {
310+
try {
311+
fos?.close()
312+
} catch (e: IOException) {
313+
e.printStackTrace()
314+
}
315+
316+
}
317+
}
318+
319+
private fun singleImageIntent(imageName: String) {
320+
// Uri to return by our mock gallery selector
321+
// Requires file 'image.jpg' to be placed at root of file structure
322+
val imageUri = Uri.parse("file://mnt/sdcard/$imageName")
323+
324+
// Build a result to return from the Camera app
325+
val intent = Intent()
326+
intent.data = imageUri
327+
val result = ActivityResult(Activity.RESULT_OK, intent)
328+
329+
// Stub out the File picker. When an intent is sent to the File picker, this tells
330+
// Espresso to respond with the ActivityResult we just created
331+
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
332+
}
333+
334+
private fun dismissWarningDialog() {
335+
try {
336+
onView(withText("Yes"))
186337
.check(matches(isDisplayed()))
187338
.perform(click())
188339
} catch (ignored: NoMatchingViewException) {
189340
}
190341
}
342+
343+
private fun openGallery() {
344+
// Open FAB
345+
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
346+
.perform(click())
347+
348+
// Click gallery
349+
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
350+
.perform(click())
351+
}
191352
}

0 commit comments

Comments
 (0)