diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 497504130e..53b70276c5 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - api-level: [28] + api-level: [ 29 ] steps: - uses: actions/checkout@v2 with: @@ -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/ diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 1cefa55e23..fbcb9b8cb3 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -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 @@ -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 @@ -52,6 +58,18 @@ object EspressoHelper { } return currentActivity } + + inline fun > getBottomSheetDialog(): BottomSheetDialogFragment? { + return (getCurrentActivity() as? FragmentActivity) + ?.supportFragmentManager + ?.fragments + ?.filterIsInstance() + ?.firstOrNull() + } +} + +fun getString(@StringRes id: Int): String { + return EspressoHelper.getCurrentActivity()!!.resources.getString(id) } fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { @@ -216,3 +234,46 @@ fun clickOnAndGoBack(@StringRes name: Int, block: () -> Unit) { block() Espresso.pressBack() } + +inline fun > interactWithSheet(contentMatcher: Matcher, noinline block: () -> Unit = {}) { + waitUntilViewVisible(contentMatcher) + val behaviour = (EspressoHelper.getBottomSheetDialog()!!.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) + } + } +} diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt new file mode 100644 index 0000000000..bb1cb622c0 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -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") + } + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index d80f11e975..f998a9f23c 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -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 @@ -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() @@ -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() } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index e904ce1c80..a3bc5b26fc 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -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() } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt new file mode 100644 index 0000000000..fd579c0d9f --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt @@ -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 + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index c77fcbfe35..24fe5adf64 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -17,21 +17,24 @@ package im.vector.app.ui.robot import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.interaction.BaristaClickInteractions import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo -import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu import im.vector.app.R import im.vector.app.espresso.tools.waitUntilViewVisible +import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.app.features.reactions.data.EmojiDataSource +import im.vector.app.interactWithSheet import im.vector.app.waitForView import java.lang.Thread.sleep @@ -39,7 +42,9 @@ class RoomDetailRobot { fun postMessage(content: String) { writeTo(R.id.composerEditText, content) + waitUntilViewVisible(withId(R.id.sendButton)) clickOn(R.id.sendButton) + waitUntilViewVisible(withText(content)) } fun crawl() { @@ -55,61 +60,54 @@ class RoomDetailRobot { pressBack() clickMenu(R.id.search) pressBack() - // Long click on the message - longClickOnMessageTest() } - private fun longClickOnMessageTest() { + fun crawlMessage(message: String) { // Test quick reaction - longClickOnMessage() - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - // Add quick reaction - clickOn("\uD83D\uDC4Dī¸") // 👍 - waitUntilViewVisible(withId(R.id.composerEditText)) - + val quickReaction = EmojiDataSource.quickEmojis[0] // 👍 + openMessageMenu(message) { + addQuickReaction(quickReaction) + } // Open reactions - longClickOn("\uD83D\uDC4Dī¸") // 👍 + longClickOn(quickReaction) // wait for bottom sheet pressBack() - // Test add reaction - longClickOnMessage() - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - clickOn(R.string.message_add_reaction) - // Filter - // TODO clickMenu(R.id.search) - // Wait for emoji to load, it's async now - sleep(2000) - clickListItem(R.id.emojiRecyclerView, 4) - waitUntilViewVisible(withId(R.id.composerEditText)) - + openMessageMenu(message) { + addReactionFromEmojiPicker() + } // Test Edit mode - longClickOnMessage() - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - clickOn(R.string.edit) - waitUntilViewVisible(withId(R.id.composerEditText)) + openMessageMenu(message) { + edit() + } // TODO Cancel action writeTo(R.id.composerEditText, "Hello universe!") // Wait a bit for the keyboard layout to update - sleep(30) + waitUntilViewVisible(withId(R.id.sendButton)) clickOn(R.id.sendButton) // Wait for the UI to update - sleep(1000) + waitUntilViewVisible(withText("Hello universe! (edited)")) // Open edit history - longClickOnMessage("Hello universe! (edited)") - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - clickOn(R.string.message_view_edit_history) - pressBack() + openMessageMenu("Hello universe! (edited)") { + editHistory() + } } - private fun longClickOnMessage(text: String = "Hello world!") { - Espresso.onView(withId(R.id.timelineRecyclerView)) + fun openMessageMenu(message: String, block: MessageMenuRobot.() -> Unit) { + onView(withId(R.id.timelineRecyclerView)) .perform( RecyclerViewActions.actionOnItem( - ViewMatchers.hasDescendant(ViewMatchers.withText(text)), + ViewMatchers.hasDescendant(ViewMatchers.withText(message)), ViewActions.longClick() ) ) + interactWithSheet(contentMatcher = withId(R.id.bottomSheetRecyclerView)) { + val messageMenuRobot = MessageMenuRobot() + block(messageMenuRobot) + if (!messageMenuRobot.autoClosed) { + pressBack() + } + } } fun openSettings(block: RoomSettingsRobot.() -> Unit) { diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt index bc7d4ac76b..dc07f06202 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt @@ -17,38 +17,46 @@ package im.vector.app.ui.robot import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions -import com.adevinta.android.barista.interaction.BaristaClickInteractions +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.espresso.tools.waitUntilActivityVisible import im.vector.app.features.roomdirectory.RoomDirectoryActivity class RoomListRobot { + fun openRoom(roomName: String, block: RoomDetailRobot.() -> Unit) { + clickOn(roomName) + block(RoomDetailRobot()) + pressBack() + } + fun verifyCreatedRoom() { - Espresso.onView(ViewMatchers.withId(R.id.roomListView)) + onView(ViewMatchers.withId(R.id.roomListView)) .perform( RecyclerViewActions.actionOnItem( - ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.room_displayname_empty_room)), + ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)), ViewActions.longClick() ) ) - Espresso.pressBack() + pressBack() } fun newRoom(block: NewRoomRobot.() -> Unit) { - BaristaClickInteractions.clickOn(R.id.createGroupRoomButton) + clickOn(R.id.createGroupRoomButton) waitUntilActivityVisible { BaristaVisibilityAssertions.assertDisplayed(R.id.publicRoomsList) } val newRoomRobot = NewRoomRobot() block(newRoomRobot) if (!newRoomRobot.createdRoom) { - Espresso.pressBack() + pressBack() } } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt index ecce51f9bb..4aeb8903dd 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsAdvancedRobot.kt @@ -17,8 +17,11 @@ package im.vector.app.ui.robot.settings import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.espresso.tools.clickOnPreference +import im.vector.app.espresso.tools.waitUntilViewVisible class SettingsAdvancedRobot { @@ -28,20 +31,19 @@ class SettingsAdvancedRobot { clickOnPreference(R.string.settings_push_rules) pressBack() + } - /* TODO P2 test developer screens - // Enable developer mode - clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY") + fun toggleDeveloperMode() { + clickOn(R.string.settings_developer_mode_summary) + } - clickOnPreference(R.string.settings_account_data) - clickOn("m.push_rules") - pressBack() - pressBack() - clickOnPreference(R.string.settings_key_requests) - pressBack() - - // Disable developer mode - clickOnSwitchPreference("SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY") - */ + fun crawlDeveloperOptions() { + clickOnPreference(R.string.settings_account_data) + waitUntilViewVisible(withText("m.push_rules")) + clickOn("m.push_rules") + pressBack() + pressBack() + clickOnPreference(R.string.settings_key_requests) + pressBack() } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt index 3f37d9daf1..a9c053f6c3 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt @@ -21,6 +21,12 @@ import im.vector.app.clickOnAndGoBack class SettingsRobot { + fun toggleDeveloperMode() { + advancedSettings { + toggleDeveloperMode() + } + } + fun general(block: SettingsGeneralRobot.() -> Unit) { clickOnAndGoBack(R.string.settings_general_title) { block(SettingsGeneralRobot()) } } @@ -50,7 +56,9 @@ class SettingsRobot { } fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) { - clickOnAndGoBack(R.string.settings_advanced_settings) { block(SettingsAdvancedRobot()) } + clickOnAndGoBack(R.string.settings_advanced_settings) { + block(SettingsAdvancedRobot()) + } } fun helpAndAbout(block: SettingsHelpRobot.() -> Unit) {