mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
[WIP] Implemented Espresso tests for upload with multilingual descriptions (#2830)
* With more upload tests * Fix tests * Fix tests
This commit is contained in:
parent
99c6f5f105
commit
bd668182b5
5 changed files with 301 additions and 76 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue