Skip to content

Commit

Permalink
Merge pull request #4439 from vector-im/feature/adm/developer-mode-sa…
Browse files Browse the repository at this point in the history
…nity-check

Developer mode sanity check & failure screenshots
  • Loading branch information
bmarty authored Nov 10, 2021
2 parents 474e2aa + 4264829 commit 8b655ed
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 61 deletions.
21 changes: 19 additions & 2 deletions .github/workflows/sanity_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
api-level: [28]
api-level: [ 29 ]
steps:
- uses: actions/checkout@v2
with:
Expand Down Expand Up @@ -56,7 +56,24 @@ jobs:
java-version: '11'
- name: Run sanity tests on API ${{ matrix.api-level }}
uses: reactivecircus/android-emulator-runner@v2
continue-on-error: true # allow pipeline to upload failure results
with:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
api-level: ${{ matrix.api-level }}
script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest
emulator-build: 7425822 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
script: |
adb root
adb logcat -c
touch emulator.log
chmod 777 emulator.log
adb logcat >> emulator.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
- name: Upload Failing Test Report Log
if: failure()
uses: actions/upload-artifact@v2
with:
name: sanity-error-results
path: |
emulator.log
failure_screenshots/
61 changes: 61 additions & 0 deletions vector/src/androidTest/java/im/vector/app/EspressoExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package im.vector.app
import android.app.Activity
import android.view.View
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
Expand All @@ -35,6 +36,11 @@ import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import com.adevinta.android.barista.interaction.BaristaClickInteractions
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.espresso.tools.waitUntilViewVisible
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.StringDescription
Expand All @@ -52,6 +58,18 @@ object EspressoHelper {
}
return currentActivity
}

inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> getBottomSheetDialog(): BottomSheetDialogFragment? {
return (getCurrentActivity() as? FragmentActivity)
?.supportFragmentManager
?.fragments
?.filterIsInstance<T>()
?.firstOrNull()
}
}

fun getString(@StringRes id: Int): String {
return EspressoHelper.getCurrentActivity()!!.resources.getString(id)
}

fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
Expand Down Expand Up @@ -216,3 +234,46 @@ fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) {
block()
Espresso.pressBack()
}

inline fun <reified T : VectorBaseBottomSheetDialogFragment<*>> interactWithSheet(contentMatcher: Matcher<View>, noinline block: () -> Unit = {}) {
waitUntilViewVisible(contentMatcher)
val behaviour = (EspressoHelper.getBottomSheetDialog<T>()!!.dialog as BottomSheetDialog).behavior
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_EXPANDED), block)
withIdlingResource(BottomSheetResource(behaviour, BottomSheetBehavior.STATE_HIDDEN)) {}
}

class BottomSheetResource(
private val bottomSheetBehavior: BottomSheetBehavior<*>,
@BottomSheetBehavior.State private val wantedState: Int
) : IdlingResource, BottomSheetBehavior.BottomSheetCallback() {

private var isIdle: Boolean = false
private var resourceCallback: IdlingResource.ResourceCallback? = null

override fun onSlide(bottomSheet: View, slideOffset: Float) {}

override fun onStateChanged(bottomSheet: View, newState: Int) {
val wasIdle = isIdle
isIdle = newState == BottomSheetBehavior.STATE_EXPANDED
if (!wasIdle && isIdle) {
bottomSheetBehavior.removeBottomSheetCallback(this)
resourceCallback?.onTransitionToIdle()
}
}

override fun getName() = "BottomSheet awaiting state: $wantedState"

override fun isIdleNow() = isIdle

override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
resourceCallback = callback

val state = bottomSheetBehavior.state
isIdle = state == wantedState
if (isIdle) {
resourceCallback!!.onTransitionToIdle()
} else {
bottomSheetBehavior.addBottomSheetCallback(this)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.app.espresso.tools

import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

private val SCREENSHOT_FOLDER_LOCATION = "${Environment.DIRECTORY_PICTURES}/failure_screenshots"
private val deviceLanguage = Locale.getDefault().language

class ScreenshotFailureRule : TestWatcher() {
override fun failed(e: Throwable?, description: Description) {
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName)
}
}

/**
* Stores screenshots in sdcard/Pictures/failure_screenshots
*/
private fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) {
val contentResolver = getInstrumentation().targetContext.applicationContext.contentResolver

val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
if (android.os.Build.VERSION.SDK_INT >= 29) {
useMediaStoreScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
} else {
usePublicExternalScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
}
}

private fun useMediaStoreScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "$screenshotName.jpeg")
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, screenshotLocation)
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) }
contentResolver.update(uri, contentValues, null, null)
}
}

private fun usePublicExternalScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
val directory = File(Environment.getExternalStoragePublicDirectory(screenshotLocation).toString())
if (!directory.exists()) {
directory.mkdirs()
}
val file = File(directory, "$screenshotName.jpeg")
saveScreenshotToStream(bitmap, FileOutputStream(file))
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
}

private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) {
outputStream.use {
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, it)
} catch (e: IOException) {
Timber.e("Screenshot was not stored at this time")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ package im.vector.app.ui
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import im.vector.app.R
import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
import im.vector.app.getString
import im.vector.app.ui.robot.ElementRobot
import im.vector.app.ui.robot.withDeveloperMode
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import java.util.UUID

Expand All @@ -34,7 +39,9 @@ import java.util.UUID
class UiAllScreensSanityTest {

@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java))
.around(ScreenshotFailureRule())

private val elementRobot = ElementRobot()

Expand Down Expand Up @@ -69,13 +76,30 @@ class UiAllScreensSanityTest {
createNewRoom {
crawl()
createRoom {
postMessage("Hello world!")
val message = "Hello world!"
postMessage(message)
crawl()
crawlMessage(message)
openSettings { crawl() }
}
}
}

elementRobot.withDeveloperMode {
settings {
advancedSettings { crawlDeveloperOptions() }
}
roomList {
openRoom(getString(R.string.room_displayname_empty_room)) {
val message = "Test view source"
postMessage(message)
openMessageMenu(message) {
viewSource()
}
}
}
}

elementRobot.roomList {
verifyCreatedRoom()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,9 @@ class ElementRobot {
}

private fun Boolean.toWarningType() = if (this) "shown" else "skipped"

fun ElementRobot.withDeveloperMode(block: ElementRobot.() -> Unit) {
settings { toggleDeveloperMode() }
block()
settings { toggleDeveloperMode() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.app.ui.robot

import androidx.test.espresso.Espresso.pressBack
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem
import im.vector.app.R
import java.lang.Thread.sleep

class MessageMenuRobot(
var autoClosed: Boolean = false
) {

fun viewSource() {
clickOn(R.string.view_source)
// wait for library
sleep(1000)
pressBack()
autoClosed = true
}

fun editHistory() {
clickOn(R.string.message_view_edit_history)
pressBack()
autoClosed = true
}

fun addQuickReaction(quickReaction: String) {
clickOn(quickReaction)
autoClosed = true
}

fun addReactionFromEmojiPicker() {
clickOn(R.string.message_add_reaction)
// Wait for emoji to load, it's async now
sleep(2000)
clickListItem(R.id.emojiRecyclerView, 4)
autoClosed = true
}

fun edit() {
clickOn(R.string.edit)
autoClosed = true
}
}
Loading

0 comments on commit 8b655ed

Please sign in to comment.