Create sanity test with all screens path

Introduce `com.schibsted.spain:barista`
This commit is contained in:
Benoit Marty 2020-11-03 23:22:12 +01:00 committed by Benoit Marty
parent ea4e9b8e5e
commit 07c805a019
11 changed files with 462 additions and 120 deletions

View File

@ -461,6 +461,10 @@ dependencies {
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
// Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// "The one who serves a great Espresso"
androidTestImplementation('com.schibsted.spain:barista:3.7.0') {
exclude group: 'org.jetbrains.kotlin'
}
}
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {

View File

@ -67,7 +67,7 @@ class RegistrationTest {
.perform(click())
// Enter local synapse
onView((withId(R.id.loginServerUrlFormHomeServerUrl)))
onView(withId(R.id.loginServerUrlFormHomeServerUrl))
.perform(typeText(homeServerUrl))
// Click on continue
@ -87,7 +87,7 @@ class RegistrationTest {
.check(matches(isDisplayed()))
// Ensure user id
onView((withId(R.id.loginField)))
onView(withId(R.id.loginField))
.perform(typeText(userId))
// Ensure login button not yet enabled
@ -95,7 +95,7 @@ class RegistrationTest {
.check(matches(not(isEnabled())))
// Ensure password
onView((withId(R.id.passwordField)))
onView(withId(R.id.passwordField))
.perform(closeSoftKeyboard(), typeText(password))
// Submit

View File

@ -79,7 +79,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
fun testBasicBootstrap() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {

View File

@ -18,15 +18,11 @@ package im.vector.app
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.ui.UiTestBase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.junit.Assert
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixCallback
@ -43,106 +39,10 @@ abstract class VerificationTestBase {
val password = "password"
val homeServerUrl: String = "http://10.0.2.2:8080"
fun doLogin(homeServerUrl: String, userId: String, password: String) {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
protected val uiTestBase = UiTestBase()
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signin button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSignIn))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
}
private fun createAccount(userId: String = "UiAutoTest", password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_splash_submit)))
Espresso.onView(ViewMatchers.withId(R.id.loginSplashSubmit))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.loginServerTitle))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.login_server_title)))
// Chose custom server
Espresso.onView(ViewMatchers.withId(R.id.loginServerChoiceOther))
.perform(ViewActions.click())
// Enter local synapse
Espresso.onView((ViewMatchers.withId(R.id.loginServerUrlFormHomeServerUrl)))
.perform(ViewActions.typeText(homeServerUrl))
Espresso.onView(ViewMatchers.withId(R.id.loginServerUrlFormSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
// Click on the signup button
Espresso.onView(ViewMatchers.withId(R.id.loginSignupSigninSubmit))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
// Ensure password flow supported
Espresso.onView(ViewMatchers.withId(R.id.loginField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.passwordField))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView((ViewMatchers.withId(R.id.loginField)))
.perform(ViewActions.typeText(userId))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.isEnabled())))
Espresso.onView((ViewMatchers.withId(R.id.passwordField)))
.perform(ViewActions.typeText(password))
Espresso.onView(ViewMatchers.withId(R.id.loginSubmit))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
.perform(ViewActions.closeSoftKeyboard(), ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.homeDrawerFragmentContainer))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
fun createAccountAndSync(matrix: Matrix, userName: String,
fun createAccountAndSync(matrix: Matrix,
userName: String,
password: String,
withInitialSync: Boolean): Session {
val hs = createHomeServerConfig()
@ -174,7 +74,7 @@ abstract class VerificationTestBase {
return session
}
fun createHomeServerConfig(): HomeServerConnectionConfig {
private fun createHomeServerConfig(): HomeServerConnectionConfig {
return HomeServerConnectionConfig.Builder()
.withHomeServerUri(Uri.parse(homeServerUrl))
.build()
@ -200,7 +100,7 @@ abstract class VerificationTestBase {
return result!!
}
fun syncSession(session: Session) {
private fun syncSession(session: Session) {
val lock = CountDownLatch(1)
GlobalScope.launch(Dispatchers.Main) { session.open() }

View File

@ -78,7 +78,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
fun checkVerifyPopup() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
@ -215,10 +215,10 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
}
fun signout() {
onView((withId(R.id.groupToolbarAvatarImageView)))
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
onView((withId(R.id.homeDrawerHeaderSettingsView)))
onView(withId(R.id.homeDrawerHeaderSettingsView))
.perform(click())
onView(withText("General"))

View File

@ -88,7 +88,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
fun checkVerifyWithPassphrase() {
val userId: String = existingSession!!.myUserId
doLogin(homeServerUrl, userId, password)
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
// Thread.sleep(6000)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
@ -137,10 +137,10 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
onView(withId(R.id.ssss__root)).check(matches(isDisplayed()))
}
onView((withId(R.id.ssss_passphrase_enter_edittext)))
onView(withId(R.id.ssss_passphrase_enter_edittext))
.perform(typeText(passphrase))
onView((withId(R.id.ssss_passphrase_submit)))
onView(withId(R.id.ssss_passphrase_submit))
.perform(click())
System.out.println("*** passphrase 1")

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 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.app.Activity
import im.vector.app.activityIdlingResource
import im.vector.app.withIdlingResource
inline fun <reified T : Activity> waitUntilActivityVisible(noinline block: (() -> Unit)) {
withIdlingResource(activityIdlingResource(T::class.java), block)
}

View File

@ -0,0 +1,322 @@
/*
* Copyright (c) 2020 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
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.schibsted.spain.barista.assertion.BaristaListAssertions.assertListItemCount
import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickBack
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn
import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
import com.schibsted.spain.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton
import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo
import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItem
import com.schibsted.spain.barista.interaction.BaristaListInteractions.clickListItemChild
import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.clickMenu
import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions.openMenu
import im.vector.app.EspressoHelper
import im.vector.app.R
import im.vector.app.SleepViewAction
import im.vector.app.activityIdlingResource
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.features.MainActivity
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.initialSyncIdlingResource
import im.vector.app.withIdlingResource
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
/**
* This test aim to open every possible screen of the application
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class UiAllScreensSanityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val uiTestBase = UiTestBase()
@Test
fun allScreensTest() {
// Create an account
val userId = "UiTest_" + UUID.randomUUID().toString()
uiTestBase.createAccount(userId = userId)
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
assertDisplayed(R.id.roomListContainer)
closeSoftKeyboard()
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
assertDisplayed(R.id.roomListContainer)
}
assertDisplayed(R.id.bottomNavigationView)
// Settings
navigateToSettings()
// Create DM
clickOn(R.id.bottom_action_people)
createDm()
// Create Room
// First navigate to the other tab
clickOn(R.id.bottom_action_rooms)
createRoom()
assertDisplayed(R.id.bottomNavigationView)
// Long click on the room
onView(withId(R.id.roomListView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.room_displayname_empty_room)),
longClick()
)
)
pressBack()
uiTestBase.signout()
// We have sent a message in a e2e room, accept to loose it
clickOn(R.id.exitAnywayButton)
// Dark pattern
clickDialogNegativeButton()
// Login again on the same account
waitUntilActivityVisible<LoginActivity> {
assertDisplayed(R.id.loginSplashLogo)
}
uiTestBase.login(userId)
ignoreVerification()
uiTestBase.signout()
clickDialogPositiveButton()
}
private fun ignoreVerification() {
Thread.sleep(6000)
val activity = EspressoHelper.getCurrentActivity()!!
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
assertDisplayed(R.id.bottomSheetFragmentContainer)
onView(ViewMatchers.isRoot()).perform(SleepViewAction.sleep(2000))
clickOn(R.string.skip)
assertDisplayed(R.string.are_you_sure)
clickOn(R.string.skip)
}
private fun createRoom() {
clickOn(R.id.createGroupRoomButton)
waitUntilActivityVisible<RoomDirectoryActivity> {
assertDisplayed(R.id.publicRoomsList)
}
clickOn(R.string.create_new_room)
// Create
assertListItemCount(R.id.createRoomForm, 10)
clickListItemChild(R.id.createRoomForm, 9, R.id.form_submit_button)
waitUntilActivityVisible<RoomDetailActivity> {
assertDisplayed(R.id.roomDetailContainer)
}
clickOn(R.id.attachmentButton)
clickBack()
// Send a message
writeTo(R.id.composerEditText, "Hello world!")
clickOn(R.id.sendButton)
navigateToRoomSettings()
// Long click on the message
onView(withId(R.id.recyclerView))
.perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("Hello world!")),
longClick()
)
)
pressBack()
// Menu
openMenu()
pressBack()
clickMenu(R.id.voice_call)
pressBack()
clickMenu(R.id.video_call)
pressBack()
pressBack()
}
private fun navigateToRoomSettings() {
clickOn(R.id.roomToolbarTitleView)
assertDisplayed(R.id.roomProfileAvatarView)
// Room settings
clickListItem(R.id.matrixProfileRecyclerView, 3)
pressBack()
// Notifications
clickListItem(R.id.matrixProfileRecyclerView, 5)
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// People
clickListItem(R.id.matrixProfileRecyclerView, 7)
assertDisplayed(R.id.inviteUsersButton)
navigateToRoomPeople()
// Fab
navigateToInvite()
pressBack()
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// Uploads
clickListItem(R.id.matrixProfileRecyclerView, 9)
// File tab
clickOn(R.string.uploads_files_title)
pressBack()
assertDisplayed(R.id.roomProfileAvatarView)
// Leave
clickListItem(R.id.matrixProfileRecyclerView, 13)
clickDialogNegativeButton()
// Menu share
// clickMenu(R.id.roomProfileShareAction)
// pressBack()
pressBack()
}
private fun navigateToInvite() {
assertDisplayed(R.id.inviteUsersButton)
clickOn(R.id.inviteUsersButton)
closeSoftKeyboard()
pressBack()
}
private fun navigateToRoomPeople() {
// Open first user
clickListItem(R.id.recyclerView, 1)
assertDisplayed(R.id.memberProfilePowerLevelView)
// Verification
clickListItem(R.id.matrixProfileRecyclerView, 1)
clickBack()
// Role
clickListItem(R.id.matrixProfileRecyclerView, 3)
clickDialogNegativeButton()
clickBack()
}
private fun createDm() {
clickOn(R.id.createChatRoomButton)
withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
assertDisplayed(R.id.addByMatrixId)
}
closeSoftKeyboard()
pressBack()
pressBack()
}
private fun navigateToSettings() {
clickOn(R.id.groupToolbarAvatarImageView)
clickOn(R.id.homeDrawerHeaderSettingsView)
clickOn(R.string.settings_general_title)
// TODO
pressBack()
clickOn(R.string.settings_notifications)
// TODO
pressBack()
clickOn(R.string.settings_preferences)
// TODO
pressBack()
clickOn(R.string.preference_voice_and_video)
// TODO
pressBack()
clickOn(R.string.settings_ignored_users)
// TODO
pressBack()
clickOn(R.string.settings_security_and_privacy)
// TODO
pressBack()
clickOn(R.string.room_settings_labs_pref_title)
// TODO
pressBack()
clickOn(R.string.settings_advanced_settings)
// TODO
pressBack()
clickOn(R.string.preference_root_help_about)
// TODO
pressBack()
pressBack()
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2020 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
import androidx.test.espresso.Espresso.closeSoftKeyboard
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled
import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled
import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn
import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo
import im.vector.app.R
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.features.home.HomeActivity
import im.vector.app.waitForView
class UiTestBase {
fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(true, userId, password, homeServerUrl)
}
fun login(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") {
initSession(false, userId, password, homeServerUrl)
}
private fun initSession(createAccount: Boolean,
userId: String,
password: String,
homeServerUrl: String) {
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit)
clickOn(R.id.loginSplashSubmit)
assertDisplayed(R.id.loginServerTitle, R.string.login_server_title)
// Chose custom server
clickOn(R.id.loginServerChoiceOther)
// Enter local synapse
writeTo(R.id.loginServerUrlFormHomeServerUrl, homeServerUrl)
assertEnabled(R.id.loginServerUrlFormSubmit)
closeSoftKeyboard()
clickOn(R.id.loginServerUrlFormSubmit)
onView(isRoot()).perform(waitForView(withId(R.id.loginSignupSigninSubmit)))
if (createAccount) {
// Click on the signup button
assertDisplayed(R.id.loginSignupSigninSubmit)
clickOn(R.id.loginSignupSigninSubmit)
} else {
// Click on the signin button
assertDisplayed(R.id.loginSignupSigninSignIn)
clickOn(R.id.loginSignupSigninSignIn)
}
// Ensure password flow supported
assertDisplayed(R.id.loginField)
assertDisplayed(R.id.passwordField)
writeTo(R.id.loginField, userId)
assertDisabled(R.id.loginSubmit)
writeTo(R.id.passwordField, password)
assertEnabled(R.id.loginSubmit)
closeSoftKeyboard()
clickOn(R.id.loginSubmit)
// Wait
waitUntilActivityVisible<HomeActivity> {
assertDisplayed(R.id.homeDetailFragmentContainer)
}
}
fun signout() {
clickOn(R.id.groupToolbarAvatarImageView)
clickOn(R.id.homeDrawerHeaderSignoutView)
}
}

View File

@ -105,7 +105,7 @@
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:background="?riotx_background"
app:itemIconSize="20dp"
app:itemIconTint="@color/bottom_navigation_icon_tint_selector"

View File

@ -11,7 +11,8 @@
android:id="@+id/roomListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="always" />
android:overScrollMode="always"
tools:listitem="@layout/item_room" />
<im.vector.app.features.home.room.list.widget.FabMenuView
android:id="@+id/createChatFabMenu"