From 2a66efb7333408f6d2921393e874dda5aa2661ec Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 08:46:53 +0000 Subject: [PATCH 01/16] adding helper to fetch text from resources --- vector/src/androidTest/java/im/vector/app/EspressoExt.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 1cefa55e23..57f4bbcef8 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -54,6 +54,10 @@ object EspressoHelper { } } +fun getString(@StringRes id: Int): String { + return EspressoHelper.getCurrentActivity()!!.resources.getString(id) +} + fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { return object : ViewAction { override fun getConstraints(): Matcher { From e5edfe9c739570de3f7cc838ca8f86368fccc687 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 08:49:09 +0000 Subject: [PATCH 02/16] adding dedicated message menu robot and adding sanity check for developer mode flows --- .../vector/app/ui/UiAllScreensSanityTest.kt | 23 +++++++- .../im/vector/app/ui/robot/ElementRobot.kt | 6 ++ .../vector/app/ui/robot/MessageMenuRobot.kt | 59 +++++++++++++++++++ .../im/vector/app/ui/robot/RoomDetailRobot.kt | 58 +++++++++--------- .../im/vector/app/ui/robot/RoomListRobot.kt | 20 +++++-- .../robot/settings/SettingsAdvancedRobot.kt | 28 +++++---- .../app/ui/robot/settings/SettingsRobot.kt | 10 +++- 7 files changed, 153 insertions(+), 51 deletions(-) create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt 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..e3917919bb 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -19,8 +19,11 @@ 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.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.runner.RunWith @@ -69,13 +72,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() } @@ -89,3 +109,4 @@ class UiAllScreensSanityTest { elementRobot.signout(expectSignOutWarning = false) } } + 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..0948ecb30d --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt @@ -0,0 +1,59 @@ +/* + * 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 +import com.adevinta.android.barista.interaction.BaristaClickInteractions +import com.adevinta.android.barista.interaction.BaristaListInteractions +import im.vector.app.R + +class MessageMenuRobot( + var autoClosed: Boolean = false +) { + + fun viewSource() { + BaristaClickInteractions.clickOn(R.string.view_source) + // wait for library + Thread.sleep(1000) + Espresso.pressBack() + autoClosed = true + } + + fun editHistory() { + BaristaClickInteractions.clickOn(R.string.message_view_edit_history) + Espresso.pressBack() + autoClosed = true + } + + fun addQuickReaction(quickReaction: String) { + BaristaClickInteractions.clickOn(quickReaction) + autoClosed = true + } + + fun addReactionFromEmojiPicker() { + BaristaClickInteractions.clickOn(R.string.message_add_reaction) + // Wait for emoji to load, it's async now + Thread.sleep(2000) + BaristaListInteractions.clickListItem(R.id.emojiRecyclerView, 4) + autoClosed = true + } + + fun edit() { + BaristaClickInteractions.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..f2978aa35f 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,7 +17,7 @@ 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 @@ -27,11 +27,11 @@ 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.reactions.data.EmojiDataSource import im.vector.app.waitForView import java.lang.Thread.sleep @@ -55,61 +55,57 @@ 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ī¸") // 👍 + val quickReaction = EmojiDataSource.quickEmojis[0] // 👍 + openMessageMenu(message) { + addQuickReaction(quickReaction) + } waitUntilViewVisible(withId(R.id.composerEditText)) - // 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) + openMessageMenu(message) { + addReactionFromEmojiPicker() + } waitUntilViewVisible(withId(R.id.composerEditText)) - // Test Edit mode - longClickOnMessage() - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - clickOn(R.string.edit) + openMessageMenu(message) { + edit() + } waitUntilViewVisible(withId(R.id.composerEditText)) // 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) // 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() ) ) + waitUntilViewVisible(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..2c6367736d 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 @@ -18,37 +18,47 @@ 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) { From 39b411669971e04566aea8dab08a99d4ddefab3f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 10:30:29 +0000 Subject: [PATCH 03/16] adding bottomsheet idling resource to wait for expand and hide events --- .../java/im/vector/app/EspressoExt.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 57f4bbcef8..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,14 @@ object EspressoHelper { } return currentActivity } + + inline fun > getBottomSheetDialog(): BottomSheetDialogFragment? { + return (getCurrentActivity() as? FragmentActivity) + ?.supportFragmentManager + ?.fragments + ?.filterIsInstance() + ?.firstOrNull() + } } fun getString(@StringRes id: Int): String { @@ -220,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) + } + } +} From fc865682b1e6a84346a3564bedc90cb5727283b8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 10:32:13 +0000 Subject: [PATCH 04/16] waiting for the message actions bottom sheet when interacting with the message options --- .../vector/app/ui/robot/MessageMenuRobot.kt | 26 ++++++++++--------- .../im/vector/app/ui/robot/RoomDetailRobot.kt | 18 ++++++------- 2 files changed, 23 insertions(+), 21 deletions(-) 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 index 0948ecb30d..8c77cb5757 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt @@ -16,44 +16,46 @@ package im.vector.app.ui.robot -import androidx.test.espresso.Espresso -import com.adevinta.android.barista.interaction.BaristaClickInteractions +import androidx.test.espresso.Espresso.pressBack +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import com.adevinta.android.barista.interaction.BaristaListInteractions +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() { - BaristaClickInteractions.clickOn(R.string.view_source) + clickOn(R.string.view_source) // wait for library - Thread.sleep(1000) - Espresso.pressBack() + sleep(1000) + pressBack() autoClosed = true } fun editHistory() { - BaristaClickInteractions.clickOn(R.string.message_view_edit_history) - Espresso.pressBack() + clickOn(R.string.message_view_edit_history) + pressBack() autoClosed = true } fun addQuickReaction(quickReaction: String) { - BaristaClickInteractions.clickOn(quickReaction) + clickOn(quickReaction) autoClosed = true } fun addReactionFromEmojiPicker() { - BaristaClickInteractions.clickOn(R.string.message_add_reaction) + clickOn(R.string.message_add_reaction) // Wait for emoji to load, it's async now - Thread.sleep(2000) - BaristaListInteractions.clickListItem(R.id.emojiRecyclerView, 4) + sleep(2000) + clickListItem(R.id.emojiRecyclerView, 4) autoClosed = true } fun edit() { - BaristaClickInteractions.clickOn(R.string.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 f2978aa35f..5b3d7f4e77 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 @@ -31,7 +31,9 @@ import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.cli 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,6 +41,7 @@ class RoomDetailRobot { fun postMessage(content: String) { writeTo(R.id.composerEditText, content) + waitUntilViewVisible(withId(R.id.sendButton)) clickOn(R.id.sendButton) } @@ -63,7 +66,6 @@ class RoomDetailRobot { openMessageMenu(message) { addQuickReaction(quickReaction) } - waitUntilViewVisible(withId(R.id.composerEditText)) // Open reactions longClickOn(quickReaction) // wait for bottom sheet @@ -72,16 +74,13 @@ class RoomDetailRobot { openMessageMenu(message) { addReactionFromEmojiPicker() } - waitUntilViewVisible(withId(R.id.composerEditText)) // Test Edit mode openMessageMenu(message) { edit() } - waitUntilViewVisible(withId(R.id.composerEditText)) // 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 @@ -100,11 +99,12 @@ class RoomDetailRobot { ViewActions.longClick() ) ) - waitUntilViewVisible(withId(R.id.bottomSheetRecyclerView)) - val messageMenuRobot = MessageMenuRobot() - block(messageMenuRobot) - if (!messageMenuRobot.autoClosed) { - pressBack() + interactWithSheet(contentMatcher = withId(R.id.bottomSheetRecyclerView)) { + val messageMenuRobot = MessageMenuRobot() + block(messageMenuRobot) + if (!messageMenuRobot.autoClosed) { + pressBack() + } } } From 2583a6d6bfe623fec999d2ddd9bc145c09394d91 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:25:54 +0000 Subject: [PATCH 05/16] taking screenshots on sanity test failure --- .../espresso/tools/ScreenshotFailureRule.kt | 118 ++++++++++++++++++ .../vector/app/ui/UiAllScreensSanityTest.kt | 6 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt 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..7964f9c186 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -0,0 +1,118 @@ +/* + * 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 e3917919bb..07821d9c6a 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -20,12 +20,14 @@ 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 @@ -37,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() From 0a1f4dd69fff5b0a34dd98b50b19212914ddada1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:32:13 +0000 Subject: [PATCH 06/16] adding screenshot pulling and storing as part of the sanity workflow --- .github/workflows/sanity_test.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 497504130e..496037984f 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -58,5 +58,14 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + script: | + ./gradlew $CI_GRADLE_ARG_PROPERTIES \ + -PallWarningsAsErrors=false \ + connectedGplayDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest \ + || adb pull sdcard/Pictures/failure_screenshots + - uses: actions/upload-artifact@v2 + with: + name: failure-screenshots + path: failure_screenshots From 918042c2310e20e029819afe002ff4e06e7b40ad Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:33:48 +0000 Subject: [PATCH 07/16] adding github action emulator optimisations --- .github/workflows/sanity_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 496037984f..91d59b9b1c 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -57,6 +57,7 @@ jobs: - name: Run sanity tests on API ${{ matrix.api-level }} uses: reactivecircus/android-emulator-runner@v2 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 \ From 411be43287725ba59cc173eb354d1a7f9340141b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:41:30 +0000 Subject: [PATCH 08/16] waiting for the edited text to appear instead of sleeping --- .../androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 5b3d7f4e77..3889507383 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 @@ -23,6 +23,7 @@ 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 @@ -84,7 +85,7 @@ class RoomDetailRobot { 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 openMessageMenu("Hello universe! (edited)") { editHistory() From c8ae3f9a83cdcf2765e4785bd792a8f688bf8fba Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:45:05 +0000 Subject: [PATCH 09/16] removing unused imports --- .../androidTest/java/im/vector/app/ui/robot/RoomListRobot.kt | 2 -- 1 file changed, 2 deletions(-) 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 2c6367736d..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,7 +17,6 @@ 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 @@ -25,7 +24,6 @@ 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 From 10a8a45391ad87d6c96a635129be5f26f63c49e6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 11:55:17 +0000 Subject: [PATCH 10/16] waiting for the sent message to be rendered in the timeline --- .../androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt | 1 + 1 file changed, 1 insertion(+) 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 3889507383..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 @@ -44,6 +44,7 @@ class RoomDetailRobot { writeTo(R.id.composerEditText, content) waitUntilViewVisible(withId(R.id.sendButton)) clickOn(R.id.sendButton) + waitUntilViewVisible(withText(content)) } fun crawl() { From 70dd3e5cb9be304f742f670d83d65163f7313aec Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 13:13:04 +0000 Subject: [PATCH 11/16] adding missing indentation --- .github/workflows/sanity_test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 91d59b9b1c..6c06af8766 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -61,10 +61,10 @@ jobs: api-level: ${{ matrix.api-level }} script: | ./gradlew $CI_GRADLE_ARG_PROPERTIES \ - -PallWarningsAsErrors=false \ - connectedGplayDebugAndroidTest \ - -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest \ - || adb pull sdcard/Pictures/failure_screenshots + -PallWarningsAsErrors=false \ + connectedGplayDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest \ + || adb pull sdcard/Pictures/failure_screenshots - uses: actions/upload-artifact@v2 with: name: failure-screenshots From 0ab5734c533e3ba8ae6ff7efab58b98bafb06c21 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 14:20:50 +0000 Subject: [PATCH 12/16] using single line for the emulator script, doesn't seem to support multiline --- .github/workflows/sanity_test.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 6c06af8766..6ad9af6fda 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -59,12 +59,7 @@ jobs: 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 \ - || adb pull sdcard/Pictures/failure_screenshots + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull sdcard/Pictures/failure_screenshots - uses: actions/upload-artifact@v2 with: name: failure-screenshots From 992c3d8a13c10cedef10a8dfe799e9e265923845 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 14:21:19 +0000 Subject: [PATCH 13/16] updating emulator to use api 29 due to storage differences --- .github/workflows/sanity_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 6ad9af6fda..95487dc81f 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: From 968d8e413a8d3ceca9e968349f0232a198b6ebf5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 17:44:49 +0000 Subject: [PATCH 14/16] using adb root for pulling off emulator files as per https://github.com/ReactiveCircus/android-emulator-runner/issues/102#issuecomment-963866536 - also uses emulated directory instead of sdcard --- .github/workflows/sanity_test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index 95487dc81f..aed3e70c72 100644 --- a/.github/workflows/sanity_test.yml +++ b/.github/workflows/sanity_test.yml @@ -59,7 +59,9 @@ jobs: 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 || adb pull sdcard/Pictures/failure_screenshots + script: | + adb root + ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots - uses: actions/upload-artifact@v2 with: name: failure-screenshots From 9152c39c732ff14c3909f74a78e7714fda0aeab2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 9 Nov 2021 18:33:57 +0000 Subject: [PATCH 15/16] adding logcat output to the sanity artifacts --- .github/workflows/sanity_test.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml index aed3e70c72..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: [29] + api-level: [ 29 ] steps: - uses: actions/checkout@v2 with: @@ -56,14 +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 }} + 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 - - uses: actions/upload-artifact@v2 - with: - name: failure-screenshots - path: 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/ From 42648298b60f2b98a607bf86b2d66de4b147e54b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 10 Nov 2021 17:09:08 +0000 Subject: [PATCH 16/16] formatting --- .../java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt | 1 - .../androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt | 1 - .../androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt | 1 - 3 files changed, 3 deletions(-) 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 index 7964f9c186..bb1cb622c0 100644 --- a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt +++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt @@ -115,4 +115,3 @@ private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) { } } } - 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 07821d9c6a..f998a9f23c 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -113,4 +113,3 @@ class UiAllScreensSanityTest { elementRobot.signout(expectSignOutWarning = false) } } - 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 index 8c77cb5757..fd579c0d9f 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/MessageMenuRobot.kt @@ -18,7 +18,6 @@ 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 import com.adevinta.android.barista.interaction.BaristaListInteractions.clickListItem import im.vector.app.R import java.lang.Thread.sleep