Merge branch 'develop' into feature/bma/removeExternalCallSupport

This commit is contained in:
Benoit Marty 2026-04-30 16:58:11 +02:00
commit e21276f323
122 changed files with 2266 additions and 2352 deletions

3
.idea/kotlinc.xml generated
View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.3.20" /> <option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.21" />
</component> </component>
</project> </project>

View file

@ -48,6 +48,8 @@ android {
} }
dependencies { dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.annotationjvm) implementation(libs.androidx.annotationjvm)
implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
} }

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiCommon)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.features.login.api) implementation(projects.features.login.api)

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.announcement.impl.fullscreen package io.element.android.features.announcement.impl.fullscreen
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent import io.element.android.features.announcement.impl.AnnouncementEvent
@ -20,43 +23,39 @@ import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class FullscreenAnnouncementViewTest { class FullscreenAnnouncementViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back sends a AnnouncementEvent`() { fun `clicking on back sends a AnnouncementEvent`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AnnouncementEvent>() val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView( setFullscreenAnnouncementView(
anAnnouncementState( anAnnouncementState(
announcement = Announcement.Fullscreen.Space, announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
} }
@Test @Test
fun `clicking on Continue sends a AnnouncementEvent`() { fun `clicking on Continue sends a AnnouncementEvent`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AnnouncementEvent>() val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView( setFullscreenAnnouncementView(
anAnnouncementState( anAnnouncementState(
announcement = Announcement.Fullscreen.Space, announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setFullscreenAnnouncementView( private fun AndroidComposeUiTest<ComponentActivity>.setFullscreenAnnouncementView(
state: AnnouncementState, state: AnnouncementState,
) { ) {
setContent { setContent {

View file

@ -5,6 +5,8 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.call.ui package io.element.android.features.call.ui
import android.view.KeyEvent import android.view.KeyEvent
@ -12,8 +14,9 @@ import android.webkit.WebView
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.call.impl.pip.PictureInPictureEvents import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.pip.aPictureInPictureState
@ -24,9 +27,7 @@ import io.element.android.features.call.impl.ui.aCallScreenState
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implementation
@ -36,32 +37,29 @@ import org.robolectric.shadows.ShadowWebView
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class CallScreenViewTest { class CallScreenViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() { fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest {
val callEvents = EventsRecorder<CallScreenEvents>() val callEvents = EventsRecorder<CallScreenEvents>()
rule.setCallScreenView( setCallScreenView(
state = aCallScreenState(eventSink = callEvents), state = aCallScreenState(eventSink = callEvents),
useInspectionMode = true, useInspectionMode = true,
) )
rule.pressBackKey() pressBackKey()
callEvents.assertEmpty() callEvents.assertEmpty()
} }
@Config(shadows = [RecordingShadowWebView::class]) @Config(shadows = [RecordingShadowWebView::class])
@Test @Test
fun `pressing back key dispatches escape key events to web view when pip is unsupported`() { fun `pressing back key dispatches escape key events to web view when pip is unsupported`() = runAndroidComposeUiTest {
rule.setCallScreenView( setCallScreenView(
state = aCallScreenState(), state = aCallScreenState(),
useInspectionMode = false, useInspectionMode = false,
) )
rule.pressBackKey() pressBackKey()
val dispatchedEvents = RecordingShadowWebView.dispatchedEvents val dispatchedEvents = RecordingShadowWebView.dispatchedEvents
assertEquals(2, dispatchedEvents.size) assertEquals(2, dispatchedEvents.size)
@ -73,10 +71,10 @@ class CallScreenViewTest {
@Config(shadows = [RecordingShadowWebView::class]) @Config(shadows = [RecordingShadowWebView::class])
@Test @Test
fun `web view javascript back handler emits pip event when pip is supported`() { fun `web view javascript back handler emits pip event when pip is supported`() = runAndroidComposeUiTest {
val pipEvents = EventsRecorder<PictureInPictureEvents>() val pipEvents = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView( setCallScreenView(
state = aCallScreenState(), state = aCallScreenState(),
useInspectionMode = false, useInspectionMode = false,
pipState = aPictureInPictureState( pipState = aPictureInPictureState(
@ -85,7 +83,7 @@ class CallScreenViewTest {
), ),
) )
rule.runOnIdle { runOnIdle {
RecordingShadowWebView.invokeJavascriptBackHandler() RecordingShadowWebView.invokeJavascriptBackHandler()
} }
@ -95,7 +93,7 @@ class CallScreenViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView( private fun AndroidComposeUiTest<ComponentActivity>.setCallScreenView(
state: io.element.android.features.call.impl.ui.CallScreenState, state: io.element.android.features.call.impl.ui.CallScreenState,
useInspectionMode: Boolean, useInspectionMode: Boolean,
pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false), pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.logout.impl package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.deactivation.impl.R import io.element.android.features.deactivation.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -26,33 +29,29 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AccountDeactivationViewTest { class AccountDeactivationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState(eventSink = eventsRecorder), state = anAccountDeactivationState(eventSink = eventsRecorder),
onBackClick = it, onBackClick = it,
) )
rule.pressBack() pressBack()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on Deactivate emits the expected Event`() { fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
password = A_PASSWORD, password = A_PASSWORD,
@ -60,14 +59,14 @@ class AccountDeactivationViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_delete) clickOn(CommonStrings.action_delete)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
} }
@Test @Test
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() { fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
password = A_PASSWORD, password = A_PASSWORD,
@ -76,14 +75,14 @@ class AccountDeactivationViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
} }
@Test @Test
fun `clicking on retry on the confirmation dialog emits the expected Event`() { fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
password = A_PASSWORD, password = A_PASSWORD,
@ -92,26 +91,26 @@ class AccountDeactivationViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true)) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
} }
@Test @Test
fun `switching on the erase all switch emits the expected Event`() { fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true)) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
} }
@Test @Test
fun `switching off the erase all switch emits the expected Event`() { fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
eraseData = true, eraseData = true,
@ -119,15 +118,15 @@ class AccountDeactivationViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false)) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `typing text in the password field emits the expected Event`() { fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>() val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView( setAccountDeactivationView(
state = anAccountDeactivationState( state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState( deactivateFormState = aDeactivateFormState(
password = A_PASSWORD, password = A_PASSWORD,
@ -135,12 +134,12 @@ class AccountDeactivationViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD")) eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView( private fun AndroidComposeUiTest<ComponentActivity>.setAccountDeactivationView(
state: AccountDeactivationState, state: AccountDeactivationState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -15,6 +15,7 @@ android {
dependencies { dependencies {
api(projects.features.enterprise.api) api(projects.features.enterprise.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.compound) implementation(projects.libraries.compound)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.forward.impl package io.element.android.features.forward.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -21,34 +24,30 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ForwardMessagesViewTest { class ForwardMessagesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `cancel error emits the expected event`() { fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>() val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
rule.setForwardMessagesView( setForwardMessagesView(
aForwardMessagesState( aForwardMessagesState(
forwardAction = AsyncAction.Failure(AN_EXCEPTION), forwardAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError) eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
} }
@Test @Test
fun `success invokes onForwardSuccess`() { fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
val data = listOf(A_ROOM_ID) val data = listOf(A_ROOM_ID)
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false)
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback -> ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
rule.setForwardMessagesView( setForwardMessagesView(
aForwardMessagesState( aForwardMessagesState(
forwardAction = AsyncAction.Success(data), forwardAction = AsyncAction.Success(data),
eventSink = eventsRecorder eventSink = eventsRecorder
@ -59,7 +58,7 @@ class ForwardMessagesViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setForwardMessagesView( private fun AndroidComposeUiTest<ComponentActivity>.setForwardMessagesView(
state: ForwardMessagesState, state: ForwardMessagesState,
onForwardSuccess: (List<RoomId>) -> Unit = EnsureNeverCalledWithParam(), onForwardSuccess: (List<RoomId>) -> Unit = EnsureNeverCalledWithParam(),
) { ) {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.ftue.impl.sessionverification.choosemode package io.element.android.features.ftue.impl.sessionverification.choosemode
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.ftue.impl.R import io.element.android.features.ftue.impl.R
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -18,65 +21,61 @@ import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ChooseSessionVerificationModeViewTest { class ChooseSessionVerificationModeViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on learn more invokes the expected callback`() { fun `clicking on learn more invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView( setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(), aChooseSelfVerificationModeState(),
onLearnMoreClick = callback, onLearnMoreClick = callback,
) )
rule.clickOn(CommonStrings.action_learn_more) clickOn(CommonStrings.action_learn_more)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on use another device calls the callback`() { fun `clicking on use another device calls the callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView( setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))), aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))),
onUseAnotherDevice = callback, onUseAnotherDevice = callback,
) )
rule.clickOn(R.string.screen_identity_use_another_device) clickOn(R.string.screen_identity_use_another_device)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on enter recovery key calls the callback`() { fun `clicking on enter recovery key calls the callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView( setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseRecoveryKey = true))), aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseRecoveryKey = true))),
onEnterRecoveryKey = callback, onEnterRecoveryKey = callback,
) )
rule.clickOn(R.string.screen_identity_confirmation_use_recovery_key) clickOn(R.string.screen_identity_confirmation_use_recovery_key)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on cannot confirm calls the reset keys callback`() { fun `clicking on cannot confirm calls the reset keys callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setChooseSelfVerificationModeView( setChooseSelfVerificationModeView(
aChooseSelfVerificationModeState(), aChooseSelfVerificationModeState(),
onResetKey = callback, onResetKey = callback,
) )
rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) clickOn(R.string.screen_identity_confirmation_cannot_confirm)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseSelfVerificationModeView( private fun AndroidComposeUiTest<ComponentActivity>.setChooseSelfVerificationModeView(
state: ChooseSelfVerificationModeState, state: ChooseSelfVerificationModeState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onUseAnotherDevice: () -> Unit = EnsureNeverCalled(), onUseAnotherDevice: () -> Unit = EnsureNeverCalled(),

View file

@ -46,6 +46,7 @@ dependencies {
implementation(projects.libraries.permissions.noop) implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.features.announcement.api) implementation(projects.features.announcement.api)
implementation(projects.features.invite.api) implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api) implementation(projects.features.networkmonitor.api)

View file

@ -6,10 +6,13 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.home.impl.filters package io.element.android.features.home.impl.filters
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.home.impl.R import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.filters.selection.FilterSelectionState import io.element.android.features.home.impl.filters.selection.FilterSelectionState
@ -17,23 +20,20 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomListFiltersViewTest { class RoomListFiltersViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on filters generates expected Event`() { fun `clicking on filters generates expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>() val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
rule.setContent { setContent {
RoomListFiltersView( RoomListFiltersView(
state = aRoomListFiltersState(eventSink = eventsRecorder), state = aRoomListFiltersState(eventSink = eventsRecorder),
) )
} }
rule.clickOn(R.string.screen_roomlist_filter_rooms) clickOn(R.string.screen_roomlist_filter_rooms)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms), RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms),
@ -42,9 +42,9 @@ class RoomListFiltersViewTest {
} }
@Test @Test
fun `clicking on clear filters generates expected Event`() { fun `clicking on clear filters generates expected Event`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<RoomListFiltersEvent>() val eventsRecorder = EventsRecorder<RoomListFiltersEvent>()
rule.setContent { setContent {
RoomListFiltersView( RoomListFiltersView(
state = aRoomListFiltersState( state = aRoomListFiltersState(
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }, filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) },
@ -52,7 +52,7 @@ class RoomListFiltersViewTest {
), ),
) )
} }
rule.pressTag(TestTags.homeScreenClearFilters.value) pressTag(TestTags.homeScreenClearFilters.value)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListFiltersEvent.ClearSelectedFilters, RoomListFiltersEvent.ClearSelectedFilters,

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.home.impl.roomlist package io.element.android.features.home.impl.roomlist
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.home.impl.R import io.element.android.features.home.impl.R
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -20,23 +23,20 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomListContextMenuTest { class RoomListContextMenuTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on Mark as read generates expected Events`() { fun `clicking on Mark as read generates expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown(hasNewContent = true) val contextMenu = aContextMenuShown(hasNewContent = true)
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.clickOn(R.string.screen_roomlist_mark_as_read) clickOn(R.string.screen_roomlist_mark_as_read)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.HideContextMenu, RoomListEvent.HideContextMenu,
@ -46,14 +46,14 @@ class RoomListContextMenuTest {
} }
@Test @Test
fun `clicking on Mark as unread generates expected Events`() { fun `clicking on Mark as unread generates expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown(hasNewContent = false) val contextMenu = aContextMenuShown(hasNewContent = false)
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.clickOn(R.string.screen_roomlist_mark_as_unread) clickOn(R.string.screen_roomlist_mark_as_unread)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.HideContextMenu, RoomListEvent.HideContextMenu,
@ -63,14 +63,14 @@ class RoomListContextMenuTest {
} }
@Test @Test
fun `clicking on Leave room generates expected Events`() { fun `clicking on Leave room generates expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown(isDm = false) val contextMenu = aContextMenuShown(isDm = false)
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.clickOn(CommonStrings.action_leave_room) clickOn(CommonStrings.action_leave_room)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.HideContextMenu, RoomListEvent.HideContextMenu,
@ -80,48 +80,48 @@ class RoomListContextMenuTest {
} }
@Test @Test
fun `clicking on Report room invokes the expected callback and generates expected Event`() { fun `clicking on Report room invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown() val contextMenu = aContextMenuShown()
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
canReportRoom = true, canReportRoom = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
onRoomSettingsClick = EnsureNeverCalledWithParam(), onRoomSettingsClick = EnsureNeverCalledWithParam(),
onReportRoomClick = callback, onReportRoomClick = callback,
) )
rule.clickOn(CommonStrings.action_report_room) clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
callback.assertSuccess() callback.assertSuccess()
} }
@Test @Test
fun `clicking on Settings invokes the expected callback and generates expected Event`() { fun `clicking on Settings invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown() val contextMenu = aContextMenuShown()
val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit)
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
eventSink = eventsRecorder, eventSink = eventsRecorder,
onRoomSettingsClick = callback, onRoomSettingsClick = callback,
) )
rule.clickOn(CommonStrings.common_settings) clickOn(CommonStrings.common_settings)
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
callback.assertSuccess() callback.assertSuccess()
} }
@Test @Test
fun `clicking on Favourites generates expected Event`() { fun `clicking on Favourites generates expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val contextMenu = aContextMenuShown(isDm = false, isFavorite = false)
val callback = EnsureNeverCalledWithParam<RoomId>() val callback = EnsureNeverCalledWithParam<RoomId>()
rule.setRoomListContextMenu( setRoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
eventSink = eventsRecorder, eventSink = eventsRecorder,
onRoomSettingsClick = callback, onRoomSettingsClick = callback,
) )
rule.clickOn(CommonStrings.common_favourite) clickOn(CommonStrings.common_favourite)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true), RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true),
@ -129,7 +129,7 @@ class RoomListContextMenuTest {
) )
} }
private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu( private fun AndroidComposeUiTest<ComponentActivity>.setRoomListContextMenu(
contextMenu: RoomListState.ContextMenu.Shown, contextMenu: RoomListState.ContextMenu.Shown,
canReportRoom: Boolean = false, canReportRoom: Boolean = false,
eventSink: (RoomListEvent) -> Unit, eventSink: (RoomListEvent) -> Unit,

View file

@ -6,10 +6,12 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.home.impl.roomlist package io.element.android.features.home.impl.roomlist
import androidx.activity.ComponentActivity import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.features.home.impl.model.aRoomListRoomSummary
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -18,19 +20,16 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomListDeclineInviteMenuTest { class RoomListDeclineInviteMenuTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on decline emits the expected Events`() { fun `clicking on decline emits the expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
rule.setSafeContent { setSafeContent {
RoomListDeclineInviteMenu( RoomListDeclineInviteMenu(
menu = menu, menu = menu,
canReportRoom = false, canReportRoom = false,
@ -38,7 +37,7 @@ class RoomListDeclineInviteMenuTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
} }
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.HideDeclineInviteMenu, RoomListEvent.HideDeclineInviteMenu,
@ -48,10 +47,10 @@ class RoomListDeclineInviteMenuTest {
} }
@Test @Test
fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
rule.setSafeContent { setSafeContent {
RoomListDeclineInviteMenu( RoomListDeclineInviteMenu(
menu = menu, menu = menu,
canReportRoom = true, canReportRoom = true,
@ -59,16 +58,16 @@ class RoomListDeclineInviteMenuTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
} }
rule.clickOn(CommonStrings.action_decline_and_block) clickOn(CommonStrings.action_decline_and_block)
val expectedEvents = listOf(RoomListEvent.HideDeclineInviteMenu) val expectedEvents = listOf(RoomListEvent.HideDeclineInviteMenu)
eventsRecorder.assertList(expectedEvents) eventsRecorder.assertList(expectedEvents)
} }
@Test @Test
fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
rule.setSafeContent { setSafeContent {
RoomListDeclineInviteMenu( RoomListDeclineInviteMenu(
menu = menu, menu = menu,
canReportRoom = false, canReportRoom = false,
@ -76,7 +75,7 @@ class RoomListDeclineInviteMenuTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
} }
rule.clickOn(CommonStrings.action_decline_and_block) clickOn(CommonStrings.action_decline_and_block)
val expectedEvents = listOf( val expectedEvents = listOf(
RoomListEvent.HideDeclineInviteMenu, RoomListEvent.HideDeclineInviteMenu,
RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true), RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true),
@ -85,10 +84,10 @@ class RoomListDeclineInviteMenuTest {
} }
@Test @Test
fun `clicking on cancel emits the expected Event`() { fun `clicking on cancel emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary())
rule.setSafeContent { setSafeContent {
RoomListDeclineInviteMenu( RoomListDeclineInviteMenu(
menu = menu, menu = menu,
canReportRoom = false, canReportRoom = false,
@ -96,7 +95,7 @@ class RoomListDeclineInviteMenuTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
} }
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertList(listOf(RoomListEvent.HideDeclineInviteMenu)) eventsRecorder.assertList(listOf(RoomListEvent.HideDeclineInviteMenu))
} }
} }

View file

@ -6,16 +6,19 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.home.impl.roomlist package io.element.android.features.home.impl.roomlist
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.home.impl.HomeView import io.element.android.features.home.impl.HomeView
import io.element.android.features.home.impl.R import io.element.android.features.home.impl.R
@ -32,22 +35,17 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomListViewTest { class RoomListViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() { fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -62,9 +60,9 @@ class RoomListViewTest {
} }
@Test @Test
fun `clicking on close recovery key banner emits the expected Event`() { fun `clicking on close recovery key banner emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -74,15 +72,15 @@ class RoomListViewTest {
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
val close = rule.activity.getString(CommonStrings.action_close) val close = activity!!.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick() onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvent.DismissBanner) eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
} }
@Test @Test
fun `clicking on close setup key banner emits the expected Event`() { fun `clicking on close setup key banner emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -92,16 +90,16 @@ class RoomListViewTest {
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
val close = rule.activity.getString(CommonStrings.action_close) val close = activity!!.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick() onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvent.DismissBanner) eventsRecorder.assertSingle(RoomListEvent.DismissBanner)
} }
@Test @Test
fun `clicking on continue recovery key banner invokes the expected callback`() { fun `clicking on continue recovery key banner invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -112,17 +110,17 @@ class RoomListViewTest {
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
} }
@Test @Test
fun `clicking on continue setup key banner invokes the expected callback`() { fun `clicking on continue setup key banner invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -131,28 +129,28 @@ class RoomListViewTest {
) )
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.clickOn(R.string.banner_set_up_recovery_submit) clickOn(R.string.banner_set_up_recovery_submit)
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
} }
@Test @Test
fun `clicking on start chat when the session has no room invokes the expected callback`() { fun `clicking on start chat when the session has no room invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomListEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomListView( setRoomListView(
state = aRoomListState( state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
contentState = anEmptyContentState(), contentState = anEmptyContentState(),
), ),
onCreateRoomClick = callback, onCreateRoomClick = callback,
) )
rule.clickOn(CommonStrings.action_start_chat) clickOn(CommonStrings.action_start_chat)
} }
} }
@Test @Test
fun `clicking on a room invokes the expected callback`() { fun `clicking on a room invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -161,7 +159,7 @@ class RoomListViewTest {
it.displayType == RoomSummaryDisplayType.ROOM it.displayType == RoomSummaryDisplayType.ROOM
} }
ensureCalledOnceWithParam(room0.roomId) { callback -> ensureCalledOnceWithParam(room0.roomId) { callback ->
rule.setRoomListView( setRoomListView(
state = state, state = state,
onRoomClick = callback, onRoomClick = callback,
) )
@ -169,14 +167,14 @@ class RoomListViewTest {
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.onNodeWithText(room0.latestEvent.content().toString()).performClick() onNodeWithText(room0.latestEvent.content().toString()).performClick()
} }
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
@Test @Test
fun `clicking on a room twice invokes the expected callback only once`() { fun `clicking on a room twice invokes the expected callback only once`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -185,13 +183,13 @@ class RoomListViewTest {
it.displayType == RoomSummaryDisplayType.ROOM it.displayType == RoomSummaryDisplayType.ROOM
} }
ensureCalledOnceWithParam(room0.roomId) { callback -> ensureCalledOnceWithParam(room0.roomId) { callback ->
rule.setRoomListView( setRoomListView(
state = state, state = state,
onRoomClick = callback, onRoomClick = callback,
) )
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.onNodeWithText(room0.latestEvent.content().toString()) onNodeWithText(room0.latestEvent.content().toString())
.performClick() .performClick()
.performClick() .performClick()
} }
@ -199,7 +197,7 @@ class RoomListViewTest {
} }
@Test @Test
fun `long clicking on a room emits the expected Event`() { fun `long clicking on a room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -207,18 +205,18 @@ class RoomListViewTest {
val room0 = state.contentAsRooms().summaries.first { val room0 = state.contentAsRooms().summaries.first {
it.displayType == RoomSummaryDisplayType.ROOM it.displayType == RoomSummaryDisplayType.ROOM
} }
rule.setRoomListView( setRoomListView(
state = state, state = state,
) )
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() }
eventsRecorder.assertSingle(RoomListEvent.ShowContextMenu(room0)) eventsRecorder.assertSingle(RoomListEvent.ShowContextMenu(room0))
} }
@Test @Test
fun `clicking on a room setting invokes the expected callback and emits expected Event`() { fun `clicking on a room setting invokes the expected callback and emits expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val state = aRoomListState( val state = aRoomListState(
contextMenu = aContextMenuShown(), contextMenu = aContextMenuShown(),
@ -226,7 +224,7 @@ class RoomListViewTest {
) )
val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId
ensureCalledOnceWithParam(room0) { callback -> ensureCalledOnceWithParam(room0) { callback ->
rule.setRoomListView( setRoomListView(
state = state, state = state,
onRoomSettingsClick = callback, onRoomSettingsClick = callback,
) )
@ -234,14 +232,14 @@ class RoomListViewTest {
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.clickOn(CommonStrings.common_settings) clickOn(CommonStrings.common_settings)
} }
eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu)
} }
@Test @Test
fun `clicking on accept and decline invite emits the expected Events`() { fun `clicking on accept and decline invite emits the expected Events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomListEvent>() val eventsRecorder = EventsRecorder<RoomListEvent>()
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -249,13 +247,13 @@ class RoomListViewTest {
val invitedRoom = state.contentAsRooms().summaries.first { val invitedRoom = state.contentAsRooms().summaries.first {
it.displayType == RoomSummaryDisplayType.INVITE it.displayType == RoomSummaryDisplayType.INVITE
} }
rule.setRoomListView(state = state) setRoomListView(state = state)
// Remove automatic initial events // Remove automatic initial events
eventsRecorder.clear() eventsRecorder.clear()
rule.clickOn(CommonStrings.action_accept) clickOn(CommonStrings.action_accept)
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
RoomListEvent.AcceptInvite(invitedRoom), RoomListEvent.AcceptInvite(invitedRoom),
@ -265,7 +263,7 @@ class RoomListViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomListView(
state: RoomListState, state: RoomListState,
onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onSettingsClick: () -> Unit = EnsureNeverCalled(), onSettingsClick: () -> Unit = EnsureNeverCalled(),

View file

@ -5,34 +5,32 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.home.impl.spacefilters package io.element.android.features.home.impl.spacefilters
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SpaceFiltersViewTest { class SpaceFiltersViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on a filter with alias shows display name and alias`() { fun `clicking on a filter with alias shows display name and alias`() = runAndroidComposeUiTest {
val filter = aSpaceServiceFilter( val filter = aSpaceServiceFilter(
displayName = "Test Space", displayName = "Test Space",
canonicalAlias = A_ROOM_ALIAS, canonicalAlias = A_ROOM_ALIAS,
) )
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>() val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
rule.setSpaceFiltersView( setSpaceFiltersView(
state = aSelectingSpaceFiltersState( state = aSelectingSpaceFiltersState(
availableFilters = listOf(filter), availableFilters = listOf(filter),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -40,20 +38,20 @@ class SpaceFiltersViewTest {
) )
// Both display name and alias should be visible // Both display name and alias should be visible
rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() onNodeWithText(filter.spaceRoom.displayName).assertExists()
rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() onNodeWithText(A_ROOM_ALIAS.value).assertExists()
rule.onNodeWithText(filter.spaceRoom.displayName).performClick() onNodeWithText(filter.spaceRoom.displayName).performClick()
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter))
} }
@Test @Test
fun `multiple filters are displayed and clickable`() { fun `multiple filters are displayed and clickable`() = runAndroidComposeUiTest {
val filter1 = aSpaceServiceFilter(displayName = "Space One") val filter1 = aSpaceServiceFilter(displayName = "Space One")
val filter2 = aSpaceServiceFilter(displayName = "Space Two") val filter2 = aSpaceServiceFilter(displayName = "Space Two")
val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>() val eventsRecorder = EventsRecorder<SpaceFiltersEvent.Selecting>()
rule.setSpaceFiltersView( setSpaceFiltersView(
state = aSelectingSpaceFiltersState( state = aSelectingSpaceFiltersState(
availableFilters = listOf(filter1, filter2), availableFilters = listOf(filter1, filter2),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -61,17 +59,17 @@ class SpaceFiltersViewTest {
) )
// Both filters should be visible // Both filters should be visible
rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() onNodeWithText(filter1.spaceRoom.displayName).assertExists()
rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() onNodeWithText(filter2.spaceRoom.displayName).assertExists()
// Click on second filter // Click on second filter
rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() onNodeWithText(filter2.spaceRoom.displayName).performClick()
eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceFiltersView( private fun AndroidComposeUiTest<ComponentActivity>.setSpaceFiltersView(
state: SpaceFiltersState, state: SpaceFiltersState,
) { ) {
setContent { setContent {

View file

@ -33,6 +33,7 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.invite.impl.declineandblock package io.element.android.features.invite.impl.declineandblock
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.invite.impl.R import io.element.android.features.invite.impl.R
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -21,98 +24,94 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DeclineAndBlockViewTest { class DeclineAndBlockViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke the expected callback`() { fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on decline when enabled emits the expected event`() { fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>() val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
blockUser = true, blockUser = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
} }
@Test @Test
fun `clicking on decline when disabled does not emit event`() { fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
blockUser = false, blockUser = false,
reportRoom = false, reportRoom = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
@Test @Test
fun `clicking on block option emits the expected event`() { fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>() val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
blockUser = true, blockUser = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_decline_and_block_block_user_option_title) clickOn(R.string.screen_decline_and_block_block_user_option_title)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
} }
@Test @Test
fun `clicking on report room option emits the expected event`() { fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>() val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
reportRoom = true, reportRoom = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_report_room) clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
} }
@Test @Test
fun `typing text in the reason field emits the expected Event`() { fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>() val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView( setDeclineAndBlockView(
aDeclineAndBlockState( aDeclineAndBlockState(
reportRoom = true, reportRoom = true,
reportReason = "", reportReason = "",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText("").performTextInput("Spam!") onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeclineAndBlockView( private fun AndroidComposeUiTest<ComponentActivity>.setDeclineAndBlockView(
state: DeclineAndBlockState, state: DeclineAndBlockState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -16,6 +16,7 @@ android {
dependencies { dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.test) implementation(projects.libraries.matrix.test)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.test.anInviteData import io.element.android.features.invite.test.anInviteData
@ -26,116 +29,112 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class JoinRoomViewTest { class JoinRoomViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke the expected callback`() { fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on Join room on CanJoin room emits the expected Event`() { fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_join_room_join_action) clickOn(R.string.screen_join_room_join_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom) eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
} }
@Test @Test
fun `clicking on Knock room on CanKnock room emits the expected Event`() { fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
knockMessage = "Knock knock", knockMessage = "Knock knock",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_join_room_knock_action) clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom) eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
} }
@Test @Test
fun `clicking on closing Knock error emits the expected Event`() { fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
knockAction = AsyncAction.Failure(Exception("Error")), knockAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test @Test
fun `clicking on cancel knock request emit the expected Event`() { fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_join_room_cancel_knock_action) clickOn(R.string.screen_join_room_cancel_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true)) eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
} }
@Test @Test
fun `clicking on closing Cancel Knock error emits the expected Event`() { fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
cancelKnockAction = AsyncAction.Failure(Exception("Error")), cancelKnockAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test @Test
fun `clicking on closing Join error emits the expected Event`() { fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
joinAction = AsyncAction.Failure(Exception("Error")), joinAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test @Test
fun `when joining room is successful, the expected callback is invoked`() { fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
joinAction = AsyncAction.Success(Unit), joinAction = AsyncAction.Success(Unit),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -146,53 +145,55 @@ class JoinRoomViewTest {
} }
@Test @Test
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val inviteData = anInviteData() val inviteData = anInviteData()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_accept) clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData))
} }
@Test @Test
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val inviteData = anInviteData() val inviteData = anInviteData()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false)) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false))
} }
@Test @Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() { fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) runAndroidComposeUiTest {
val inviteData = anInviteData() val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val joinRoomState = aJoinRoomState( val inviteData = anInviteData()
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), val joinRoomState = aJoinRoomState(
canReportRoom = true, contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
eventSink = eventsRecorder, canReportRoom = true,
) eventSink = eventsRecorder,
ensureCalledOnceWithParam(inviteData) {
rule.setJoinRoomView(
state = joinRoomState,
onDeclineInviteAndBlockUser = it,
) )
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) ensureCalledOnceWithParam(inviteData) {
setJoinRoomView(
state = joinRoomState,
onDeclineInviteAndBlockUser = it,
)
clickOn(R.string.screen_join_room_decline_and_block_button_title)
}
} }
} }
@Test @Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() { fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val inviteData = anInviteData() val inviteData = anInviteData()
val joinRoomState = aJoinRoomState( val joinRoomState = aJoinRoomState(
@ -200,29 +201,29 @@ class JoinRoomViewTest {
canReportRoom = false, canReportRoom = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.setJoinRoomView(state = joinRoomState) setJoinRoomView(state = joinRoomState)
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true)) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true))
} }
@Test @Test
fun `clicking on Retry when an error occurs emits the expected Event`() { fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aFailureContentState(), contentState = aFailureContentState(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
} }
@Test @Test
fun `clicking on ok when user is unauthorized the expected callback`() { fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(), contentState = aLoadedContentState(),
joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin), joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin),
@ -230,25 +231,25 @@ class JoinRoomViewTest {
), ),
onBackClick = it onBackClick = it
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
} }
} }
@Test @Test
fun `clicking on forget when user is banned invokes the expected callback`() { fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_join_room_forget_action) clickOn(R.string.screen_join_room_forget_action)
eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom) eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView( private fun AndroidComposeUiTest<ComponentActivity>.setJoinRoomView(
state: JoinRoomState, state: JoinRoomState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onJoinSuccess: () -> Unit = EnsureNeverCalled(), onJoinSuccess: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.knockrequests.impl.banner package io.element.android.features.knockrequests.impl.banner
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.R
import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
@ -21,35 +24,30 @@ import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class KnockRequestsBannerViewTest { class KnockRequestsBannerViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on view on single request invoke the expected callback`() { fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setKnockRequestsBannerView( setKnockRequestsBannerView(
state = aKnockRequestsBannerState( state = aKnockRequestsBannerState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onViewRequestsClick = it onViewRequestsClick = it
) )
rule.clickOn(R.string.screen_room_single_knock_request_view_button_title) clickOn(R.string.screen_room_single_knock_request_view_button_title)
} }
} }
@Test @Test
fun `clicking on view all when multiple requests invoke the expected callback`() { fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setKnockRequestsBannerView( setKnockRequestsBannerView(
state = aKnockRequestsBannerState( state = aKnockRequestsBannerState(
knockRequests = listOf( knockRequests = listOf(
aKnockRequestPresentable(displayName = "Alice"), aKnockRequestPresentable(displayName = "Alice"),
@ -60,37 +58,37 @@ class KnockRequestsBannerViewTest {
), ),
onViewRequestsClick = it onViewRequestsClick = it
) )
rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title)
} }
} }
@Test @Test
fun `clicking on accept on a single request emit the expected event`() { fun `clicking on accept on a single request emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>() val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
rule.setKnockRequestsBannerView( setKnockRequestsBannerView(
state = aKnockRequestsBannerState( state = aKnockRequestsBannerState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_accept) clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest) eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest)
} }
@Test @Test
fun `clicking on dismiss emit the expected event`() { fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>() val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
rule.setKnockRequestsBannerView( setKnockRequestsBannerView(
state = aKnockRequestsBannerState( state = aKnockRequestsBannerState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val close = rule.activity.getString(CommonStrings.action_close) val close = activity!!.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick() onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss) eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsBannerView( private fun AndroidComposeUiTest<ComponentActivity>.setKnockRequestsBannerView(
state: KnockRequestsBannerState, state: KnockRequestsBannerState,
onViewRequestsClick: () -> Unit = EnsureNeverCalled(), onViewRequestsClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.knockrequests.impl.list package io.element.android.features.knockrequests.impl.list
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.R
import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
@ -23,90 +26,86 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class KnockRequestsListViewTest { class KnockRequestsListViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke the expected callback`() { fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<KnockRequestsListEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on accept emit the expected event`() { fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequest = aKnockRequestPresentable() val knockRequest = aKnockRequestPresentable()
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(persistentListOf(knockRequest)), knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_accept) clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest)) eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest))
} }
@Test @Test
fun `clicking on decline emit the expected event`() { fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequest = aKnockRequestPresentable() val knockRequest = aKnockRequestPresentable()
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(persistentListOf(knockRequest)), knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest))
} }
@Test @Test
fun `clicking on decline and ban emit the expected event`() { fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequest = aKnockRequestPresentable() val knockRequest = aKnockRequestPresentable()
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(persistentListOf(knockRequest)), knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest)) eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest))
} }
@Test @Test
fun `clicking on accept all emit the expected event`() { fun `clicking on accept all emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(knockRequests), knockRequests = AsyncData.Success(knockRequests),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title) clickOn(R.string.screen_knock_requests_list_accept_all_button_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll) eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll)
} }
@Test @Test
fun `retry on async view retry emit the expected event`() { fun `retry on async view retry emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(knockRequests), knockRequests = AsyncData.Success(knockRequests),
asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")),
@ -114,15 +113,15 @@ class KnockRequestsListViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction)
} }
@Test @Test
fun `canceling async view emit the expected event`() { fun `canceling async view emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(knockRequests), knockRequests = AsyncData.Success(knockRequests),
asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")),
@ -130,15 +129,15 @@ class KnockRequestsListViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction)
} }
@Test @Test
fun `confirming async view emit the expected event`() { fun `confirming async view emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>() val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
rule.setKnockRequestsListView( setKnockRequestsListView(
aKnockRequestsListState( aKnockRequestsListState(
knockRequests = AsyncData.Success(knockRequests), knockRequests = AsyncData.Success(knockRequests),
asyncAction = AsyncAction.ConfirmingNoParams, asyncAction = AsyncAction.ConfirmingNoParams,
@ -146,12 +145,12 @@ class KnockRequestsListViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setKnockRequestsListView( private fun AndroidComposeUiTest<ComponentActivity>.setKnockRequestsListView(
state: KnockRequestsListState, state: KnockRequestsListState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -5,11 +5,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.desktop package io.element.android.features.linknewdevice.impl.screens.desktop
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.linknewdevice.impl.R import io.element.android.features.linknewdevice.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -18,42 +21,37 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DesktopNoticeViewTest { class DesktopNoticeViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aDesktopNoticeState(), state = aDesktopNoticeState(),
onBackClicked = callback, onBackClicked = callback,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on back button clicked - calls the expected callback`() { fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aDesktopNoticeState(), state = aDesktopNoticeState(),
onBackClicked = callback, onBackClicked = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `when can continue - calls the expected callback`() { fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aDesktopNoticeState(canContinue = true), state = aDesktopNoticeState(canContinue = true),
onReadyToScanClick = callback, onReadyToScanClick = callback,
) )
@ -61,16 +59,16 @@ class DesktopNoticeViewTest {
} }
@Test @Test
fun `on submit button clicked - emits the Continue event`() { fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<DesktopNoticeEvent>() val eventRecorder = EventsRecorder<DesktopNoticeEvent>()
rule.setView( setView(
state = aDesktopNoticeState(eventSink = eventRecorder), state = aDesktopNoticeState(eventSink = eventRecorder),
) )
rule.clickOn(R.string.screen_link_new_device_desktop_submit) clickOn(R.string.screen_link_new_device_desktop_submit)
eventRecorder.assertSingle(DesktopNoticeEvent.Continue) eventRecorder.assertSingle(DesktopNoticeEvent.Continue)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: DesktopNoticeState, state: DesktopNoticeState,
onBackClicked: () -> Unit = EnsureNeverCalled(), onBackClicked: () -> Unit = EnsureNeverCalled(),
onReadyToScanClick: () -> Unit = EnsureNeverCalled(), onReadyToScanClick: () -> Unit = EnsureNeverCalled(),

View file

@ -5,58 +5,56 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.error package io.element.android.features.linknewdevice.impl.screens.error
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ErrorViewTest { class ErrorViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the onCancel callback`() { fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setErrorView( setErrorView(
onCancel = callback, onCancel = callback,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on try again button clicked - calls the expected callback`() { fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setErrorView( setErrorView(
onRetry = callback onRetry = callback
) )
rule.clickOn(CommonStrings.action_try_again) clickOn(CommonStrings.action_try_again)
} }
} }
@Test @Test
fun `on cancel button clicked - calls the expected callback`() { fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setErrorView( setErrorView(
onCancel = callback onCancel = callback
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setErrorView( private fun AndroidComposeUiTest<ComponentActivity>.setErrorView(
onRetry: () -> Unit = EnsureNeverCalled(), onRetry: () -> Unit = EnsureNeverCalled(),
onCancel: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(),
errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError, errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError,

View file

@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.number package io.element.android.features.linknewdevice.impl.screens.number
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -20,65 +23,60 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class EnterNumberViewTest { class EnterNumberViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aEnterNumberState(), state = aEnterNumberState(),
onBackClicked = callback, onBackClicked = callback,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on back button clicked - calls the expected callback`() { fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aEnterNumberState(), state = aEnterNumberState(),
onBackClicked = callback, onBackClicked = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `on continue button clicked - emits the Continue event`() { fun `on continue button clicked - emits the Continue event`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<EnterNumberEvent>() val eventRecorder = EventsRecorder<EnterNumberEvent>()
rule.setView( setView(
state = aEnterNumberState( state = aEnterNumberState(
number = "12", number = "12",
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventRecorder.assertSingle(EnterNumberEvent.Continue) eventRecorder.assertSingle(EnterNumberEvent.Continue)
} }
@Test @Test
fun `when the number is not complete, continue button is disabled`() { fun `when the number is not complete, continue button is disabled`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<EnterNumberEvent>(expectEvents = false) val eventRecorder = EventsRecorder<EnterNumberEvent>(expectEvents = false)
rule.setView( setView(
state = aEnterNumberState( state = aEnterNumberState(
number = "1", number = "1",
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
) )
val continueStr = rule.activity.getString(CommonStrings.action_continue) val continueStr = activity!!.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsNotEnabled() onNodeWithText(continueStr).assertIsNotEnabled()
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: EnterNumberState, state: EnterNumberState,
onBackClicked: () -> Unit = EnsureNeverCalled(), onBackClicked: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -5,36 +5,34 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.qrcode package io.element.android.features.linknewdevice.impl.screens.qrcode
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ShowQrCodeViewTest { class ShowQrCodeViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
onBackClick = callback onBackClick = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {

View file

@ -5,11 +5,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.root package io.element.android.features.linknewdevice.impl.screens.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.linknewdevice.impl.R import io.element.android.features.linknewdevice.impl.R
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -19,74 +22,69 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LinkNewDeviceRootViewTest { class LinkNewDeviceRootViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the onRetry callback`() { fun `on back pressed - calls the onRetry callback`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false) val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLinkNewDeviceRootView( setLinkNewDeviceRootView(
state = aLinkNewDeviceRootState( state = aLinkNewDeviceRootState(
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
onBackClick = callback onBackClick = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `link desktop button clicked - calls the expected callback`() { fun `link desktop button clicked - calls the expected callback`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false) val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLinkNewDeviceRootView( setLinkNewDeviceRootView(
state = aLinkNewDeviceRootState( state = aLinkNewDeviceRootState(
isSupported = AsyncData.Success(true), isSupported = AsyncData.Success(true),
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
onLinkDesktopDeviceClick = callback, onLinkDesktopDeviceClick = callback,
) )
rule.clickOn(R.string.screen_link_new_device_root_desktop_computer) clickOn(R.string.screen_link_new_device_root_desktop_computer)
} }
} }
@Test @Test
fun `link mobile button clicked - emits the expected event`() { fun `link mobile button clicked - emits the expected event`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>() val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>()
rule.setLinkNewDeviceRootView( setLinkNewDeviceRootView(
state = aLinkNewDeviceRootState( state = aLinkNewDeviceRootState(
isSupported = AsyncData.Success(true), isSupported = AsyncData.Success(true),
eventSink = eventRecorder, eventSink = eventRecorder,
) )
) )
rule.clickOn(R.string.screen_link_new_device_root_mobile_device) clickOn(R.string.screen_link_new_device_root_mobile_device)
eventRecorder.assertSingle(LinkNewDeviceRootEvent.LinkMobileDevice) eventRecorder.assertSingle(LinkNewDeviceRootEvent.LinkMobileDevice)
} }
@Test @Test
fun `not supported - dismiss click - invokes the expected callback`() { fun `not supported - dismiss click - invokes the expected callback`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false) val eventRecorder = EventsRecorder<LinkNewDeviceRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLinkNewDeviceRootView( setLinkNewDeviceRootView(
state = aLinkNewDeviceRootState( state = aLinkNewDeviceRootState(
isSupported = AsyncData.Success(false), isSupported = AsyncData.Success(false),
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.clickOn(CommonStrings.action_dismiss) clickOn(CommonStrings.action_dismiss)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLinkNewDeviceRootView( private fun AndroidComposeUiTest<ComponentActivity>.setLinkNewDeviceRootView(
state: LinkNewDeviceRootState = aLinkNewDeviceRootState(), state: LinkNewDeviceRootState = aLinkNewDeviceRootState(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(), onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(),

View file

@ -5,11 +5,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.linknewdevice.impl.screens.scan package io.element.android.features.linknewdevice.impl.screens.scan
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
@ -19,44 +22,39 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ScanQrCodeViewTest { class ScanQrCodeViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<ScanQrCodeEvent>(expectEvents = false) val eventRecorder = EventsRecorder<ScanQrCodeEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
state = aScanQrCodeState( state = aScanQrCodeState(
eventSink = eventRecorder, eventSink = eventRecorder,
), ),
onBackClick = callback onBackClick = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `try again button clicked - emits the expected event`() { fun `try again button clicked - emits the expected event`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<ScanQrCodeEvent>() val eventRecorder = EventsRecorder<ScanQrCodeEvent>()
rule.setView( setView(
state = aScanQrCodeState( state = aScanQrCodeState(
scanAction = AsyncAction.Failure(AN_EXCEPTION), scanAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventRecorder, eventSink = eventRecorder,
) )
) )
rule.clickOn(CommonStrings.action_try_again) clickOn(CommonStrings.action_try_again)
eventRecorder.assertSingle(ScanQrCodeEvent.TryAgain) eventRecorder.assertSingle(ScanQrCodeEvent.TryAgain)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: ScanQrCodeState = aScanQrCodeState(), state: ScanQrCodeState = aScanQrCodeState(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -5,15 +5,18 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.location.impl.share package io.element.android.features.location.impl.share
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -23,102 +26,98 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ShareLocationViewTest { class ShareLocationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `test back action`() { fun `test back action`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<ShareLocationEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setShareLocationView( setShareLocationView(
state = aShareLocationState( state = aShareLocationState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = callback, navigateUp = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `test fab click`() { fun `test fab click`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() onNodeWithTag(TestTags.floatingActionButton.value).performClick()
eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation) eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation)
} }
@Test @Test
fun `when permission denied is displayed user can open the settings`() { fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings) eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings)
} }
@Test @Test
fun `when permission denied is displayed user can close the dialog`() { fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
} }
@Test @Test
fun `when permission rationale is displayed user can request permissions`() { fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions) eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions)
} }
@Test @Test
fun `when permission rationale is displayed user can close the dialog`() { fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
} }
@Test @Test
fun `when location service disabled is displayed user can open location settings`() { fun `when location service disabled is displayed user can open location settings`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
hasLocationPermission = true, hasLocationPermission = true,
@ -126,14 +125,14 @@ class ShareLocationViewTest {
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings) eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings)
} }
@Test @Test
fun `when location service disabled is displayed user can close the dialog`() { fun `when location service disabled is displayed user can close the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShareLocationEvent>() val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView( setShareLocationView(
aShareLocationState( aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
hasLocationPermission = true, hasLocationPermission = true,
@ -141,12 +140,12 @@ class ShareLocationViewTest {
), ),
navigateUp = EnsureNeverCalled(), navigateUp = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShareLocationView( private fun AndroidComposeUiTest<ComponentActivity>.setShareLocationView(
state: ShareLocationState, state: ShareLocationState,
navigateUp: () -> Unit = EnsureNeverCalled(), navigateUp: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,16 +6,19 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.location.impl.show package io.element.android.features.location.impl.show
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
@ -26,115 +29,111 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ShowLocationViewTest { class ShowLocationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `test back action`() { fun `test back action`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<ShowLocationEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setShowLocationView( setShowLocationView(
state = aShowLocationState( state = aShowLocationState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `test share action`() { fun `test share action`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
val shareContentDescription = rule.activity.getString(CommonStrings.action_share) val shareContentDescription = activity!!.getString(CommonStrings.action_share)
rule.onNodeWithContentDescription(shareContentDescription).performClick() onNodeWithContentDescription(shareContentDescription).performClick()
// The default aStaticLocationMode uses Location(1.23, 2.34, 4f) // The default aStaticLocationMode uses Location(1.23, 2.34, 4f)
eventsRecorder.assertSingle(ShowLocationEvent.Share(Location(1.23, 2.34, 4f))) eventsRecorder.assertSingle(ShowLocationEvent.Share(Location(1.23, 2.34, 4f)))
} }
@Test @Test
fun `test fab click`() { fun `test fab click`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() onNodeWithTag(TestTags.floatingActionButton.value).performClick()
eventsRecorder.assertSingle(ShowLocationEvent.TrackMyLocation(true)) eventsRecorder.assertSingle(ShowLocationEvent.TrackMyLocation(true))
} }
@Test @Test
fun `when permission denied is displayed user can open the settings`() { fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvent.OpenAppSettings) eventsRecorder.assertSingle(ShowLocationEvent.OpenAppSettings)
} }
@Test @Test
fun `when permission denied is displayed user can close the dialog`() { fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog)
} }
@Test @Test
fun `when permission rationale is displayed user can request permissions`() { fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvent.RequestPermissions) eventsRecorder.assertSingle(ShowLocationEvent.RequestPermissions)
} }
@Test @Test
fun `when permission rationale is displayed user can close the dialog`() { fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ShowLocationEvent>() val eventsRecorder = EventsRecorder<ShowLocationEvent>()
rule.setShowLocationView( setShowLocationView(
aShowLocationState( aShowLocationState(
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = EnsureNeverCalled(), onBackClick = EnsureNeverCalled(),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView( private fun AndroidComposeUiTest<ComponentActivity>.setShowLocationView(
state: ShowLocationState, state: ShowLocationState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -16,7 +16,7 @@ android {
dependencies { dependencies {
api(projects.features.location.api) api(projects.features.location.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(libs.appyx.core)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
} }

View file

@ -6,60 +6,57 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.lockscreen.impl.unlock.keypad package io.element.android.features.lockscreen.impl.unlock.keypad
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.pressKey
import androidx.compose.ui.test.requestFocus import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class PinKeypadTest { class PinKeypadTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on a number emits the expected event`() { fun `clicking on a number emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinKeypadModel>() val eventsRecorder = EventsRecorder<PinKeypadModel>()
rule.setPinKeyPad(onClick = eventsRecorder) setPinKeyPad(onClick = eventsRecorder)
rule.onNode(hasText("1")).performClick() onNode(hasText("1")).performClick()
eventsRecorder.assertSingle(PinKeypadModel.Number('1')) eventsRecorder.assertSingle(PinKeypadModel.Number('1'))
} }
@Test @Test
fun `clicking on the delete previous character button emits the expected event`() { fun `clicking on the delete previous character button emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinKeypadModel>() val eventsRecorder = EventsRecorder<PinKeypadModel>()
rule.setPinKeyPad(onClick = eventsRecorder) setPinKeyPad(onClick = eventsRecorder)
rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.a11y_delete))).performClick() onNode(hasContentDescription(activity!!.getString(CommonStrings.a11y_delete))).performClick()
eventsRecorder.assertSingle(PinKeypadModel.Back) eventsRecorder.assertSingle(PinKeypadModel.Back)
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
@Test @Test
fun `typing using the hardware keyboard emits the expected events`() { fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinKeypadModel>() val eventsRecorder = EventsRecorder<PinKeypadModel>()
rule.setPinKeyPad(onClick = eventsRecorder) setPinKeyPad(onClick = eventsRecorder)
rule.onNodeWithText("1").requestFocus() onNodeWithText("1").requestFocus()
rule.onAllNodes(isRoot())[0].performKeyInput { onAllNodes(isRoot())[0].performKeyInput {
val keys = listOf( val keys = listOf(
Key.A, Key.A,
Key.NumPad1, Key.NumPad1,
@ -118,7 +115,7 @@ class PinKeypadTest {
) )
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinKeyPad( private fun AndroidComposeUiTest<ComponentActivity>.setPinKeyPad(
onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(),
) { ) {
setContent { setContent {

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.chooseaccountprovider package io.element.android.features.login.impl.screens.chooseaccountprovider
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -25,36 +28,31 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ChooseAccountProviderViewTest { class ChooseAccountProviderViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<ChooseAccountProviderEvents>(expectEvents = false) val eventSink = EventsRecorder<ChooseAccountProviderEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setChooseAccountProviderView( setChooseAccountProviderView(
state = aChooseAccountProviderState( state = aChooseAccountProviderState(
eventSink = eventSink, eventSink = eventSink,
), ),
onBackClick = it, onBackClick = it,
) )
rule.pressBack() pressBack()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `selecting an account provider emits the the expected event`() { fun `selecting an account provider emits the the expected event`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<ChooseAccountProviderEvents>() val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
rule.setChooseAccountProviderView( setChooseAccountProviderView(
state = aChooseAccountProviderState( state = aChooseAccountProviderState(
accountProviders = listOf( accountProviders = listOf(
ChooseAccountProviderPresenterTest.accountProvider1, ChooseAccountProviderPresenterTest.accountProvider1,
@ -64,24 +62,24 @@ class ChooseAccountProviderViewTest {
eventSink = eventSink, eventSink = eventSink,
), ),
) )
rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick()
eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1))
} }
@Test @Test
fun `when error is displayed - closing the dialog emits the expected event`() { fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<ChooseAccountProviderEvents>() val eventSink = EventsRecorder<ChooseAccountProviderEvents>()
rule.setChooseAccountProviderView( setChooseAccountProviderView(
state = aChooseAccountProviderState( state = aChooseAccountProviderState(
loginMode = AsyncData.Failure(AN_EXCEPTION), loginMode = AsyncData.Failure(AN_EXCEPTION),
eventSink = eventSink, eventSink = eventSink,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) eventSink.assertSingle(ChooseAccountProviderEvents.ClearError)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView( private fun AndroidComposeUiTest<ComponentActivity>.setChooseAccountProviderView(
state: ChooseAccountProviderState, state: ChooseAccountProviderState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,20 +6,23 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.loginpassword package io.element.android.features.login.impl.screens.loginpassword
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
@ -30,158 +33,154 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LoginPasswordViewTest { class LoginPasswordViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke back callback`() { fun `clicking on back invoke back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `changing login invokes the expected event`() { fun `changing login invokes the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>() val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val userNameHint = rule.activity.getString(CommonStrings.common_username) val userNameHint = activity!!.getString(CommonStrings.common_username)
rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME) onNodeWithText(userNameHint).performTextInput(A_USER_NAME)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin(A_USER_NAME) LoginPasswordEvents.SetLogin(A_USER_NAME)
) )
} }
@Test @Test
fun `changing login removes new lines the expected event`() { fun `changing login removes new lines the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>() val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val userNameHint = rule.activity.getString(CommonStrings.common_username) val userNameHint = activity!!.getString(CommonStrings.common_username)
rule.onNodeWithText(userNameHint).performTextInput("a\nb") onNodeWithText(userNameHint).performTextInput("a\nb")
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin("ab") LoginPasswordEvents.SetLogin("ab")
) )
} }
@Test @Test
fun `clearing login invokes the expected event`() { fun `clearing login invokes the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>() val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
formState = aLoginFormState(A_USER_NAME), formState = aLoginFormState(A_USER_NAME),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val a11yClear = rule.activity.getString(CommonStrings.action_clear) val a11yClear = activity!!.getString(CommonStrings.action_clear)
rule.onNodeWithContentDescription(a11yClear).performClick() onNodeWithContentDescription(a11yClear).performClick()
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LoginPasswordEvents.SetLogin("") LoginPasswordEvents.SetLogin("")
) )
} }
@Test @Test
fun `changing password invokes the expected event`() { fun `changing password invokes the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>() val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val userNameHint = rule.activity.getString(CommonStrings.common_password) val userNameHint = activity!!.getString(CommonStrings.common_password)
rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD) onNodeWithText(userNameHint).performTextInput(A_PASSWORD)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LoginPasswordEvents.SetPassword(A_PASSWORD) LoginPasswordEvents.SetPassword(A_PASSWORD)
) )
} }
@Test @Test
fun `reveal password makes the password visible`() { fun `reveal password makes the password visible`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
formState = aLoginFormState(password = A_PASSWORD), formState = aLoginFormState(password = A_PASSWORD),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••"))
val resources = activity!!.resources
// Show password // Show password
val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) val a11yShowPassword = resources.getString(CommonStrings.a11y_show_password)
rule.onNodeWithContentDescription(a11yShowPassword).performClick() onNodeWithContentDescription(a11yShowPassword).performClick()
rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD))
// Hide password // Hide password
val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) val a11yHidePassword = resources.getString(CommonStrings.a11y_hide_password)
rule.onNodeWithContentDescription(a11yHidePassword).performClick() onNodeWithContentDescription(a11yHidePassword).performClick()
rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••"))
} }
@Test @Test
fun `when login is empty, continue button is not enabled`() { fun `when login is empty, continue button is not enabled`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
formState = aLoginFormState(password = A_PASSWORD), formState = aLoginFormState(password = A_PASSWORD),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val continueStr = rule.activity.getString(CommonStrings.action_continue) val continueStr = activity!!.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsNotEnabled() onNodeWithText(continueStr).assertIsNotEnabled()
} }
@Test @Test
fun `when password is empty, continue button is not enabled`() { fun `when password is empty, continue button is not enabled`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
formState = aLoginFormState(login = A_USER_NAME), formState = aLoginFormState(login = A_USER_NAME),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val continueStr = rule.activity.getString(CommonStrings.action_continue) val continueStr = activity!!.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsNotEnabled() onNodeWithText(continueStr).assertIsNotEnabled()
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on Continue sends expected event`() { fun `clicking on Continue sends expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>() val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
rule.setLoginPasswordView( setLoginPasswordView(
aLoginPasswordState( aLoginPasswordState(
formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val continueStr = rule.activity.getString(CommonStrings.action_continue) val continueStr = activity!!.getString(CommonStrings.action_continue)
rule.onNodeWithText(continueStr).assertIsEnabled() onNodeWithText(continueStr).assertIsEnabled()
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LoginPasswordEvents.Submit LoginPasswordEvents.Submit
) )
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLoginPasswordView( private fun AndroidComposeUiTest<ComponentActivity>.setLoginPasswordView(
state: LoginPasswordState, state: LoginPasswordState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.onboarding package io.element.android.features.login.impl.screens.onboarding
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues
import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameter
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
@ -29,22 +32,17 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestParameterInjector import org.robolectric.RobolectricTestParameterInjector
@RunWith(RobolectricTestParameterInjector::class) @RunWith(RobolectricTestParameterInjector::class)
class OnboardingViewTest { class OnboardingViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `when can create account - clicking on create account calls the expected callback`() { fun `when can create account - clicking on create account calls the expected callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canCreateAccount = true, canCreateAccount = true,
showDeveloperSettings = false, showDeveloperSettings = false,
@ -52,40 +50,40 @@ class OnboardingViewTest {
), ),
onCreateAccount = callback, onCreateAccount = callback,
) )
rule.clickOn(R.string.screen_onboarding_sign_up) clickOn(R.string.screen_onboarding_sign_up)
// Developer settings should not be shown // Developer settings should not be shown
val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options) val developerSettingsText = activity!!.getString(CommonStrings.common_developer_options)
rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() onNodeWithContentDescription(developerSettingsText).assertDoesNotExist()
} }
} }
@Test @Test
fun `when can go back - clicking on back calls the expected callback`() { fun `when can go back - clicking on back calls the expected callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
isAddingAccount = true, isAddingAccount = true,
eventSink = eventSink, eventSink = eventSink,
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canLoginWithQrCode = true, canLoginWithQrCode = true,
eventSink = eventSink, eventSink = eventSink,
), ),
onSignInWithQrCode = callback, onSignInWithQrCode = callback,
) )
rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) clickOn(R.string.screen_onboarding_sign_in_with_qr_code)
} }
} }
@ -95,10 +93,10 @@ class OnboardingViewTest {
"can search account provider" to false, "can search account provider" to false,
"cannot search account provider" to true, "cannot search account provider" to true,
) )
) { ) = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canLoginWithQrCode = true, canLoginWithQrCode = true,
mustChooseAccountProvider = mustChooseAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider,
@ -106,7 +104,7 @@ class OnboardingViewTest {
), ),
onSignIn = callback, onSignIn = callback,
) )
rule.clickOn(R.string.screen_onboarding_sign_in_manually) clickOn(R.string.screen_onboarding_sign_in_manually)
} }
} }
@ -116,10 +114,10 @@ class OnboardingViewTest {
"can search account provider" to false, "can search account provider" to false,
"cannot search account provider" to true, "cannot search account provider" to true,
) )
) { ) = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> ensureCalledOnceWithParam(mustChooseAccountProvider) { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canLoginWithQrCode = false, canLoginWithQrCode = false,
canCreateAccount = false, canCreateAccount = false,
@ -128,89 +126,89 @@ class OnboardingViewTest {
), ),
onSignIn = callback, onSignIn = callback,
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
} }
} }
@Test @Test
fun `when sign in to pre defined account provider - clicking on button emits the expected event`() { fun `when sign in to pre defined account provider - clicking on button emits the expected event`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
defaultAccountProvider = "element.io", defaultAccountProvider = "element.io",
eventSink = eventSink, eventSink = eventSink,
), ),
) )
val buttonText = rule.activity.getString(R.string.screen_onboarding_sign_in_to, "element.io") val buttonText = activity!!.getString(R.string.screen_onboarding_sign_in_to, "element.io")
rule.onNodeWithText(buttonText).performClick() onNodeWithText(buttonText).performClick()
eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io"))
} }
@Test @Test
fun `when error is displayed - closing the dialog emits the expected event`() { fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
defaultAccountProvider = "element.io", defaultAccountProvider = "element.io",
loginMode = AsyncData.Failure(AN_EXCEPTION), loginMode = AsyncData.Failure(AN_EXCEPTION),
eventSink = eventSink, eventSink = eventSink,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventSink.assertSingle(OnBoardingEvents.ClearError) eventSink.assertSingle(OnBoardingEvents.ClearError)
} }
@Test @Test
fun `clicking on report a problem calls the sign in callback`() { fun `clicking on report a problem calls the sign in callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canReportBug = true, canReportBug = true,
eventSink = eventSink, eventSink = eventSink,
), ),
onReportProblem = callback, onReportProblem = callback,
) )
val text = rule.activity.getString(CommonStrings.common_report_a_problem) val text = activity!!.getString(CommonStrings.common_report_a_problem)
rule.onNodeWithText(text).assertExists() onNodeWithText(text).assertExists()
rule.clickOn(CommonStrings.common_report_a_problem) clickOn(CommonStrings.common_report_a_problem)
} }
} }
@Test @Test
fun `clicking on settings calls the developer settings callback`() { fun `clicking on settings calls the developer settings callback`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
showDeveloperSettings = true, showDeveloperSettings = true,
eventSink = eventSink, eventSink = eventSink,
), ),
onDeveloperSettingsClick = callback, onDeveloperSettingsClick = callback,
) )
val text = rule.activity.getString(CommonStrings.common_developer_options) val text = activity!!.getString(CommonStrings.common_developer_options)
rule.onNodeWithContentDescription(text).performClick() onNodeWithContentDescription(text).performClick()
} }
} }
@Test @Test
fun `cannot report a problem when the feature is disabled`() { fun `cannot report a problem when the feature is disabled`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false) val eventSink = EventsRecorder<OnBoardingEvents>(expectEvents = false)
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
canReportBug = false, canReportBug = false,
eventSink = eventSink, eventSink = eventSink,
), ),
) )
val text = rule.activity.getString(CommonStrings.common_report_a_problem) val text = activity!!.getString(CommonStrings.common_report_a_problem)
rule.onNodeWithText(text).assertDoesNotExist() onNodeWithText(text).assertDoesNotExist()
} }
@Test @Test
fun `when success PasswordLogin - the expected callback is invoked and the event is received`() { fun `when success PasswordLogin - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.PasswordLogin), loginMode = AsyncData.Success(LoginMode.PasswordLogin),
eventSink = eventSink, eventSink = eventSink,
@ -222,11 +220,11 @@ class OnboardingViewTest {
} }
@Test @Test
fun `when success Oidc - the expected callback is invoked and the event is received`() { fun `when success Oidc - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
val oAuthDetails = OAuthDetails("aUrl") val oAuthDetails = OAuthDetails("aUrl")
ensureCalledOnceWithParam(oAuthDetails) { callback -> ensureCalledOnceWithParam(oAuthDetails) { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)),
eventSink = eventSink, eventSink = eventSink,
@ -238,11 +236,11 @@ class OnboardingViewTest {
} }
@Test @Test
fun `when success AccountCreation - the expected callback is invoked and the event is received`() { fun `when success AccountCreation - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
val oAuthDetails = OAuthDetails("aUrl") val oAuthDetails = OAuthDetails("aUrl")
ensureCalledOnceWithParam(oAuthDetails.url) { callback -> ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
rule.setOnboardingView( setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")),
eventSink = eventSink, eventSink = eventSink,
@ -253,7 +251,7 @@ class OnboardingViewTest {
eventSink.assertSingle(OnBoardingEvents.ClearError) eventSink.assertSingle(OnBoardingEvents.ClearError)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView( private fun AndroidComposeUiTest<ComponentActivity>.setOnboardingView(
state: OnBoardingState, state: OnBoardingState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,49 +6,47 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.qrcode.confirmation package io.element.android.features.login.impl.screens.qrcode.confirmation
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class QrCodeConfirmationViewTest { class QrCodeConfirmationViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeConfirmationView( setQrCodeConfirmationView(
step = QrCodeConfirmationStep.DisplayCheckCode("12"), step = QrCodeConfirmationStep.DisplayCheckCode("12"),
onCancel = callback onCancel = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on Cancel button clicked - calls the expected callback`() { fun `on Cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeConfirmationView( setQrCodeConfirmationView(
step = QrCodeConfirmationStep.DisplayVerificationCode("123456"), step = QrCodeConfirmationStep.DisplayVerificationCode("123456"),
onCancel = callback onCancel = callback
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeConfirmationView( private fun AndroidComposeUiTest<ComponentActivity>.setQrCodeConfirmationView(
step: QrCodeConfirmationStep, step: QrCodeConfirmationStep,
onCancel: () -> Unit onCancel: () -> Unit
) { ) {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.qrcode.error package io.element.android.features.login.impl.screens.qrcode.error
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -18,47 +21,42 @@ import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class QrCodeErrorViewTest { class QrCodeErrorViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the onCancel callback`() { fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeErrorView( setQrCodeErrorView(
onCancel = callback, onCancel = callback,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on try again button clicked - calls the expected callback`() { fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeErrorView( setQrCodeErrorView(
onRetry = callback, onRetry = callback,
) )
rule.clickOn(CommonStrings.action_try_again) clickOn(CommonStrings.action_try_again)
} }
} }
@Test @Test
fun `on cancel button clicked - calls the expected callback`() { fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeErrorView( setQrCodeErrorView(
onCancel = callback, onCancel = callback,
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView( private fun AndroidComposeUiTest<ComponentActivity>.setQrCodeErrorView(
onRetry: () -> Unit = EnsureNeverCalled(), onRetry: () -> Unit = EnsureNeverCalled(),
onCancel: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(),
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError, errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.qrcode.intro package io.element.android.features.login.impl.screens.qrcode.intro
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -19,42 +22,37 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class QrCodeIntroViewTest { class QrCodeIntroViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeIntroView( setQrCodeIntroView(
state = aQrCodeIntroState(), state = aQrCodeIntroState(),
onBackClicked = callback onBackClicked = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on back button clicked - calls the expected callback`() { fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeIntroView( setQrCodeIntroView(
state = aQrCodeIntroState(), state = aQrCodeIntroState(),
onBackClicked = callback onBackClicked = callback
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `when can continue - calls the expected callback`() { fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeIntroView( setQrCodeIntroView(
state = aQrCodeIntroState(canContinue = true), state = aQrCodeIntroState(canContinue = true),
onContinue = callback onContinue = callback
) )
@ -62,16 +60,16 @@ class QrCodeIntroViewTest {
} }
@Test @Test
fun `on submit button clicked - emits the Continue event`() { fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest {
val eventRecorder = EventsRecorder<QrCodeIntroEvents>() val eventRecorder = EventsRecorder<QrCodeIntroEvents>()
rule.setQrCodeIntroView( setQrCodeIntroView(
state = aQrCodeIntroState(eventSink = eventRecorder), state = aQrCodeIntroState(eventSink = eventRecorder),
) )
rule.clickOn(R.string.screen_qr_code_login_initial_state_button_title) clickOn(R.string.screen_qr_code_login_initial_state_button_title)
eventRecorder.assertSingle(QrCodeIntroEvents.Continue) eventRecorder.assertSingle(QrCodeIntroEvents.Continue)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeIntroView( private fun AndroidComposeUiTest<ComponentActivity>.setQrCodeIntroView(
state: QrCodeIntroState, state: QrCodeIntroState,
onBackClicked: () -> Unit = EnsureNeverCalled(), onBackClicked: () -> Unit = EnsureNeverCalled(),
onContinue: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(),

View file

@ -6,12 +6,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.login.impl.screens.qrcode.scan package io.element.android.features.login.impl.screens.qrcode.scan
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -24,16 +27,11 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class QrCodeScanViewTest { class QrCodeScanViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
private var provider: ProcessCameraProvider? = null private var provider: ProcessCameraProvider? = null
@Before @Before
@ -48,28 +46,28 @@ class QrCodeScanViewTest {
} }
@Test @Test
fun `on back pressed - calls the expected callback`() { fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setQrCodeScanView( setQrCodeScanView(
state = aQrCodeScanState(), state = aQrCodeScanState(),
onBackClick = callback onBackClick = callback
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `on QR code data ready - calls the expected callback`() { fun `on QR code data ready - calls the expected callback`() = runAndroidComposeUiTest {
val data = FakeMatrixQrCodeLoginData() val data = FakeMatrixQrCodeLoginData()
ensureCalledOnceWithParam<MatrixQrCodeLoginData>(data) { callback -> ensureCalledOnceWithParam<MatrixQrCodeLoginData>(data) { callback ->
rule.setQrCodeScanView( setQrCodeScanView(
state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)), state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)),
onQrCodeDataReady = callback onQrCodeDataReady = callback
) )
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeScanView( private fun AndroidComposeUiTest<ComponentActivity>.setQrCodeScanView(
state: QrCodeScanState, state: QrCodeScanState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(), onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.workmanager.api) implementation(projects.libraries.workmanager.api)
api(projects.features.logout.api) api(projects.features.logout.api)

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.logout.impl package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -21,97 +24,93 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LogoutViewTest { class LogoutViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on logout sends a LogoutEvents`() { fun `clicking on logout sends a LogoutEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>() val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_signout) clickOn(CommonStrings.action_signout)
eventsRecorder.assertSingle(LogoutEvents.Logout(false)) eventsRecorder.assertSingle(LogoutEvents.Logout(false))
} }
@Test @Test
fun `confirming logout sends a LogoutEvents`() { fun `confirming logout sends a LogoutEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>() val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(LogoutEvents.Logout(false)) eventsRecorder.assertSingle(LogoutEvents.Logout(false))
} }
@Test @Test
fun `clicking on back invoke back callback`() { fun `clicking on back invoke back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on confirm after error sends a LogoutEvents`() { fun `clicking on confirm after error sends a LogoutEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>() val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
logoutAction = AsyncAction.Failure(Exception("Failed to logout")), logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_signout_anyway) clickOn(CommonStrings.action_signout_anyway)
eventsRecorder.assertSingle(LogoutEvents.Logout(true)) eventsRecorder.assertSingle(LogoutEvents.Logout(true))
} }
@Test @Test
fun `clicking on cancel after error sends a LogoutEvents`() { fun `clicking on cancel after error sends a LogoutEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>() val eventsRecorder = EventsRecorder<LogoutEvents>()
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
logoutAction = AsyncAction.Failure(Exception("Failed to logout")), logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) eventsRecorder.assertSingle(LogoutEvents.CloseDialogs)
} }
@Test @Test
fun `last session setting button invoke onChangeRecoveryKeyClicked`() { fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setLogoutView( setLogoutView(
aLogoutState( aLogoutState(
isLastDevice = true, isLastDevice = true,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onChangeRecoveryKeyClick = callback, onChangeRecoveryKeyClick = callback,
) )
rule.clickOn(CommonStrings.common_settings) clickOn(CommonStrings.common_settings)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView( private fun AndroidComposeUiTest<ComponentActivity>.setLogoutView(
state: LogoutState, state: LogoutState,
onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(), onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.logout.impl.direct package io.element.android.features.logout.impl.direct
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
@ -21,83 +24,79 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DefaultDirectLogoutViewTest { class DefaultDirectLogoutViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on confirm logout sends expected Event`() { fun `clicking on confirm logout sends expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_signout) clickOn(CommonStrings.action_signout)
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false))
} }
@Test @Test
fun `clicking on cancel logout sends expected Event`() { fun `clicking on cancel logout sends expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
} }
@Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.") @Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.")
@Test @Test
fun `clicking on back invoke back callback`() { fun `clicking on back invoke back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.ConfirmingNoParams, logoutAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
} }
@Test @Test
fun `clicking on confirm after error sends expected Event`() { fun `clicking on confirm after error sends expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.Failure(Exception("Error")), logoutAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_signout_anyway) clickOn(CommonStrings.action_signout_anyway)
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true))
} }
@Test @Test
fun `clicking on cancel after error sends expected Event`() { fun `clicking on cancel after error sends expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DirectLogoutEvents>() val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
rule.setDefaultDirectLogoutView( setDefaultDirectLogoutView(
state = aDirectLogoutState( state = aDirectLogoutState(
logoutAction = AsyncAction.Failure(Exception("Error")), logoutAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDefaultDirectLogoutView( private fun AndroidComposeUiTest<ComponentActivity>.setDefaultDirectLogoutView(
state: DirectLogoutState, state: DirectLogoutState,
) { ) {
setContent { setContent {

View file

@ -6,13 +6,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithTag
@ -25,6 +27,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeRight import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
@ -78,82 +81,78 @@ import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MessagesViewTest { class MessagesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke expected callback`() { fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on room name invoke expected callback`() { fun `clicking on room name invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onRoomDetailsClick = callback, onRoomDetailsClick = callback,
) )
rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick()
} }
} }
@Test @Test
fun `clicking on join call invoke expected callback`() { fun `clicking on join call invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnceWithParam(false) { callback -> ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onJoinCallClick = callback, onJoinCallClick = callback,
) )
val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call) val joinCallContentDescription = activity!!.getString(CommonStrings.a11y_start_call)
rule.onNodeWithContentDescription(joinCallContentDescription).performClick() onNodeWithContentDescription(joinCallContentDescription).performClick()
} }
} }
@Test @Test
fun `clicking on join voice call invoke expected callback`() { fun `clicking on join voice call invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomCallState = aStandByCallState(isDM = true) roomCallState = aStandByCallState(isDM = true)
) )
ensureCalledOnceWithParam(true) { callback -> ensureCalledOnceWithParam(true) { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onJoinCallClick = callback, onJoinCallClick = callback,
) )
val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call) val joinVoiceCallContentDescription = activity!!.getString(CommonStrings.a11y_start_voice_call)
rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
} }
} }
@Test @Test
fun `clicking on an Event invoke expected callback`() { fun `clicking on an Event invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -167,12 +166,12 @@ class MessagesViewTest {
expectedParam2 = timelineItem, expectedParam2 = timelineItem,
result = true, result = true,
) )
rule.setMessagesView( setMessagesView(
state = state, state = state,
onEventClick = callback, onEventClick = callback,
) )
// Cannot perform click on "Text", it's not detected. Use tag instead // Cannot perform click on "Text", it's not detected. Use tag instead
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick()
callback.assertSuccess() callback.assertSuccess()
} }
@ -202,7 +201,7 @@ class MessagesViewTest {
userHasPermissionToRedactOther: Boolean = false, userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = false, userHasPermissionToSendReaction: Boolean = false,
userCanPinEvent: Boolean = false, userCanPinEvent: Boolean = false,
) { ) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ActionListEvent>() val eventsRecorder = EventsRecorder<ActionListEvent>()
val state = aMessagesState( val state = aMessagesState(
actionListState = anActionListState( actionListState = anActionListState(
@ -220,11 +219,11 @@ class MessagesViewTest {
), ),
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
// Cannot perform click on "Text", it's not detected. Use tag instead // Cannot perform click on "Text", it's not detected. Use tag instead
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() }
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
ActionListEvent.ComputeForMessage( ActionListEvent.ComputeForMessage(
event = timelineItem, event = timelineItem,
@ -235,7 +234,7 @@ class MessagesViewTest {
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `clicking on a read receipt list emits the expected Event`() { fun `clicking on a read receipt list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvent>() val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -255,10 +254,10 @@ class MessagesViewTest {
), ),
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem)) eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem))
} }
@ -272,7 +271,7 @@ class MessagesViewTest {
swipeTest(userHasPermissionToSendMessage = false) swipeTest(userHasPermissionToSendMessage = false)
} }
private fun swipeTest(userHasPermissionToSendMessage: Boolean) { private fun swipeTest(userHasPermissionToSendMessage: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true) val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true)
val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false) val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false)
@ -285,10 +284,10 @@ class MessagesViewTest {
), ),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
rule.onAllNodesWithTag(TestTags.messageBubble.value).apply { onAllNodesWithTag(TestTags.messageBubble.value).apply {
onFirst().performTouchInput { swipeRight(endX = 200f) } onFirst().performTouchInput { swipeRight(endX = 200f) }
onLast().performTouchInput { swipeRight(endX = 200f) } onLast().performTouchInput { swipeRight(endX = 200f) }
} }
@ -300,7 +299,7 @@ class MessagesViewTest {
} }
@Test @Test
fun `clicking on send location invoke expected callback`() { fun `clicking on send location invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
composerState = aMessageComposerState( composerState = aMessageComposerState(
@ -309,16 +308,16 @@ class MessagesViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onSendLocationClick = callback, onSendLocationClick = callback,
) )
rule.clickOn(R.string.screen_room_attachment_source_location) clickOn(R.string.screen_room_attachment_source_location)
} }
} }
@Test @Test
fun `clicking on create poll invoke expected callback`() { fun `clicking on create poll invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
composerState = aMessageComposerState( composerState = aMessageComposerState(
@ -327,25 +326,25 @@ class MessagesViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setMessagesView( setMessagesView(
state = state, state = state,
onCreatePollClick = callback, onCreatePollClick = callback,
) )
// Then click on the poll action // Then click on the poll action
rule.clickOn(R.string.screen_room_attachment_source_poll) clickOn(R.string.screen_room_attachment_source_poll)
} }
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `clicking on the avatar of the sender of an Event emits the expected event`() { fun `clicking on the avatar of the sender of an Event emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first() val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
rule.setMessagesView(state = state) setMessagesView(state = state)
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
MessagesEvent.OnUserClicked( MessagesEvent.OnUserClicked(
MatrixUser( MatrixUser(
@ -359,12 +358,12 @@ class MessagesViewTest {
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `clicking on the display name of the sender of an Event emits expected event`() { fun `clicking on the display name of the sender of an Event emits expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState(eventSink = eventsRecorder) val state = aMessagesState(eventSink = eventsRecorder)
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first() val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
rule.setMessagesView(state = state) setMessagesView(state = state)
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
MessagesEvent.OnUserClicked( MessagesEvent.OnUserClicked(
MatrixUser( MatrixUser(
@ -377,7 +376,7 @@ class MessagesViewTest {
} }
@Test @Test
fun `selecting a action on a message emits the expected Event`() { fun `selecting a action on a message emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState( val state = aMessagesState(
eventSink = eventsRecorder eventSink = eventsRecorder
@ -395,17 +394,17 @@ class MessagesViewTest {
) )
), ),
) )
rule.setMessagesView( setMessagesView(
state = stateWithMessageAction, state = stateWithMessageAction,
) )
rule.clickOn(CommonStrings.action_edit) clickOn(CommonStrings.action_edit)
// Give time for the close animation to complete // Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000) mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem)) eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem))
} }
@Test @Test
fun `clicking on a reaction emits the expected Event`() { fun `clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -414,10 +413,10 @@ class MessagesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
rule.onAllNodesWithText( onAllNodesWithText(
text = "👍️", text = "👍️",
useUnmergedTree = true, useUnmergedTree = true,
).onFirst().performClick() ).onFirst().performClick()
@ -425,7 +424,7 @@ class MessagesViewTest {
} }
@Test @Test
fun `long clicking on a reaction emits the expected Event`() { fun `long clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReactionSummaryEvent>() val eventsRecorder = EventsRecorder<ReactionSummaryEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -437,10 +436,10 @@ class MessagesViewTest {
), ),
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
rule.onAllNodesWithText( onAllNodesWithText(
text = "👍️", text = "👍️",
useUnmergedTree = true, useUnmergedTree = true,
).onFirst().performTouchInput { longClick() } ).onFirst().performTouchInput { longClick() }
@ -448,7 +447,7 @@ class MessagesViewTest {
} }
@Test @Test
fun `clicking on more reaction emits the expected Event`() { fun `clicking on more reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<CustomReactionEvent>() val eventsRecorder = EventsRecorder<CustomReactionEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -459,16 +458,16 @@ class MessagesViewTest {
), ),
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView( setMessagesView(
state = state, state = state,
) )
val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction) val moreReactionContentDescription = activity!!.getString(R.string.screen_room_timeline_add_reaction)
rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick()
eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem))
} }
@Test @Test
fun `clicking on more reaction from action list emits the expected Event`() { fun `clicking on more reaction from action list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<CustomReactionEvent>() val eventsRecorder = EventsRecorder<CustomReactionEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState( timelineState = aTimelineState(
@ -491,18 +490,18 @@ class MessagesViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.setMessagesView( setMessagesView(
state = stateWithActionListState, state = stateWithActionListState,
) )
val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis) val moreReactionContentDescription = activity!!.getString(CommonStrings.a11y_react_with_other_emojis)
rule.onNodeWithContentDescription(moreReactionContentDescription).performClick() onNodeWithContentDescription(moreReactionContentDescription).performClick()
// Give time for the close animation to complete // Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000) mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem))
} }
@Test @Test
fun `clicking on verified user send failure from action list emits the expected Event`() { fun `clicking on verified user send failure from action list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
val state = aMessagesState() val state = aMessagesState()
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
@ -519,21 +518,21 @@ class MessagesViewTest {
), ),
timelineState = aTimelineState(eventSink = eventsRecorder) timelineState = aTimelineState(eventSink = eventsRecorder)
) )
rule.setMessagesView( setMessagesView(
state = stateWithActionListState, state = stateWithActionListState,
) )
// Clear initial 'LoadMore' event emitted when setting the state // Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear() eventsRecorder.clear()
val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") val verifiedUserSendFailure = activity!!.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice")
rule.onNodeWithText(verifiedUserSendFailure).performClick() onNodeWithText(verifiedUserSendFailure).performClick()
// Give time for the close animation to complete // Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000) mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem))
} }
@Test @Test
fun `clicking on a custom emoji emits the expected Events`() { fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest {
val aUnicode = "🙈" val aUnicode = "🙈"
val customReactionStateEventsRecorder = EventsRecorder<CustomReactionEvent>() val customReactionStateEventsRecorder = EventsRecorder<CustomReactionEvent>()
val eventsRecorder = EventsRecorder<MessagesEvent>() val eventsRecorder = EventsRecorder<MessagesEvent>()
@ -563,18 +562,18 @@ class MessagesViewTest {
eventSink = customReactionStateEventsRecorder eventSink = customReactionStateEventsRecorder
), ),
) )
rule.setMessagesView( setMessagesView(
state = stateWithCustomReactionState, state = stateWithCustomReactionState,
) )
rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick() onNodeWithText(aUnicode, useUnmergedTree = true).performClick()
// Give time for the close animation to complete // Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000) mainClock.advanceTimeBy(milliseconds = 1_000)
customReactionStateEventsRecorder.assertSingle(CustomReactionEvent.DismissCustomReactionSheet) customReactionStateEventsRecorder.assertSingle(CustomReactionEvent.DismissCustomReactionSheet)
eventsRecorder.assertSingle(MessagesEvent.ToggleReaction(aUnicode, timelineItem.eventOrTransactionId)) eventsRecorder.assertSingle(MessagesEvent.ToggleReaction(aUnicode, timelineItem.eventOrTransactionId))
} }
@Test @Test
fun `clicking on pinned messages banner emits the expected Event`() { fun `clicking on pinned messages banner emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
val state = aMessagesState( val state = aMessagesState(
timelineState = aTimelineState(eventSink = eventsRecorder), timelineState = aTimelineState(eventSink = eventsRecorder),
@ -587,16 +586,16 @@ class MessagesViewTest {
), ),
), ),
) )
rule.setMessagesView(state = state) setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state // Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear() eventsRecorder.clear()
rule.onNodeWithText("This is a pinned message").performClick() onNodeWithText("This is a pinned message").performClick()
eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds))
} }
@Test @Test
fun `clicking on successor room button emits expected event`() { fun `clicking on successor room button emits expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
val successorRoomId = RoomId("!successor:server.org") val successorRoomId = RoomId("!successor:server.org")
val state = aMessagesState( val state = aMessagesState(
@ -606,18 +605,18 @@ class MessagesViewTest {
), ),
timelineState = aTimelineState(eventSink = eventsRecorder) timelineState = aTimelineState(eventSink = eventsRecorder)
) )
rule.setMessagesView(state = state) setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state // Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear() eventsRecorder.clear()
val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) val text = activity!!.getString(R.string.screen_room_timeline_tombstoned_room_action)
// The bottomsheet subcompose seems to make the node to appear twice // The bottomsheet subcompose seems to make the node to appear twice
rule.onAllNodesWithText(text).onFirst().performClick() onAllNodesWithText(text).onFirst().performClick()
eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId))
} }
@Test @Test
fun `clicking on threads list button calls the expected function`() { fun `clicking on threads list button calls the expected function`() = runAndroidComposeUiTest {
val state = aMessagesState( val state = aMessagesState(
threads = MessagesState.Threads( threads = MessagesState.Threads(
hasThreads = true, hasThreads = true,
@ -625,28 +624,28 @@ class MessagesViewTest {
) )
) )
val onThreadsListClicked = lambdaRecorder<Unit> {} val onThreadsListClicked = lambdaRecorder<Unit> {}
rule.setMessagesView( setMessagesView(
state = state, state = state,
onThreadsListClicked = onThreadsListClicked, onThreadsListClicked = onThreadsListClicked,
) )
rule.onNodeWithContentDescription("Threads").performClick() onNodeWithContentDescription("Threads").performClick()
onThreadsListClicked.assertions().isCalledOnce() onThreadsListClicked.assertions().isCalledOnce()
} }
@Test @Test
fun `no banner shown when there is no successor room`() { fun `no banner shown when there is no successor room`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState( val state = aMessagesState(
successorRoom = null, successorRoom = null,
eventSink = eventsRecorder eventSink = eventsRecorder
) )
rule.setMessagesView(state = state) setMessagesView(state = state)
rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message)
rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessagesView( private fun AndroidComposeUiTest<ComponentActivity>.setMessagesView(
state: MessagesState, state: MessagesState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,12 +6,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.crypto.identity package io.element.android.features.messages.impl.crypto.identity
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -21,19 +24,15 @@ import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class IdentityChangeStateViewTest { class IdentityChangeStateViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `show and resolve pin violation`() { fun `show and resolve pin violation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IdentityChangeEvent>() val eventsRecorder = EventsRecorder<IdentityChangeEvent>()
rule.setIdentityChangeStateView( setIdentityChangeStateView(
state = anIdentityChangeState( state = anIdentityChangeState(
listOf( listOf(
RoomMemberIdentityStateChange( RoomMemberIdentityStateChange(
@ -45,18 +44,18 @@ class IdentityChangeStateViewTest {
), ),
) )
rule.onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning") onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning")
rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
rule.clickOn(res = CommonStrings.action_dismiss) clickOn(res = CommonStrings.action_dismiss)
eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost"))) eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost")))
} }
@Test @Test
fun `show and resolve verification violation`() { fun `show and resolve verification violation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IdentityChangeEvent>() val eventsRecorder = EventsRecorder<IdentityChangeEvent>()
rule.setIdentityChangeStateView( setIdentityChangeStateView(
state = anIdentityChangeState( state = anIdentityChangeState(
listOf( listOf(
RoomMemberIdentityStateChange( RoomMemberIdentityStateChange(
@ -68,17 +67,17 @@ class IdentityChangeStateViewTest {
), ),
) )
rule.onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning") onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning")
rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
rule.clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action)
eventsRecorder.assertSingle(IdentityChangeEvent.WithdrawVerification(UserId("@alice:localhost"))) eventsRecorder.assertSingle(IdentityChangeEvent.WithdrawVerification(UserId("@alice:localhost")))
} }
@Test @Test
fun `Should not show any banner if no violations`() { fun `Should not show any banner if no violations`() = runAndroidComposeUiTest {
rule.setIdentityChangeStateView( setIdentityChangeStateView(
state = anIdentityChangeState( state = anIdentityChangeState(
listOf( listOf(
RoomMemberIdentityStateChange( RoomMemberIdentityStateChange(
@ -93,10 +92,10 @@ class IdentityChangeStateViewTest {
), ),
) )
rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist() onNodeWithText("identity was reset", substring = true).assertDoesNotExist()
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setIdentityChangeStateView( private fun AndroidComposeUiTest<ComponentActivity>.setIdentityChangeStateView(
state: IdentityChangeState, state: IdentityChangeState,
) { ) {
setContent { setContent {

View file

@ -6,54 +6,53 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.crypto.sendfailure.resolve package io.element.android.features.messages.impl.crypto.sendfailure.resolve
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ResolveVerifiedUserSendFailureViewTest { class ResolveVerifiedUserSendFailureViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on resolve and resend emit the expected event`() { fun `clicking on resolve and resend emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>() val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>()
rule.setResolveVerifiedUserSendFailureView( setResolveVerifiedUserSendFailureView(
state = aResolveVerifiedUserSendFailureState( state = aResolveVerifiedUserSendFailureState(
verifiedUserSendFailure = aChangedIdentitySendFailure(), verifiedUserSendFailure = aChangedIdentitySendFailure(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.ResolveAndResend) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.ResolveAndResend)
} }
@Test @Test
fun `clicking on retry emit the expected event`() { fun `clicking on retry emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>() val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>()
rule.setResolveVerifiedUserSendFailureView( setResolveVerifiedUserSendFailureView(
state = aResolveVerifiedUserSendFailureState( state = aResolveVerifiedUserSendFailureState(
verifiedUserSendFailure = aChangedIdentitySendFailure(), verifiedUserSendFailure = aChangedIdentitySendFailure(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(res = CommonStrings.action_retry) clickOn(res = CommonStrings.action_retry)
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResolveVerifiedUserSendFailureView( private fun AndroidComposeUiTest<ComponentActivity>.setResolveVerifiedUserSendFailureView(
state: ResolveVerifiedUserSendFailureState, state: ResolveVerifiedUserSendFailureState,
) { ) {
setSafeContent { setSafeContent {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.link package io.element.android.features.messages.impl.link
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -19,51 +22,46 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LinkViewTest { class LinkViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on cancel emits the expected event`() { fun `clicking on cancel emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>() val eventsRecorder = EventsRecorder<LinkEvent>()
rule.setLinkView( setLinkView(
aLinkState( aLinkState(
linkClick = ConfirmingLinkClick(aLink), linkClick = ConfirmingLinkClick(aLink),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LinkEvent.Cancel LinkEvent.Cancel
) )
} }
@Test @Test
fun `clicking on continue emits the expected event`() { fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>() val eventsRecorder = EventsRecorder<LinkEvent>()
rule.setLinkView( setLinkView(
aLinkState( aLinkState(
linkClick = ConfirmingLinkClick(aLink), linkClick = ConfirmingLinkClick(aLink),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
LinkEvent.Confirm LinkEvent.Confirm
) )
} }
@Test @Test
fun `success state invokes the callback and emits the expected event`() { fun `success state invokes the callback and emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>() val eventsRecorder = EventsRecorder<LinkEvent>()
ensureCalledOnceWithParam(aLink) { callback -> ensureCalledOnceWithParam(aLink) { callback ->
rule.setLinkView( setLinkView(
aLinkState( aLinkState(
linkClick = AsyncAction.Success(aLink), linkClick = AsyncAction.Success(aLink),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -77,7 +75,7 @@ class LinkViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLinkView( private fun AndroidComposeUiTest<ComponentActivity>.setLinkView(
state: LinkState, state: LinkState,
onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(), onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(),
) { ) {

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.pinned.banner package io.element.android.features.messages.impl.pinned.banner
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -22,49 +25,45 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PinnedMessagesBannerViewTest { class PinnedMessagesBannerViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on the banner invoke expected callback`() { fun `clicking on the banner invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>() val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>()
val state = aLoadedPinnedMessagesBannerState( val state = aLoadedPinnedMessagesBannerState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
val pinnedEventId = state.currentPinnedMessage.eventId val pinnedEventId = state.currentPinnedMessage.eventId
ensureCalledOnceWithParam(pinnedEventId) { callback -> ensureCalledOnceWithParam(pinnedEventId) { callback ->
rule.setPinnedMessagesBannerView( setPinnedMessagesBannerView(
state = state, state = state,
onClick = callback onClick = callback
) )
rule.onRoot().performClick() onRoot().performClick()
eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned) eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned)
} }
} }
@Test @Test
fun `clicking on view all emit the expected event`() { fun `clicking on view all emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>(expectEvents = true) val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>(expectEvents = true)
val state = aLoadedPinnedMessagesBannerState( val state = aLoadedPinnedMessagesBannerState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setPinnedMessagesBannerView( setPinnedMessagesBannerView(
state = state, state = state,
onViewAllClick = callback onViewAllClick = callback
) )
rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinnedMessagesBannerView( private fun AndroidComposeUiTest<ComponentActivity>.setPinnedMessagesBannerView(
state: PinnedMessagesBannerState, state: PinnedMessagesBannerState,
onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(), onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(),
onViewAllClick: () -> Unit = EnsureNeverCalled(), onViewAllClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,16 +6,19 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.pinned.list package io.element.android.features.messages.impl.pinned.list
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListEvent
import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.anActionListState
@ -31,33 +34,28 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PinnedMessagesListViewTest { class PinnedMessagesListViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back calls the expected callback`() { fun `clicking on back calls the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false)
val state = aLoadedPinnedMessagesListState( val state = aLoadedPinnedMessagesListState(
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setPinnedMessagesListView( setPinnedMessagesListView(
state = state, state = state,
onBackClick = callback onBackClick = callback
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `click on an event calls the expected callback`() { fun `click on an event calls the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false)
val content = aTimelineItemFileContent() val content = aTimelineItemFileContent()
val state = aLoadedPinnedMessagesListState( val state = aLoadedPinnedMessagesListState(
@ -67,16 +65,16 @@ class PinnedMessagesListViewTest {
val event = state.timelineItems.first() as TimelineItem.Event val event = state.timelineItems.first() as TimelineItem.Event
ensureCalledOnceWithParam(event) { callback -> ensureCalledOnceWithParam(event) { callback ->
rule.setPinnedMessagesListView( setPinnedMessagesListView(
state = state, state = state,
onEventClick = callback onEventClick = callback
) )
rule.onAllNodesWithText(content.filename).onFirst().performClick() onAllNodesWithText(content.filename).onFirst().performClick()
} }
} }
@Test @Test
fun `long click on an event emits the expected event`() { fun `long click on an event emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ActionListEvent>(expectEvents = true) val eventsRecorder = EventsRecorder<ActionListEvent>(expectEvents = true)
val content = aTimelineItemFileContent() val content = aTimelineItemFileContent()
val state = aLoadedPinnedMessagesListState( val state = aLoadedPinnedMessagesListState(
@ -84,10 +82,10 @@ class PinnedMessagesListViewTest {
actionListState = anActionListState(eventSink = eventsRecorder) actionListState = anActionListState(eventSink = eventsRecorder)
) )
rule.setPinnedMessagesListView( setPinnedMessagesListView(
state = state, state = state,
) )
rule.onAllNodesWithText(content.filename).onFirst() onAllNodesWithText(content.filename).onFirst()
.performTouchInput { .performTouchInput {
longClick() longClick()
} }
@ -96,7 +94,7 @@ class PinnedMessagesListViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinnedMessagesListView( private fun AndroidComposeUiTest<ComponentActivity>.setPinnedMessagesListView(
state: PinnedMessagesListState, state: PinnedMessagesListState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runComposeUiTest
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
@ -18,15 +21,12 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class DefaultHtmlConverterProviderTest { class DefaultHtmlConverterProviderTest {
@get:Rule val composeTestRule = createComposeRule()
private val provider = DefaultHtmlConverterProvider( private val provider = DefaultHtmlConverterProvider(
mentionSpanProvider = MentionSpanProvider( mentionSpanProvider = MentionSpanProvider(
permalinkParser = FakePermalinkParser(), permalinkParser = FakePermalinkParser(),
@ -43,8 +43,8 @@ class DefaultHtmlConverterProviderTest {
} }
@Test @Test
fun `calling provide after calling Update first should return an HtmlConverter`() { fun `calling provide after calling Update first should return an HtmlConverter`() = runComposeUiTest {
composeTestRule.setContent { setContent {
CompositionLocalProvider(LocalInspectionMode provides true) { CompositionLocalProvider(LocalInspectionMode provides true) {
provider.Update() provider.Update()
} }

View file

@ -6,15 +6,18 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.timeline.components.MessageShieldData import io.element.android.features.messages.impl.timeline.components.MessageShieldData
import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.components.aCriticalShield
@ -39,19 +42,15 @@ import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TimelineViewTest { class TimelineViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `reaching the end of the timeline with more events to load emits a LoadMore event`() { fun `reaching the end of the timeline with more events to load emits a LoadMore event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf<TimelineItem>( timelineItems = persistentListOf<TimelineItem>(
TimelineItem.Virtual( TimelineItem.Virtual(
@ -66,9 +65,9 @@ class TimelineViewTest {
} }
@Test @Test
fun `reaching the end of the timeline does not send a LoadMore event`() { fun `reaching the end of the timeline does not send a LoadMore event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -78,9 +77,9 @@ class TimelineViewTest {
} }
@Test @Test
fun `scroll to bottom on live timeline does not emit the Event`() { fun `scroll to bottom on live timeline does not emit the Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = true, isLive = true,
@ -92,14 +91,14 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear() eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick() onNodeWithContentDescription(contentDescription).performClick()
} }
@Test @Test
fun `scroll to bottom on detached timeline emits the expected Event`() { fun `scroll to bottom on detached timeline emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false, isLive = false,
@ -110,15 +109,15 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear() eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick() onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertSingle(TimelineEvent.JumpToLive) eventsRecorder.assertSingle(TimelineEvent.JumpToLive)
} }
@Test @Test
fun `an empty timeline triggers a prefetch`() { fun `an empty timeline triggers a prefetch`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf(), timelineItems = persistentListOf(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -129,9 +128,9 @@ class TimelineViewTest {
} }
@Test @Test
fun `show shield dialog`() { fun `show shield dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf<TimelineItem>( timelineItems = persistentListOf<TimelineItem>(
aTimelineItemEvent( aTimelineItemEvent(
@ -143,8 +142,8 @@ class TimelineViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details) val contentDescription = activity!!.getString(CommonStrings.a11y_encryption_details)
rule.onNodeWithContentDescription(contentDescription).performClick() onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
TimelineEvent.OnScrollFinished(0), TimelineEvent.OnScrollFinished(0),
@ -154,9 +153,9 @@ class TimelineViewTest {
} }
@Test @Test
fun `hide shield dialog`() { fun `hide shield dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false, isLive = false,
@ -167,16 +166,16 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear() eventsRecorder.clear()
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
} }
@Ignore( @Ignore(
"performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." + "performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." +
"This needs to be reworked to use a different approach to check the LoadMore event was emitted." "This needs to be reworked to use a different approach to check the LoadMore event was emitted."
) )
@Test @Test
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { fun `scrolling near to the start of the loaded items triggers a pre-fetch`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>() val eventsRecorder = EventsRecorder<TimelineEvent>()
val items = List<TimelineItem>(200) { val items = List<TimelineItem>(200) {
aTimelineItemEvent( aTimelineItemEvent(
@ -185,7 +184,7 @@ class TimelineViewTest {
) )
}.toImmutableList() }.toImmutableList()
rule.setTimelineView( setTimelineView(
state = aTimelineState( state = aTimelineState(
timelineItems = items, timelineItems = items,
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -194,9 +193,9 @@ class TimelineViewTest {
), ),
) )
rule.onNodeWithTag("timeline").performScrollToIndex(180) onNodeWithTag("timeline").performScrollToIndex(180)
rule.mainClock.advanceTimeBy(1000) mainClock.advanceTimeBy(1000)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
@ -207,7 +206,7 @@ class TimelineViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView( private fun AndroidComposeUiTest<ComponentActivity>.setTimelineView(
state: TimelineState, state: TimelineState,
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,12 +6,15 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.components.event package io.element.android.features.messages.impl.timeline.components.event
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelineEvent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
@ -20,14 +23,11 @@ import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TimelineItemPollViewTest { class TimelineItemPollViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `answering a poll with first answer should emit a PollAnswerSelected event`() { fun `answering a poll with first answer should emit a PollAnswerSelected event`() {
testAnswer(answerIndex = 0) testAnswer(answerIndex = 0)
@ -38,17 +38,17 @@ class TimelineItemPollViewTest {
testAnswer(answerIndex = 1) testAnswer(answerIndex = 1)
} }
private fun testAnswer(answerIndex: Int) { private fun testAnswer(answerIndex: Int) = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>() val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent() val content = aTimelineItemPollContent()
rule.setContent { setContent {
TimelineItemPollView( TimelineItemPollView(
content = content, content = content,
eventSink = eventsRecorder eventSink = eventsRecorder
) )
} }
val answer = content.answerItems[answerIndex].answer val answer = content.answerItems[answerIndex].answer
rule.onNode( onNode(
matcher = hasText(answer.text), matcher = hasText(answer.text),
useUnmergedTree = true, useUnmergedTree = true,
).performClick() ).performClick()
@ -56,38 +56,38 @@ class TimelineItemPollViewTest {
} }
@Test @Test
fun `editing a poll should emit a PollEditClicked event`() { fun `editing a poll should emit a PollEditClicked event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>() val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent( val content = aTimelineItemPollContent(
isMine = true, isMine = true,
isEditable = true, isEditable = true,
) )
rule.setContent { setContent {
TimelineItemPollView( TimelineItemPollView(
content = content, content = content,
eventSink = eventsRecorder eventSink = eventsRecorder
) )
} }
rule.clickOn(CommonStrings.action_edit_poll) clickOn(CommonStrings.action_edit_poll)
eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!)) eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!))
} }
@Test @Test
fun `closing a poll should emit a PollEndClicked event`() { fun `closing a poll should emit a PollEndClicked event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>() val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent( val content = aTimelineItemPollContent(
isMine = true, isMine = true,
) )
rule.setContent { setContent {
TimelineItemPollView( TimelineItemPollView(
content = content, content = content,
eventSink = eventsRecorder eventSink = eventsRecorder
) )
} }
rule.clickOn(CommonStrings.action_end_poll) clickOn(CommonStrings.action_end_poll)
// A confirmation dialog should be shown // A confirmation dialog should be shown
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(TimelineEvent.EndPoll(content.eventId!!)) eventsRecorder.assertSingle(TimelineEvent.EndPoll(content.eventId!!))
} }
} }

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.components.event package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannedString import android.text.SpannedString
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -38,45 +41,40 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.wysiwyg.view.spans.CustomMentionSpan import io.element.android.wysiwyg.view.spans.CustomMentionSpan
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TimelineTextViewTest { class TimelineTextViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
private val mentionSpanTheme = MentionSpanTheme(currentUserId = A_USER_ID) private val mentionSpanTheme = MentionSpanTheme(currentUserId = A_USER_ID)
private val formatLambda = lambdaRecorder<MentionType, CharSequence> { mentionType -> mentionType.toString() } private val formatLambda = lambdaRecorder<MentionType, CharSequence> { mentionType -> mentionType.toString() }
private val mentionSpanFormatter = FakeMentionSpanFormatter(formatLambda) private val mentionSpanFormatter = FakeMentionSpanFormatter(formatLambda)
@Test @Test
fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest { fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runAndroidComposeUiTest {
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>" val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans()).isEmpty() assertThat(result.getMentionSpans()).isEmpty()
assert(formatLambda).isNeverCalled() assert(formatLambda).isNeverCalled()
} }
@Test @Test
fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest { fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runAndroidComposeUiTest {
val charSequence = SpannableString("Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>") val charSequence = SpannableString("Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>")
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans()).isEmpty() assertThat(result.getMentionSpans()).isEmpty()
assert(formatLambda).isNeverCalled() assert(formatLambda).isNeverCalled()
} }
@Test @Test
fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest { fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runAndroidComposeUiTest {
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>" val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null))
assertThat(result.getMentionSpans()).isEmpty() assertThat(result.getMentionSpans()).isEmpty()
assertThat(result.toString()).isEqualTo(charSequence) assertThat(result.toString()).isEqualTo(charSequence)
@ -84,7 +82,7 @@ class TimelineTextViewTest {
} }
@Test @Test
fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest { fun `getTextWithResolvedMentions - with Room mention format correctly`() = runAndroidComposeUiTest {
val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias()) val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias())
val charSequence = buildSpannedString { val charSequence = buildSpannedString {
append("Hello ") append("Hello ")
@ -93,7 +91,7 @@ class TimelineTextViewTest {
} }
} }
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val expectedDisplayText = mentionType.toString() val expectedDisplayText = mentionType.toString()
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
@ -102,7 +100,7 @@ class TimelineTextViewTest {
} }
@Test @Test
fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest { fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runAndroidComposeUiTest {
val mentionType = MentionType.User(userId = A_USER_ID) val mentionType = MentionType.User(userId = A_USER_ID)
val charSequence = buildSpannedString { val charSequence = buildSpannedString {
append("Hello ") append("Hello ")
@ -111,7 +109,7 @@ class TimelineTextViewTest {
} }
} }
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val expectedDisplayText = mentionType.toString() val expectedDisplayText = mentionType.toString()
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
@ -119,7 +117,7 @@ class TimelineTextViewTest {
} }
@Test @Test
fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest { fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runAndroidComposeUiTest {
val mentionType = MentionType.User(userId = A_USER_ID) val mentionType = MentionType.User(userId = A_USER_ID)
val charSequence = buildSpannedString { val charSequence = buildSpannedString {
append("Hello ") append("Hello ")
@ -129,12 +127,12 @@ class TimelineTextViewTest {
} }
val mentionSpanUpdater = aMentionSpanUpdater() val mentionSpanUpdater = aMentionSpanUpdater()
val expectedDisplayText = mentionType.toString() val expectedDisplayText = mentionType.toString()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
assert(formatLambda).isCalledOnce() assert(formatLambda).isCalledOnce()
} }
private suspend fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.getText( private suspend fun AndroidComposeUiTest<ComponentActivity>.getText(
mentionSpanUpdater: MentionSpanUpdater, mentionSpanUpdater: MentionSpanUpdater,
content: TimelineItemTextBasedContent, content: TimelineItemTextBasedContent,
): CharSequence { ): CharSequence {

View file

@ -6,56 +6,55 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.protection package io.element.android.features.messages.impl.timeline.protection
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ProtectedViewTest { class ProtectedViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `when hideContent is false, the content is rendered`() { fun `when hideContent is false, the content is rendered`() = runAndroidComposeUiTest {
rule.setProtectedView( setProtectedView(
hideContent = false, hideContent = false,
content = { content = {
Text("Hello") Text("Hello")
} }
) )
rule.onNodeWithText("Hello").assertExists() onNodeWithText("Hello").assertExists()
} }
@Test @Test
fun `when hideContent is true, the content is not rendered, and user can reveal it`() { fun `when hideContent is true, the content is not rendered, and user can reveal it`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setProtectedView( setProtectedView(
hideContent = true, hideContent = true,
onShowClick = it, onShowClick = it,
content = { content = {
Text("Hello") Text("Hello")
} }
) )
rule.onNodeWithText("Hello").assertDoesNotExist() onNodeWithText("Hello").assertDoesNotExist()
rule.clickOn(CommonStrings.action_show) clickOn(CommonStrings.action_show)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setProtectedView( private fun AndroidComposeUiTest<ComponentActivity>.setProtectedView(
hideContent: Boolean = false, hideContent: Boolean = false,
onShowClick: () -> Unit = { lambdaError() }, onShowClick: () -> Unit = { lambdaError() },
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {},

View file

@ -16,6 +16,7 @@ android {
dependencies { dependencies {
api(projects.features.messages.impl) api(projects.features.messages.impl)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.test) implementation(projects.libraries.matrix.test)
implementation(projects.libraries.audio.test) implementation(projects.libraries.audio.test)
implementation(projects.libraries.mediaplayer.test) implementation(projects.libraries.mediaplayer.test)

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.poll.impl.history package io.element.android.features.poll.impl.history
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.poll.api.pollcontent.aPollContentState import io.element.android.features.poll.api.pollcontent.aPollContentState
import io.element.android.features.poll.impl.R import io.element.android.features.poll.impl.R
@ -26,34 +29,29 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PollHistoryViewTest { class PollHistoryViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setPollHistoryViewView( setPollHistoryViewView(
aPollHistoryState( aPollHistoryState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
goBack = it goBack = it
) )
rule.pressBack() pressBack()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on edit poll invokes the expected callback`() { fun `clicking on edit poll invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
val eventId = EventId("\$anEventId") val eventId = EventId("\$anEventId")
val state = aPollHistoryState( val state = aPollHistoryState(
@ -69,17 +67,17 @@ class PollHistoryViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
) )
ensureCalledOnceWithParam(eventId) { ensureCalledOnceWithParam(eventId) {
rule.setPollHistoryViewView( setPollHistoryViewView(
state = state, state = state,
onEditPoll = it onEditPoll = it
) )
rule.clickOn(CommonStrings.action_edit_poll) clickOn(CommonStrings.action_edit_poll)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on poll end emits the expected Event`() { fun `clicking on poll end emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>() val eventsRecorder = EventsRecorder<PollHistoryEvents>()
val eventId = EventId("\$anEventId") val eventId = EventId("\$anEventId")
val state = aPollHistoryState( val state = aPollHistoryState(
@ -95,16 +93,16 @@ class PollHistoryViewTest {
), ),
eventSink = eventsRecorder eventSink = eventsRecorder
) )
rule.setPollHistoryViewView( setPollHistoryViewView(
state = state, state = state,
) )
rule.clickOn(CommonStrings.action_end_poll) clickOn(CommonStrings.action_end_poll)
// Cancel the dialog // Cancel the dialog
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
// Do it again, and confirm the dialog // Do it again, and confirm the dialog
rule.clickOn(CommonStrings.action_end_poll) clickOn(CommonStrings.action_end_poll)
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
PollHistoryEvents.EndPoll(eventId) PollHistoryEvents.EndPoll(eventId)
) )
@ -112,7 +110,7 @@ class PollHistoryViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on poll answer emits the expected Event`() { fun `clicking on poll answer emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>() val eventsRecorder = EventsRecorder<PollHistoryEvents>()
val eventId = EventId("\$anEventId") val eventId = EventId("\$anEventId")
val state = aPollHistoryState( val state = aPollHistoryState(
@ -129,10 +127,10 @@ class PollHistoryViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
) )
val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer
rule.setPollHistoryViewView( setPollHistoryViewView(
state = state, state = state,
) )
rule.onNodeWithText( onNodeWithText(
text = answer.text, text = answer.text,
useUnmergedTree = true, useUnmergedTree = true,
).performClick() ).performClick()
@ -142,14 +140,14 @@ class PollHistoryViewTest {
} }
@Test @Test
fun `clicking on past tab emits the expected Event`() { fun `clicking on past tab emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>() val eventsRecorder = EventsRecorder<PollHistoryEvents>()
rule.setPollHistoryViewView( setPollHistoryViewView(
aPollHistoryState( aPollHistoryState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_polls_history_filter_past) clickOn(R.string.screen_polls_history_filter_past)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST) PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST)
) )
@ -157,22 +155,22 @@ class PollHistoryViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on load more emits the expected Event`() { fun `clicking on load more emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PollHistoryEvents>() val eventsRecorder = EventsRecorder<PollHistoryEvents>()
rule.setPollHistoryViewView( setPollHistoryViewView(
aPollHistoryState( aPollHistoryState(
hasMoreToLoad = true, hasMoreToLoad = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_load_more) clickOn(CommonStrings.action_load_more)
eventsRecorder.assertSingle( eventsRecorder.assertSingle(
PollHistoryEvents.LoadMore PollHistoryEvents.LoadMore
) )
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPollHistoryViewView( private fun AndroidComposeUiTest<ComponentActivity>.setPollHistoryViewView(
state: PollHistoryState, state: PollHistoryState,
onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(), onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(),
goBack: () -> Unit = EnsureNeverCalled(), goBack: () -> Unit = EnsureNeverCalled(),

View file

@ -15,6 +15,7 @@ android {
} }
dependencies { dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
api(projects.features.poll.api) api(projects.features.poll.api)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)

View file

@ -68,6 +68,7 @@ dependencies {
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.libraries.fullscreenintent.api) implementation(projects.libraries.fullscreenintent.api)
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.about package io.element.android.features.preferences.impl.about
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -19,51 +22,47 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AboutViewTest { class AboutViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes back callback`() { fun `clicking on back invokes back callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setAboutView( setAboutView(
anAboutState(), anAboutState(),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on an item invokes the expected callback`() { fun `clicking on an item invokes the expected callback`() = runAndroidComposeUiTest {
val state = anAboutState() val state = anAboutState()
ensureCalledOnceWithParam(state.elementLegals.first()) { callback -> ensureCalledOnceWithParam(state.elementLegals.first()) { callback ->
rule.setAboutView( setAboutView(
state, state,
onElementLegalClick = callback, onElementLegalClick = callback,
) )
rule.clickOn(state.elementLegals.first().titleRes) clickOn(state.elementLegals.first().titleRes)
} }
} }
@Test @Test
fun `clicking on the open source licenses invokes the expected callback`() { fun `clicking on the open source licenses invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setAboutView( setAboutView(
anAboutState(), anAboutState(),
onOpenSourceLicensesClick = callback, onOpenSourceLicensesClick = callback,
) )
rule.clickOn(CommonStrings.common_open_source_licenses) clickOn(CommonStrings.common_open_source_licenses)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAboutView( private fun AndroidComposeUiTest<ComponentActivity>.setAboutView(
state: AboutState, state: AboutState,
onElementLegalClick: (ElementLegal) -> Unit = EnsureNeverCalledWithParam(), onElementLegalClick: (ElementLegal) -> Unit = EnsureNeverCalledWithParam(),
onOpenSourceLicensesClick: () -> Unit = EnsureNeverCalled(), onOpenSourceLicensesClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.advanced package io.element.android.features.preferences.impl.advanced
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
@ -30,104 +33,99 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AdvancedSettingsViewTest { class AdvancedSettingsViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on other theme emits the expected event`() { fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.common_appearance) clickOn(CommonStrings.common_appearance)
rule.clickOn(CommonStrings.common_dark) clickOn(CommonStrings.common_dark)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark))
} }
@Test @Test
fun `black theme is shown when available`() { fun `black theme is shown when available`() = runAndroidComposeUiTest {
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
availableThemeOptions = ThemeOption.entries.toImmutableList(), availableThemeOptions = ThemeOption.entries.toImmutableList(),
), ),
) )
rule.clickOn(CommonStrings.common_appearance) clickOn(CommonStrings.common_appearance)
rule.run { run {
val text = activity.getString(CommonStrings.common_black) val text = activity!!.getString(CommonStrings.common_black)
onNodeWithText(text).assertExists() onNodeWithText(text).assertExists()
} }
} }
@Test @Test
fun `black theme is hidden when unavailable`() { fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest {
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(), availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(),
), ),
) )
rule.clickOn(CommonStrings.common_appearance) clickOn(CommonStrings.common_appearance)
rule.assertNoNodeWithText(CommonStrings.common_black) assertNoNodeWithText(CommonStrings.common_black)
} }
@Test @Test
fun `clicking on View source emits the expected event`() { fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_view_source) clickOn(CommonStrings.action_view_source)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true))
} }
@Test @Test
fun `clicking on Share presence emits the expected event`() { fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_share_presence) clickOn(R.string.screen_advanced_settings_share_presence)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
} }
@Test @Test
fun `clicking on media to enable compression emits the expected event`() { fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
analyticsService = analyticsService analyticsService = analyticsService
) )
rule.clickOn(R.string.screen_advanced_settings_media_compression_description) clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true))
assertThat(analyticsService.capturedEvents).isEqualTo( assertThat(analyticsService.capturedEvents).isEqualTo(
listOf( listOf(
@ -139,17 +137,17 @@ class AdvancedSettingsViewTest {
} }
@Test @Test
fun `clicking on media to disable compression emits the expected event`() { fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true), mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
analyticsService = analyticsService analyticsService = analyticsService
) )
rule.clickOn(R.string.screen_advanced_settings_media_compression_description) clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false))
assertThat(analyticsService.capturedEvents).isEqualTo( assertThat(analyticsService.capturedEvents).isEqualTo(
listOf( listOf(
@ -162,65 +160,65 @@ class AdvancedSettingsViewTest {
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `clicking on hide invite avatars emits the expected event`() { fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
hideInviteAvatars = false hideInviteAvatars = false
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true))
} }
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always hide emits the expected event`() { fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On timelineMediaPreviewValue = MediaPreviewValue.On
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off))
} }
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview private rooms emits the expected event`() { fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On timelineMediaPreviewValue = MediaPreviewValue.On
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private))
} }
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `clicking on timeline media preview always show emits the expected event`() { fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>() val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.Off timelineMediaPreviewValue = MediaPreviewValue.Off
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On))
} }
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `hide invite avatars toggle is disabled when action is loading`() { fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false)
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
hideInviteAvatars = false, hideInviteAvatars = false,
@ -228,14 +226,14 @@ class AdvancedSettingsViewTest {
), ),
) )
// The toggle should be disabled, so clicking should not emit any events // The toggle should be disabled, so clicking should not emit any events
rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title)
} }
@Test @Test
@Config(qualifiers = "h1080dp") @Config(qualifiers = "h1080dp")
fun `timeline media preview options are disabled when action is loading`() { fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>(expectEvents = false)
rule.setAdvancedSettingsView( setAdvancedSettingsView(
state = aAdvancedSettingsState( state = aAdvancedSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
timelineMediaPreviewValue = MediaPreviewValue.On, timelineMediaPreviewValue = MediaPreviewValue.On,
@ -243,12 +241,12 @@ class AdvancedSettingsViewTest {
), ),
) )
// The options should be disabled, so clicking should not emit any events // The options should be disabled, so clicking should not emit any events
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide)
rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView( private fun AndroidComposeUiTest<ComponentActivity>.setAdvancedSettingsView(
state: AdvancedSettingsState, state: AdvancedSettingsState,
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.blockedusers package io.element.android.features.preferences.impl.blockedusers
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -23,72 +26,67 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class BlockedUserViewTest { class BlockedUserViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes back callback`() { fun `clicking on back invokes back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<BlockedUsersEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<BlockedUsersEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setBlockedUsersView( setBlockedUsersView(
aBlockedUsersState( aBlockedUsersState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on a user emits the expected Event`() { fun `clicking on a user emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<BlockedUsersEvents>() val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
val userList = aMatrixUserList() val userList = aMatrixUserList()
rule.setBlockedUsersView( setBlockedUsersView(
aBlockedUsersState( aBlockedUsersState(
blockedUsers = userList, blockedUsers = userList,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick() onNodeWithText(userList.first().displayName.orEmpty()).performClick()
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId)) eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
} }
@Test @Test
fun `clicking on cancel sends a BlockedUsersEvents`() { fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<BlockedUsersEvents>() val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
rule.setBlockedUsersView( setBlockedUsersView(
aBlockedUsersState( aBlockedUsersState(
unblockUserAction = AsyncAction.ConfirmingNoParams, unblockUserAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(BlockedUsersEvents.Cancel) eventsRecorder.assertSingle(BlockedUsersEvents.Cancel)
} }
@Test @Test
fun `clicking on confirm sends a BlockedUsersEvents`() { fun `clicking on confirm sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<BlockedUsersEvents>() val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
rule.setBlockedUsersView( setBlockedUsersView(
aBlockedUsersState( aBlockedUsersState(
unblockUserAction = AsyncAction.ConfirmingNoParams, unblockUserAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_blocked_users_unblock_alert_action) clickOn(R.string.screen_blocked_users_unblock_alert_action)
eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock) eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setBlockedUsersView( private fun AndroidComposeUiTest<ComponentActivity>.setBlockedUsersView(
state: BlockedUsersState, state: BlockedUsersState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -20,76 +23,71 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DeveloperSettingsViewTest { class DeveloperSettingsViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setDeveloperSettingsView( setDeveloperSettingsView(
state = aDeveloperSettingsState( state = aDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Config(qualifiers = "h2000dp") @Config(qualifiers = "h2000dp")
@Test @Test
fun `clicking on push history notification invokes the expected callback`() { fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setDeveloperSettingsView( setDeveloperSettingsView(
state = aDeveloperSettingsState( state = aDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onPushHistoryClick = it onPushHistoryClick = it
) )
rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title)
} }
} }
@Config(qualifiers = "h2000dp") @Config(qualifiers = "h2000dp")
@Test @Test
fun `clicking on open showkase invokes the expected callback`() { fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setDeveloperSettingsView( setDeveloperSettingsView(
state = aDeveloperSettingsState( state = aDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onOpenShowkase = it onOpenShowkase = it
) )
rule.onNodeWithText("Open Showkase browser").performClick() onNodeWithText("Open Showkase browser").performClick()
} }
} }
@Config(qualifiers = "h2200dp") @Config(qualifiers = "h2200dp")
@Test @Test
fun `clicking on clear cache emits the expected event`() { fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>() val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
rule.setDeveloperSettingsView( setDeveloperSettingsView(
state = aDeveloperSettingsState( state = aDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.onNodeWithText("Clear cache").performClick() onNodeWithText("Clear cache").performClick()
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView( private fun AndroidComposeUiTest<ComponentActivity>.setDeveloperSettingsView(
state: DeveloperSettingsState, state: DeveloperSettingsState,
onOpenShowkase: () -> Unit = EnsureNeverCalled(), onOpenShowkase: () -> Unit = EnsureNeverCalled(),
onPushHistoryClick: () -> Unit = EnsureNeverCalled(), onPushHistoryClick: () -> Unit = EnsureNeverCalled(),

View file

@ -5,19 +5,22 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.developer.appsettings package io.element.android.features.preferences.impl.developer.appsettings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isEditable import androidx.compose.ui.test.isEditable
import androidx.compose.ui.test.isFocusable import androidx.compose.ui.test.isFocusable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
@ -27,78 +30,73 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AppDeveloperSettingsPageTest { class AppDeveloperSettingsPageTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setAppDeveloperSettingsView( setAppDeveloperSettingsView(
state = anAppDeveloperSettingsState( state = anAppDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Config(qualifiers = "h1500dp") @Config(qualifiers = "h1500dp")
@Test @Test
fun `clicking on element call url open the dialogs and submit emits the expected event`() { fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>() val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>()
rule.setAppDeveloperSettingsView( setAppDeveloperSettingsView(
state = anAppDeveloperSettingsState( state = anAppDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) clickOn(R.string.screen_advanced_settings_element_call_base_url)
val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) val textInputNode = onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog()))
textInputNode.performTextInput("https://call.element.dev") textInputNode.performTextInput("https://call.element.dev")
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev")) eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev"))
} }
@Config(qualifiers = "h2000dp") @Config(qualifiers = "h2000dp")
@Test @Test
fun `clicking on open showkase invokes the expected callback`() { fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setAppDeveloperSettingsView( setAppDeveloperSettingsView(
state = anAppDeveloperSettingsState( state = anAppDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onOpenShowkase = it onOpenShowkase = it
) )
rule.onNodeWithText("Open Showkase browser").performClick() onNodeWithText("Open Showkase browser").performClick()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on log level emits the expected event`() { fun `clicking on log level emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>() val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>()
rule.setAppDeveloperSettingsView( setAppDeveloperSettingsView(
state = anAppDeveloperSettingsState( state = anAppDeveloperSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.onNodeWithText("Tracing log level").performClick() onNodeWithText("Tracing log level").performClick()
rule.onNodeWithText("Debug").performClick() onNodeWithText("Debug").performClick()
eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAppDeveloperSettingsView( private fun AndroidComposeUiTest<ComponentActivity>.setAppDeveloperSettingsView(
state: AppDeveloperSettingsState, state: AppDeveloperSettingsState,
onOpenShowkase: () -> Unit = EnsureNeverCalled(), onOpenShowkase: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.notifications package io.element.android.features.preferences.impl.notifications
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -25,76 +28,71 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class NotificationSettingsViewTest { class NotificationSettingsViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnce { ensureCalledOnce {
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on troubleshoot notification invokes the expected callback`() { fun `clicking on troubleshoot notification invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnce { ensureCalledOnce {
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onTroubleshootNotificationsClick = it onTroubleshootNotificationsClick = it
) )
rule.clickOn(R.string.troubleshoot_notifications_entry_point_title) clickOn(R.string.troubleshoot_notifications_entry_point_title)
} }
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on group chats invokes the expected callback`() { fun `clicking on group chats invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnceWithParam(false) { ensureCalledOnceWithParam(false) {
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onOpenEditDefault = it onOpenEditDefault = it
) )
rule.clickOn(R.string.screen_notification_settings_group_chats) clickOn(R.string.screen_notification_settings_group_chats)
} }
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on direct chats invokes the expected callback`() { fun `clicking on direct chats invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnceWithParam(true) { ensureCalledOnceWithParam(true) {
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onOpenEditDefault = it onOpenEditDefault = it
) )
rule.clickOn(R.string.screen_notification_settings_direct_chats) clickOn(R.string.screen_notification_settings_direct_chats)
} }
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
} }
@ -111,15 +109,15 @@ class NotificationSettingsViewTest {
testNotificationToggle(false) testNotificationToggle(false)
} }
private fun testNotificationToggle(initialState: Boolean) { private fun testNotificationToggle(initialState: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
appNotificationEnabled = initialState, appNotificationEnabled = initialState,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_notification_settings_enable_notifications) clickOn(R.string.screen_notification_settings_enable_notifications)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -140,15 +138,15 @@ class NotificationSettingsViewTest {
testAtRoomToggle(false) testAtRoomToggle(false)
} }
private fun testAtRoomToggle(initialState: Boolean) { private fun testAtRoomToggle(initialState: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
atRoomNotificationsEnabled = initialState, atRoomNotificationsEnabled = initialState,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_notification_settings_room_mention_label) clickOn(R.string.screen_notification_settings_room_mention_label)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -169,15 +167,15 @@ class NotificationSettingsViewTest {
testInvitationToggle(false) testInvitationToggle(false)
} }
private fun testInvitationToggle(initialState: Boolean) { private fun testInvitationToggle(initialState: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
inviteForMeNotificationsEnabled = initialState, inviteForMeNotificationsEnabled = initialState,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_notification_settings_invite_for_me_label) clickOn(R.string.screen_notification_settings_invite_for_me_label)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -188,15 +186,15 @@ class NotificationSettingsViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `with an error configuration, clicking on continue emits the expected events`() { fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -207,15 +205,15 @@ class NotificationSettingsViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `with invalid configuration, clicking on continue emits the expected events`() { fun `with invalid configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aInvalidNotificationSettingsState( state = aInvalidNotificationSettingsState(
fixFailed = false, fixFailed = false,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -226,15 +224,15 @@ class NotificationSettingsViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `with invalid configuration and error, clicking on OK emits the expected events`() { fun `with invalid configuration and error, clicking on OK emits the expected events`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aInvalidNotificationSettingsState( state = aInvalidNotificationSettingsState(
fixFailed = true, fixFailed = true,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -245,14 +243,14 @@ class NotificationSettingsViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on Push notification provider emits the expected event`() { fun `clicking on Push notification provider emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_advanced_settings_push_provider_android) clickOn(R.string.screen_advanced_settings_push_provider_android)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -262,16 +260,16 @@ class NotificationSettingsViewTest {
} }
@Test @Test
fun `clicking on a push provider emits the expected event`() { fun `clicking on a push provider emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>() val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView( setNotificationSettingsView(
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
showChangePushProviderDialog = true, showChangePushProviderDialog = true,
availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2")) availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2"))
), ),
) )
rule.onNodeWithText("P2").performClick() onNodeWithText("P2").performClick()
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled, NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
@ -281,7 +279,7 @@ class NotificationSettingsViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotificationSettingsView( private fun AndroidComposeUiTest<ComponentActivity>.setNotificationSettingsView(
state: NotificationSettingsState, state: NotificationSettingsState,
onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(), onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(),
onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(), onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(),

View file

@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ -25,49 +28,45 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PreferencesRootViewTest { class PreferencesRootViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes back callback`() { fun `clicking on back invokes back callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `click on User profile invokes the expected callback`() { fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
val user = aMatrixUser() val user = aMatrixUser()
ensureCalledOnceWithParam(user) { callback -> ensureCalledOnceWithParam(user) { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
myUser = user, myUser = user,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenUserProfile = callback, onOpenUserProfile = callback,
) )
rule.onNodeWithText("Alice").performClick() onNodeWithText("Alice").performClick()
} }
} }
@Test @Test
fun `clicking on other session sends a SwitchToSession`() { fun `clicking on other session sends a SwitchToSession`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>() val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
isMultiAccountEnabled = true, isMultiAccountEnabled = true,
otherSessions = listOf( otherSessions = listOf(
@ -79,366 +78,366 @@ class PreferencesRootViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText("Bob").performClick() onNodeWithText("Bob").performClick()
eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2)) eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2))
} }
@Test @Test
fun `click on Add account invokes the expected callback`() { fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
isMultiAccountEnabled = true, isMultiAccountEnabled = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onAddAccountClick = callback, onAddAccountClick = callback,
) )
rule.clickOn(CommonStrings.common_add_another_account) clickOn(CommonStrings.common_add_another_account)
} }
} }
@Test @Test
fun `when multi account is not enabled, item is not shown`() { fun `when multi account is not enabled, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
isMultiAccountEnabled = false, isMultiAccountEnabled = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_add_another_account)).assertDoesNotExist()
} }
@Test @Test
fun `click on Encryption invokes the expected callback`() { fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showSecureBackup = true, showSecureBackup = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onSecureBackupClick = callback, onSecureBackupClick = callback,
) )
rule.clickOn(CommonStrings.common_encryption) clickOn(CommonStrings.common_encryption)
} }
} }
@Test @Test
fun `when showSecureBackup is false, item is not shown`() { fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showSecureBackup = false, showSecureBackup = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_encryption)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist()
} }
@Test @Test
fun `click on Manage account invokes the expected callback`() { fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnceWithParam("aUrl") { callback -> ensureCalledOnceWithParam("aUrl") { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
accountManagementUrl = "aUrl", accountManagementUrl = "aUrl",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onManageAccountClick = callback, onManageAccountClick = callback,
) )
rule.clickOn(CommonStrings.action_manage_account_and_devices) clickOn(CommonStrings.action_manage_account_and_devices)
} }
} }
@Test @Test
fun `when accountManagementUrl is null, item is not shown`() { fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
accountManagementUrl = null, accountManagementUrl = null,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
} }
@Test @Test
fun `click on Link new devices invokes the expected callback`() { fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLinkNewDevice = true, showLinkNewDevice = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onLinkNewDeviceClick = callback, onLinkNewDeviceClick = callback,
) )
rule.clickOn(CommonStrings.common_link_new_device) clickOn(CommonStrings.common_link_new_device)
} }
} }
@Test @Test
fun `when showLinkNewDevice is false, item is not shown`() { fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLinkNewDevice = false, showLinkNewDevice = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
} }
@Test @Test
fun `click on Analytics invokes the expected callback`() { fun `click on Analytics invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showAnalyticsSettings = true, showAnalyticsSettings = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenAnalytics = callback, onOpenAnalytics = callback,
) )
rule.clickOn(CommonStrings.common_analytics) clickOn(CommonStrings.common_analytics)
} }
} }
@Test @Test
fun `when showAnalyticsSettings is false, item is not shown`() { fun `when showAnalyticsSettings is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showAnalyticsSettings = false, showAnalyticsSettings = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_analytics)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_analytics)).assertDoesNotExist()
} }
@Test @Test
fun `click on Report a problem invokes the expected callback`() { fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
canReportBug = true, canReportBug = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenRageShake = callback, onOpenRageShake = callback,
) )
rule.clickOn(CommonStrings.common_report_a_problem) clickOn(CommonStrings.common_report_a_problem)
} }
} }
@Test @Test
fun `when canReportBug is false, item is not shown`() { fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
canReportBug = false, canReportBug = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
} }
@Test @Test
fun `click on Screen lock invokes the expected callback`() { fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenLockScreenSettings = callback, onOpenLockScreenSettings = callback,
) )
rule.clickOn(CommonStrings.common_screen_lock) clickOn(CommonStrings.common_screen_lock)
} }
} }
@Test @Test
fun `click on About invokes the expected callback`() { fun `click on About invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenAbout = callback, onOpenAbout = callback,
) )
rule.clickOn(CommonStrings.common_about) clickOn(CommonStrings.common_about)
} }
} }
@Test @Test
fun `click on Developer settings invokes the expected callback`() { fun `click on Developer settings invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showDeveloperSettings = true, showDeveloperSettings = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenDeveloperSettings = callback, onOpenDeveloperSettings = callback,
) )
rule.clickOn(CommonStrings.common_developer_options) clickOn(CommonStrings.common_developer_options)
} }
} }
@Test @Test
fun `when showDeveloperSettings is false, item is not shown`() { fun `when showDeveloperSettings is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showDeveloperSettings = false, showDeveloperSettings = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_developer_options)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_developer_options)).assertDoesNotExist()
} }
@Test @Test
fun `click on Advanced settings invokes the expected callback`() { fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenAdvancedSettings = callback, onOpenAdvancedSettings = callback,
) )
rule.clickOn(CommonStrings.common_advanced_settings) clickOn(CommonStrings.common_advanced_settings)
} }
} }
@Test @Test
fun `click on Labs invokes the expected callback`() { fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLabsItem = true, showLabsItem = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenLabs = callback, onOpenLabs = callback,
) )
rule.clickOn(R.string.screen_labs_title) clickOn(R.string.screen_labs_title)
} }
} }
@Test @Test
fun `when showLabsItem is false, item is not shown`() { fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
showLabsItem = false, showLabsItem = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(R.string.screen_labs_title)).assertDoesNotExist() onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist()
} }
@Test @Test
fun `click on Notification invokes the expected callback`() { fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenNotificationSettings = callback, onOpenNotificationSettings = callback,
) )
rule.clickOn(R.string.screen_notification_settings_title) clickOn(R.string.screen_notification_settings_title)
} }
} }
@Test @Test
fun `click on Blocked users invokes the expected callback`() { fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
nbOfBlockedUsers = 1, nbOfBlockedUsers = 1,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenBlockedUsers = callback, onOpenBlockedUsers = callback,
) )
rule.clickOn(CommonStrings.common_blocked_users) clickOn(CommonStrings.common_blocked_users)
} }
} }
@Test @Test
fun `when nbOfBlockedUsers is 0, item is not shown`() { fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
nbOfBlockedUsers = 0, nbOfBlockedUsers = 0,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
} }
@Test @Test
fun `click on Remove this device invokes the expected callback`() { fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onSignOutClick = callback, onSignOutClick = callback,
) )
rule.clickOn(CommonStrings.action_signout) clickOn(CommonStrings.action_signout)
} }
} }
@Test @Test
fun `click on Deactivate invokes the expected callback`() { fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
canDeactivateAccount = true, canDeactivateAccount = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onDeactivateClick = callback, onDeactivateClick = callback,
) )
rule.clickOn(CommonStrings.action_delete_account) clickOn(CommonStrings.action_delete_account)
} }
} }
@Test @Test
fun `when canDeactivateAccount is false, item is not shown`() { fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
canDeactivateAccount = false, canDeactivateAccount = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete_account)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist()
} }
@Test @Test
fun `clicking on version sends a PreferencesRootEvents`() { fun `clicking on version sends a PreferencesRootEvents`() = runAndroidComposeUiTest {
val version = "VERSION" val version = "VERSION"
val eventsRecorder = EventsRecorder<PreferencesRootEvent>() val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
rule.setView( setView(
aPreferencesRootState( aPreferencesRootState(
version = version, version = version,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(version).performClick() onNodeWithText(version).performClick()
eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick) eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: PreferencesRootState, state: PreferencesRootState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onAddAccountClick: () -> Unit = EnsureNeverCalled(), onAddAccountClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
@ -23,96 +26,93 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class EditUserProfileViewTest { class EditUserProfileViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back emits the expected event`() { fun `clicking on back emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBack() pressBack()
eventsRecorder.assertSingle(EditUserProfileEvent.Exit) eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
} }
@Test @Test
fun `clicking on save from the exit confirmation dialog emits the expected event`() { fun `clicking on save from the exit confirmation dialog emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save, inDialog = true) clickOn(CommonStrings.action_save, inDialog = true)
eventsRecorder.assertSingle(EditUserProfileEvent.Save) eventsRecorder.assertSingle(EditUserProfileEvent.Save)
} }
@Test @Test
fun `clicking on discard exit emits the expected event`() { fun `clicking on discard exit emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_discard) clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(EditUserProfileEvent.Exit) eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
} }
@Test @Test
fun `clicking on save emits the expected event`() { fun `clicking on save emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveButtonEnabled = true, saveButtonEnabled = true,
saveAction = AsyncAction.Uninitialized, saveAction = AsyncAction.Uninitialized,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(EditUserProfileEvent.Save) eventsRecorder.assertSingle(EditUserProfileEvent.Save)
} }
@Test @Test
fun `clicking on avatar opens the bottom sheet dialog`() { fun `clicking on avatar opens the bottom sheet dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
val actions = listOf( val actions = listOf(
AvatarAction.TakePhoto, AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
AvatarAction.Remove, AvatarAction.Remove,
) )
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.Uninitialized, saveAction = AsyncAction.Uninitialized,
avatarActions = actions, avatarActions = actions,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
val contentDescription = rule.activity.getString(CommonStrings.a11y_avatar) val resources = activity!!.resources
rule.onNodeWithContentDescription(contentDescription).performClick() val contentDescription = resources.getString(CommonStrings.a11y_avatar)
onNodeWithContentDescription(contentDescription).performClick()
// Assert that the actions are displayed // Assert that the actions are displayed
actions.forEach { action -> actions.forEach { action ->
val text = rule.activity.getString(action.titleResId) val text = resources.getString(action.titleResId)
rule.onNodeWithText(text).assertExists() onNodeWithText(text).assertExists()
} }
} }
@Test @Test
fun `success invokes the expected callback`() { fun `success invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<EditUserProfileEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<EditUserProfileEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setEditUserProfileView( setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.Success(Unit), saveAction = AsyncAction.Success(Unit),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -123,7 +123,7 @@ class EditUserProfileViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setEditUserProfileView( private fun AndroidComposeUiTest<ComponentActivity>.setEditUserProfileView(
state: EditUserProfileState, state: EditUserProfileState,
onEditProfileSuccess: () -> Unit = EnsureNeverCalled(), onEditProfileSuccess: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -14,6 +14,7 @@ android {
} }
dependencies { dependencies {
implementation(projects.libraries.architecture)
implementation(projects.features.preferences.api) implementation(projects.features.preferences.api)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
} }

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.reportroom.impl package io.element.android.features.reportroom.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -20,76 +23,72 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReportRoomViewTest { class ReportRoomViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invoke the expected callback`() { fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setReportRoomView( setReportRoomView(
aReportRoomState( aReportRoomState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on report when enabled emits the expected event`() { fun `clicking on report when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReportRoomEvents>() val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView( setReportRoomView(
aReportRoomState( aReportRoomState(
reason = "Spam", reason = "Spam",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_report) clickOn(CommonStrings.action_report)
eventsRecorder.assertSingle(ReportRoomEvents.Report) eventsRecorder.assertSingle(ReportRoomEvents.Report)
} }
@Test @Test
fun `clicking on decline when disabled does not emit event`() { fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false)
rule.setReportRoomView( setReportRoomView(
aReportRoomState(eventSink = eventsRecorder), aReportRoomState(eventSink = eventsRecorder),
) )
rule.clickOn(CommonStrings.action_report) clickOn(CommonStrings.action_report)
} }
@Test @Test
fun `clicking on leave room option emits the expected event`() { fun `clicking on leave room option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReportRoomEvents>() val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView( setReportRoomView(
aReportRoomState(eventSink = eventsRecorder), aReportRoomState(eventSink = eventsRecorder),
) )
rule.clickOn(CommonStrings.action_leave_room) clickOn(CommonStrings.action_leave_room)
eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom)
} }
@Test @Test
fun `typing text in the reason field emits the expected Event`() { fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReportRoomEvents>() val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView( setReportRoomView(
aReportRoomState( aReportRoomState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
reason = "" reason = ""
), ),
) )
rule.onNodeWithText("").performTextInput("Spam!") onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!"))
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setReportRoomView( private fun AndroidComposeUiTest<ComponentActivity>.setReportRoomView(
state: ReportRoomState, state: ReportRoomState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.rolesandpermissions.impl.permissions package io.element.android.features.rolesandpermissions.impl.permissions
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.rolesandpermissions.impl.R import io.element.android.features.rolesandpermissions.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -23,84 +26,80 @@ import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ChangeRoomPermissionsViewTest { class ChangeRoomPermissionsViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `click on back icon invokes Exit`() { fun `click on back icon invokes Exit`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
eventSink = recorder eventSink = recorder
) )
) )
rule.pressBack() pressBack()
recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
} }
@Test @Test
fun `click on back key invokes Exit`() { fun `click on back key invokes Exit`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
eventSink = recorder eventSink = recorder
) )
) )
rule.pressBackKey() pressBackKey()
recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
} }
@Test @Test
fun `when confirming exit with pending changes, using the back key actually exits`() { fun `when confirming exit with pending changes, using the back key actually exits`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.pressBackKey() pressBackKey()
recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
} }
@Test @Test
fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_discard) clickOn(CommonStrings.action_discard)
recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit)
} }
@Test @Test
fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() { fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_save, inDialog = true) clickOn(CommonStrings.action_save, inDialog = true)
recorder.assertSingle(ChangeRoomPermissionsEvent.Save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
} }
@Test @Test
fun `click on a role item triggers ChangeRole event`() { fun `click on a role item triggers ChangeRole event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
itemsBySection = persistentMapOf( itemsBySection = persistentMapOf(
// Makes sure there is only one item to click on // Makes sure there is only one item to click on
@ -109,70 +108,70 @@ class ChangeRoomPermissionsViewTest {
eventSink = recorder, eventSink = recorder,
) )
) )
rule.clickOn(R.string.screen_room_change_permissions_room_name) clickOn(R.string.screen_room_change_permissions_room_name)
rule.clickOn(R.string.screen_room_change_permissions_everyone) clickOn(R.string.screen_room_change_permissions_everyone)
recorder.assertSingle( recorder.assertSingle(
ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Everyone), ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Everyone),
) )
} }
@Test @Test
fun `click on the Save menu item triggers Save event`() { fun `click on the Save menu item triggers Save event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
recorder.assertSingle(ChangeRoomPermissionsEvent.Save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
} }
@Test @Test
fun `a successful save exits the screen`() { fun `a successful save exits the screen`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(true) { callback -> ensureCalledOnceWithParam(true) { callback ->
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.Success(true), saveAction = AsyncAction.Success(true),
), ),
onComplete = callback, onComplete = callback,
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
} }
} }
@Test @Test
fun `a cancellation exits the screen`() { fun `a cancellation exits the screen`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(false) { callback -> ensureCalledOnceWithParam(false) { callback ->
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.Success(false), saveAction = AsyncAction.Success(false),
), ),
onComplete = callback, onComplete = callback,
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
} }
} }
@Test @Test
fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ChangeRoomPermissionsEvent>() val recorder = EventsRecorder<ChangeRoomPermissionsEvent>()
rule.setChangeRoomPermissionsRule( setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")),
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRoomPermissionsRule( private fun AndroidComposeUiTest<ComponentActivity>.setChangeRoomPermissionsRule(
state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(), state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(),
onComplete: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onComplete: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
) { ) {

View file

@ -6,15 +6,18 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.rolesandpermissions.impl.roles package io.element.android.features.rolesandpermissions.impl.roles
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -30,20 +33,16 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ChangeRolesViewTest { class ChangeRolesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `passing a 'User' role throws an exception`() { fun `passing a 'User' role throws an exception`() = runAndroidComposeUiTest {
val exception = runCatchingExceptions { val exception = runCatchingExceptions {
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
role = RoomMember.Role.User, role = RoomMember.Role.User,
eventSink = EnsureNeverCalledWithParam(), eventSink = EnsureNeverCalledWithParam(),
@ -54,106 +53,106 @@ class ChangeRolesViewTest {
} }
@Test @Test
fun `back key - with search active toggles the search`() { fun `back key - with search active toggles the search`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = true, isSearchActive = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBackKey() pressBackKey()
// Advance time to let the event be processed, as the search toggle might have some delay (e.g. for the animation) // Advance time to let the event be processed, as the search toggle might have some delay (e.g. for the animation)
rule.mainClock.advanceTimeBy(1) mainClock.advanceTimeBy(1)
eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive) eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive)
} }
@Test @Test
fun `back key - with search inactive exits the screen`() { fun `back key - with search inactive exits the screen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = false, isSearchActive = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(ChangeRolesEvent.Exit) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
fun `back button - exits the screen`() { fun `back button - exits the screen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = false, isSearchActive = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBack() pressBack()
eventsRecorder.assertSingle(ChangeRolesEvent.Exit) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
fun `save button - with changes, it saves them`() { fun `save button - with changes, it saves them`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
hasPendingChanges = true, hasPendingChanges = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(ChangeRolesEvent.Save) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test
fun `save button - with no changes, does nothing`() { fun `save button - with no changes, does nothing`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
hasPendingChanges = false, hasPendingChanges = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
@Test @Test
fun `exit confirmation dialog - discard exits the screen`() { fun `exit confirmation dialog - discard exits the screen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = true, isSearchActive = true,
savingState = AsyncAction.ConfirmingCancellation, savingState = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_discard) clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(ChangeRolesEvent.Exit) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
fun `exit confirmation dialog - save emits the save event`() { fun `exit confirmation dialog - save emits the save event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = true, isSearchActive = true,
savingState = AsyncAction.ConfirmingCancellation, savingState = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(ChangeRolesEvent.Save) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test
fun `save admins confirmation dialog - submit saves the changes`() { fun `save admins confirmation dialog - submit saves the changes`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
role = RoomMember.Role.Admin, role = RoomMember.Role.Admin,
isSearchActive = true, isSearchActive = true,
@ -161,14 +160,14 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ChangeRolesEvent.Save) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test
fun `save owners confirmation dialog - continue saves the changes`() { fun `save owners confirmation dialog - continue saves the changes`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
role = RoomMember.Role.Owner(isCreator = false), role = RoomMember.Role.Owner(isCreator = false),
isSearchActive = true, isSearchActive = true,
@ -176,14 +175,14 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ChangeRolesEvent.Save) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test
fun `save admins confirmation dialog - cancel removes the dialog`() { fun `save admins confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
role = RoomMember.Role.Admin, role = RoomMember.Role.Admin,
isSearchActive = true, isSearchActive = true,
@ -191,14 +190,14 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
} }
@Test @Test
fun `save owners confirmation dialog - cancel removes the dialog`() { fun `save owners confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
role = RoomMember.Role.Owner(isCreator = false), role = RoomMember.Role.Owner(isCreator = false),
isSearchActive = true, isSearchActive = true,
@ -206,39 +205,39 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
} }
@Test @Test
fun `error dialog - dismissing removes the dialog`() { fun `error dialog - dismissing removes the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
isSearchActive = true, isSearchActive = true,
savingState = AsyncAction.Failure(IllegalStateException("boom")), savingState = AsyncAction.Failure(IllegalStateException("boom")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
} }
@Test @Test
fun `testing removing user from selected list emits the expected event`() { fun `testing removing user from selected list emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
val selectedUsers = aMatrixUserList().take(2) val selectedUsers = aMatrixUserList().take(2)
val userToDeselect = selectedUsers[1] val userToDeselect = selectedUsers[1]
assertThat(userToDeselect.displayName).isEqualTo("Bob") assertThat(userToDeselect.displayName).isEqualTo("Bob")
rule.setChangeRolesContent( setChangeRolesContent(
state = aChangeRolesStateWithSelectedUsers().copy( state = aChangeRolesStateWithSelectedUsers().copy(
selectedUsers = selectedUsers.toImmutableList(), selectedUsers = selectedUsers.toImmutableList(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
// Unselect the user from the row list // Unselect the user from the row list
val contentDescription = rule.activity.getString(CommonStrings.action_remove) val contentDescription = activity!!.getString(CommonStrings.action_remove)
rule.onNodeWithContentDescription( onNodeWithContentDescription(
label = contentDescription, label = contentDescription,
useUnmergedTree = true, useUnmergedTree = true,
).performClick() ).performClick()
@ -247,7 +246,7 @@ class ChangeRolesViewTest {
@Test @Test
@Config(qualifiers = "h1000dp") @Config(qualifiers = "h1000dp")
fun `testing adding user to the selected list emits the expected event`() { fun `testing adding user to the selected list emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
val selectedUsers = aMatrixUserList().take(2) val selectedUsers = aMatrixUserList().take(2)
val state = aChangeRolesStateWithSelectedUsers().copy( val state = aChangeRolesStateWithSelectedUsers().copy(
@ -256,16 +255,16 @@ class ChangeRolesViewTest {
) )
val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser() val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser()
assertThat(userToSelect.displayName).isEqualTo("Carol") assertThat(userToSelect.displayName).isEqualTo("Carol")
rule.setChangeRolesContent( setChangeRolesContent(
state = state, state = state,
) )
// Select the user from the user list // Select the user from the user list
rule.onNodeWithText("Carol").performClick() onNodeWithText("Carol").performClick()
eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect))
} }
@Test @Test
fun `testing removing user to the selected list emits the expected event`() { fun `testing removing user to the selected list emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
val selectedUsers = aMatrixUserList().take(2) val selectedUsers = aMatrixUserList().take(2)
val state = aChangeRolesStateWithSelectedUsers().copy( val state = aChangeRolesStateWithSelectedUsers().copy(
@ -274,18 +273,18 @@ class ChangeRolesViewTest {
) )
val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser() val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser()
assertThat(userToSelect.displayName).isEqualTo("Bob") assertThat(userToSelect.displayName).isEqualTo("Bob")
rule.setChangeRolesContent( setChangeRolesContent(
state = state, state = state,
) )
// Unselect the user from the user list // Unselect the user from the user list
rule.onAllNodesWithText( onAllNodesWithText(
text = "Bob", text = "Bob",
useUnmergedTree = true, useUnmergedTree = true,
)[1].performClick() )[1].performClick()
eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect))
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRolesContent( private fun AndroidComposeUiTest<ComponentActivity>.setChangeRolesContent(
state: ChangeRolesState, state: ChangeRolesState,
) { ) {
setContent { setContent {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.rolesandpermissions.impl.root package io.element.android.features.rolesandpermissions.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.rolesandpermissions.impl.R import io.element.android.features.rolesandpermissions.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -23,159 +26,154 @@ import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledTimes import io.element.android.tests.testutils.ensureCalledTimes
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RolesAndPermissionsViewTest { class RolesAndPermissionsViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `click on back invokes expected callback`() { fun `click on back invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
goBack = callback, goBack = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `tapping on Admins opens admin list`() { fun `tapping on Admins opens admin list`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
aRolesAndPermissionsState( aRolesAndPermissionsState(
roomSupportsOwners = false, roomSupportsOwners = false,
eventSink = EventsRecorder(expectEvents = false) eventSink = EventsRecorder(expectEvents = false)
), ),
openAdminList = callback, openAdminList = callback,
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_admins) clickOn(R.string.screen_room_roles_and_permissions_admins)
} }
} }
@Test @Test
fun `tapping on Admins and Owners opens admin list`() { fun `tapping on Admins and Owners opens admin list`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
aRolesAndPermissionsState( aRolesAndPermissionsState(
roomSupportsOwners = true, roomSupportsOwners = true,
eventSink = EventsRecorder(expectEvents = false) eventSink = EventsRecorder(expectEvents = false)
), ),
openAdminList = callback, openAdminList = callback,
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners)
} }
} }
@Test @Test
fun `tapping on Moderators opens moderators list`() { fun `tapping on Moderators opens moderators list`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
openModeratorList = callback, openModeratorList = callback,
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_moderators) clickOn(R.string.screen_room_roles_and_permissions_moderators)
} }
} }
@Test @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `tapping permission item open the change permissions screen`() { fun `tapping permission item open the change permissions screen`() = runAndroidComposeUiTest {
ensureCalledTimes(1) { callback -> ensureCalledTimes(1) { callback ->
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
openEditPermissions = callback, openEditPermissions = callback,
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_permissions_header) clickOn(R.string.screen_room_roles_and_permissions_permissions_header)
} }
} }
@Test @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `tapping on reset permissions triggers ResetPermissions event`() { fun `tapping on reset permissions triggers ResetPermissions event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_reset) clickOn(R.string.screen_room_roles_and_permissions_reset)
recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions)
} }
@Test @Test
fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() { fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
resetPermissionsAction = AsyncAction.ConfirmingNoParams, resetPermissionsAction = AsyncAction.ConfirmingNoParams,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_reset) clickOn(CommonStrings.action_reset)
recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions)
} }
@Test @Test
fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() { fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
resetPermissionsAction = AsyncAction.ConfirmingNoParams, resetPermissionsAction = AsyncAction.ConfirmingNoParams,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction)
} }
@Test @Test
fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() { fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
changeOwnRoleAction = AsyncAction.ConfirmingNoParams, changeOwnRoleAction = AsyncAction.ConfirmingNoParams,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator)
rule.mainClock.advanceTimeBy(1_000L) mainClock.advanceTimeBy(1_000L)
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator))
} }
@Test @Test
fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runTest { fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
changeOwnRoleAction = AsyncAction.ConfirmingNoParams, changeOwnRoleAction = AsyncAction.ConfirmingNoParams,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member)
rule.mainClock.advanceTimeBy(1_000L) mainClock.advanceTimeBy(1_000L)
recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User)) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User))
} }
@Test @Test
fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() { fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<RolesAndPermissionsEvents>() val recorder = EventsRecorder<RolesAndPermissionsEvents>()
rule.setRolesAndPermissionsView( setRolesAndPermissionsView(
state = aRolesAndPermissionsState( state = aRolesAndPermissionsState(
changeOwnRoleAction = AsyncAction.ConfirmingNoParams, changeOwnRoleAction = AsyncAction.ConfirmingNoParams,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
rule.mainClock.advanceTimeBy(1_000L) mainClock.advanceTimeBy(1_000L)
recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRolesAndPermissionsView( private fun AndroidComposeUiTest<ComponentActivity>.setRolesAndPermissionsView(
state: RolesAndPermissionsState = aRolesAndPermissionsState( state: RolesAndPermissionsState = aRolesAndPermissionsState(
roomSupportsOwners = false, roomSupportsOwners = false,
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roomaliasresolver.impl package io.element.android.features.roomaliasresolver.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
@ -22,48 +25,44 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomAliasHelperViewTest { class RoomAliasHelperViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setRoomAliasResolverView( setRoomAliasResolverView(
aRoomAliasResolverState( aRoomAliasResolverState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it onBackClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on Retry emits the expected Event`() { fun `clicking on Retry emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>() val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>()
rule.setRoomAliasResolverView( setRoomAliasResolverView(
aRoomAliasResolverState( aRoomAliasResolverState(
resolveState = AsyncData.Failure(Exception("Error")), resolveState = AsyncData.Failure(Exception("Error")),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry) eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry)
} }
@Test @Test
fun `success state invokes the expected Callback`() { fun `success state invokes the expected Callback`() = runAndroidComposeUiTest {
val result = aResolvedRoomAlias() val result = aResolvedRoomAlias()
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
ensureCalledOnceWithParam(result) { ensureCalledOnceWithParam(result) {
rule.setRoomAliasResolverView( setRoomAliasResolverView(
aRoomAliasResolverState( aRoomAliasResolverState(
resolveState = AsyncData.Success(result), resolveState = AsyncData.Success(result),
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -74,7 +73,7 @@ class RoomAliasHelperViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomAliasResolverView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomAliasResolverView(
state: RoomAliasResolverState, state: RoomAliasResolverState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(), onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.features.userprofile.shared.aUserProfileState
@ -32,98 +35,94 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomDetailsViewTest { class RoomDetailsViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `click on back invokes expected callback`() { fun `click on back invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
goBack = callback, goBack = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `click on share invokes expected callback`() { fun `click on share invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
onShareRoom = callback, onShareRoom = callback,
) )
rule.clickOn(CommonStrings.action_share) clickOn(CommonStrings.action_share)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on room members invokes expected callback`() { fun `click on room members invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
openRoomMemberList = callback, openRoomMemberList = callback,
) )
rule.clickOn(CommonStrings.common_people) clickOn(CommonStrings.common_people)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on polls invokes expected callback`() { fun `click on polls invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
openPollHistory = callback, openPollHistory = callback,
) )
rule.clickOn(R.string.screen_polls_history_title) clickOn(R.string.screen_polls_history_title)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on media gallery invokes expected callback`() { fun `click on media gallery invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
openMediaGallery = callback, openMediaGallery = callback,
) )
rule.clickOn(R.string.screen_room_details_media_gallery_title) clickOn(R.string.screen_room_details_media_gallery_title)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on notification invokes expected callback`() { fun `click on notification invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
openRoomNotificationSettings = callback, openRoomNotificationSettings = callback,
) )
rule.clickOn(R.string.screen_room_details_notification_title) clickOn(R.string.screen_room_details_notification_title)
} }
} }
@Test @Test
fun `click on invite invokes expected callback`() { fun `click on invite invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canInvite = true, canInvite = true,
), ),
invitePeople = callback, invitePeople = callback,
) )
rule.clickOn(CommonStrings.action_invite) clickOn(CommonStrings.action_invite)
} }
} }
@Test @Test
fun `click on call invokes expected callback`() { fun `click on call invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(CallIntent.AUDIO) { callback -> ensureCalledOnceWithParam(CallIntent.AUDIO) { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canInvite = true, canInvite = true,
@ -134,103 +133,103 @@ class RoomDetailsViewTest {
), ),
onJoinCallClick = callback, onJoinCallClick = callback,
) )
rule.clickOn(CommonStrings.action_call) clickOn(CommonStrings.action_call)
} }
} }
@Test @Test
fun `click on video call invokes expected callback`() { fun `click on video call invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(CallIntent.VIDEO) { callback -> ensureCalledOnceWithParam(CallIntent.VIDEO) { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canInvite = true, canInvite = true,
), ),
onJoinCallClick = callback, onJoinCallClick = callback,
) )
rule.clickOn(CommonStrings.common_video) clickOn(CommonStrings.common_video)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on pinned messages invokes expected callback`() { fun `click on pinned messages invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canInvite = true, canInvite = true,
), ),
onPinnedMessagesClick = callback, onPinnedMessagesClick = callback,
) )
rule.clickOn(R.string.screen_room_details_pinned_events_row_title) clickOn(R.string.screen_room_details_pinned_events_row_title)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on security and privacy invokes expected callback`() { fun `click on security and privacy invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canShowSecurityAndPrivacy = true, canShowSecurityAndPrivacy = true,
), ),
onSecurityAndPrivacyClick = callback, onSecurityAndPrivacyClick = callback,
) )
rule.clickOn(R.string.screen_room_details_security_and_privacy_title) clickOn(R.string.screen_room_details_security_and_privacy_title)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on add topic emit expected event`() { fun `click on add topic emit expected event`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.AddTopic) { callback -> ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.AddTopic) { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
roomTopic = RoomTopicState.CanAddTopic, roomTopic = RoomTopicState.CanAddTopic,
), ),
onActionClick = callback, onActionClick = callback,
) )
rule.clickOn(R.string.screen_room_details_add_topic_title) clickOn(R.string.screen_room_details_add_topic_title)
} }
} }
@Test @Test
fun `click on menu edit emit expected event`() { fun `click on menu edit emit expected event`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.Edit) { callback -> ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.Edit) { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canEdit = true, canEdit = true,
), ),
onActionClick = callback, onActionClick = callback,
) )
val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu)
rule.onNodeWithContentDescription(menuContentDescription).performClick() onNodeWithContentDescription(menuContentDescription).performClick()
rule.clickOn(CommonStrings.action_edit) clickOn(CommonStrings.action_edit)
} }
} }
@Test @Test
fun `click on avatar test`() { fun `click on avatar test`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
val state = aRoomDetailsState( val state = aRoomDetailsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomAvatarUrl = "an_avatar_url", roomAvatarUrl = "an_avatar_url",
) )
val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url") val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url")
rule.setRoomDetailView( setRoomDetailView(
state = state, state = state,
openAvatarPreview = callback, openAvatarPreview = callback,
) )
rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() onNodeWithTag(TestTags.roomDetailAvatar.value).performClick()
callback.assertSuccess() callback.assertSuccess()
} }
@Test @Test
fun `click on avatar test on DM`() { fun `click on avatar test on DM`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
val state = aRoomDetailsState( val state = aRoomDetailsState(
roomType = RoomDetailsType.Dm( roomType = RoomDetailsType.Dm(
@ -241,114 +240,114 @@ class RoomDetailsViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url") val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url")
rule.setRoomDetailView( setRoomDetailView(
state = state, state = state,
openAvatarPreview = callback, openAvatarPreview = callback,
) )
rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() onNodeWithTag(TestTags.memberDetailAvatar.value).performClick()
callback.assertSuccess() callback.assertSuccess()
} }
@Test @Test
fun `click on mute emit expected event`() { fun `click on mute emit expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
val state = aRoomDetailsState( val state = aRoomDetailsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES), roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES),
) )
rule.setRoomDetailView( setRoomDetailView(
state = state, state = state,
) )
rule.clickOn(CommonStrings.common_mute) clickOn(CommonStrings.common_mute)
eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification) eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification)
} }
@Test @Test
fun `click on unmute emit expected event`() { fun `click on unmute emit expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
val state = aRoomDetailsState( val state = aRoomDetailsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE), roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE),
) )
rule.setRoomDetailView( setRoomDetailView(
state = state, state = state,
) )
rule.clickOn(CommonStrings.common_unmute) clickOn(CommonStrings.common_unmute)
eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification) eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on favorite emit expected Event`() { fun `click on favorite emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.common_favourite) clickOn(CommonStrings.common_favourite)
eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true)) eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true))
} }
@Config(qualifiers = "h1500dp") @Config(qualifiers = "h1500dp")
@Test @Test
fun `click on leave emit expected Event`() { fun `click on leave emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_room_details_leave_room_title) clickOn(R.string.screen_room_details_leave_room_title)
eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
} }
@Config(qualifiers = "h1500dp") @Config(qualifiers = "h1500dp")
@Test @Test
fun `click on report room invokes expected callback`() { fun `click on report room invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),
onReportRoomClick = callback, onReportRoomClick = callback,
) )
rule.clickOn(CommonStrings.action_report_room) clickOn(CommonStrings.action_report_room)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on knock requests invokes expected callback`() { fun `click on knock requests invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
canShowKnockRequests = true, canShowKnockRequests = true,
), ),
onKnockRequestsClick = callback, onKnockRequestsClick = callback,
) )
rule.clickOn(R.string.screen_room_details_requests_to_join_title) clickOn(R.string.screen_room_details_requests_to_join_title)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `click on profile invokes the expected callback`() { fun `click on profile invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(A_USER_ID) { callback -> ensureCalledOnceWithParam(A_USER_ID) { callback ->
rule.setRoomDetailView( setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), roomMemberDetailsState = aUserProfileState(userId = A_USER_ID),
), ),
onProfileClick = callback, onProfileClick = callback,
) )
rule.clickOn(R.string.screen_room_details_profile_row_title) clickOn(R.string.screen_room_details_profile_row_title)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
state: RoomDetailsState = aRoomDetailsState( state: RoomDetailsState = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),

View file

@ -5,18 +5,21 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roomdetailsedit.impl package io.element.android.features.roomdetailsedit.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert import androidx.compose.ui.test.assert
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isEditable import androidx.compose.ui.test.isEditable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
@ -28,58 +31,54 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomDetailsEditViewTest { class RoomDetailsEditViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back emits the expected Event`() { fun `clicking on back emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBack() pressBack()
eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
} }
@Test @Test
fun `clicking on discard when confirming exit emits the expected Event`() { fun `clicking on discard when confirming exit emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_discard) clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
} }
@Test @Test
fun `clicking on save when confirming exit emits the expected Event`() { fun `clicking on save when confirming exit emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save, inDialog = true) clickOn(CommonStrings.action_save, inDialog = true)
eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
} }
@Test @Test
fun `when edition is successful, the expected callback is invoked`() { fun `when edition is successful, the expected callback is invoked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
saveAction = AsyncAction.Success(Unit) saveAction = AsyncAction.Success(Unit)
@ -90,55 +89,55 @@ class RoomDetailsEditViewTest {
} }
@Test @Test
fun `when name is changed, the expected Event is emitted`() { fun `when name is changed, the expected Event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomRawName = "Marketing", roomRawName = "Marketing",
), ),
) )
rule.onNodeWithText("Marketing").performTextInput("A") onNodeWithText("Marketing").performTextInput("A")
eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing")) eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing"))
} }
@Test @Test
fun `when user cannot change name, nothing happen`() { fun `when user cannot change name, nothing happen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomRawName = "Marketing", roomRawName = "Marketing",
canChangeName = false, canChangeName = false,
), ),
) )
rule.onNodeWithText("Marketing").assert(!isEditable()) onNodeWithText("Marketing").assert(!isEditable())
} }
@Test @Test
fun `when topic is changed, the expected Event is emitted`() { fun `when topic is changed, the expected Event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomTopic = "My Topic", roomTopic = "My Topic",
), ),
) )
rule.onNodeWithText("My Topic").performTextInput("A") onNodeWithText("My Topic").performTextInput("A")
eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic")) eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic"))
} }
@Test @Test
fun `when user cannot change topic, nothing happen`() { fun `when user cannot change topic, nothing happen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomTopic = "My Topic", roomTopic = "My Topic",
canChangeTopic = false, canChangeTopic = false,
), ),
) )
rule.onNodeWithText("My Topic").assert(!isEditable()) onNodeWithText("My Topic").assert(!isEditable())
} }
@Ignore("This test is failing because the bottom sheet does not open") @Ignore("This test is failing because the bottom sheet does not open")
@ -171,73 +170,73 @@ class RoomDetailsEditViewTest {
private fun testAvatarChange( private fun testAvatarChange(
@StringRes stringActionRes: Int, @StringRes stringActionRes: Int,
expectedEvent: RoomDetailsEditEvent.HandleAvatarAction, expectedEvent: RoomDetailsEditEvent.HandleAvatarAction,
) { ) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
// Open the bottom sheet // Open the bottom sheet
rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() onNode(hasTestTag(TestTags.editAvatar.value)).performClick()
rule.onNodeWithText(rule.activity.getString(stringActionRes)).assertExists() onNodeWithText(activity!!.getString(stringActionRes)).assertExists()
rule.clickOn(stringActionRes) clickOn(stringActionRes)
eventsRecorder.assertSingle(expectedEvent) eventsRecorder.assertSingle(expectedEvent)
} }
@Test @Test
fun `when user cannot change avatar, nothing happen`() { fun `when user cannot change avatar, nothing happen`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
canChangeAvatar = false, canChangeAvatar = false,
), ),
) )
rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() onNode(hasTestTag(TestTags.editAvatar.value)).performClick()
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_take_photo)).assertDoesNotExist() onNodeWithText(activity!!.getString(CommonStrings.action_take_photo)).assertDoesNotExist()
} }
@Test @Test
fun `when save is clicked, the expected Event is emitted`() { fun `when save is clicked, the expected Event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
saveButtonEnabled = true, saveButtonEnabled = true,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
} }
@Test @Test
fun `when save is clicked, but nothing need to be saved, nothing happens`() { fun `when save is clicked, but nothing need to be saved, nothing happens`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
saveButtonEnabled = false, saveButtonEnabled = false,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
} }
@Test @Test
fun `when error is shown, closing the dialog emit the expected Event`() { fun `when error is shown, closing the dialog emit the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
saveAction = AsyncAction.Failure(RuntimeException("Whelp")), saveAction = AsyncAction.Failure(RuntimeException("Whelp")),
), ),
) )
rule.clickOn(CommonStrings.action_ok) clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog) eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailsEditView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailsEditView(
state: RoomDetailsEditState, state: RoomDetailsEditState,
onDone: () -> Unit = EnsureNeverCalled(), onDone: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,15 +6,18 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@ -22,31 +25,27 @@ import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomDirectoryViewTest { class RoomDirectoryViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `typing text in search field emits the expected Event`() { fun `typing text in search field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
rule.setRoomDirectoryView( setRoomDirectoryView(
state = aRoomDirectoryState( state = aRoomDirectoryState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( onNodeWithTag(TestTags.searchTextField.value).performTextInput(
text = "Test" text = "Test"
) )
eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test"))
} }
@Test @Test
fun `clicking on room item then onResultClick lambda is called once`() { fun `clicking on room item then onResultClick lambda is called once`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
val state = aRoomDirectoryState( val state = aRoomDirectoryState(
roomDescriptions = aRoomDescriptionList(), roomDescriptions = aRoomDescriptionList(),
@ -54,27 +53,27 @@ class RoomDirectoryViewTest {
) )
val clickedRoom = state.roomDescriptions.first() val clickedRoom = state.roomDescriptions.first()
ensureCalledOnceWithParam(clickedRoom) { callback -> ensureCalledOnceWithParam(clickedRoom) { callback ->
rule.setRoomDirectoryView( setRoomDirectoryView(
state = state, state = state,
onResultClick = callback, onResultClick = callback,
) )
rule.onNodeWithText(clickedRoom.computedName).performClick() onNodeWithText(clickedRoom.computedName).performClick()
} }
} }
@Test @Test
fun `composing load more indicator emits expected Event`() { fun `composing load more indicator emits expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>() val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
val state = aRoomDirectoryState( val state = aRoomDirectoryState(
displayLoadMoreIndicator = true, displayLoadMoreIndicator = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
rule.setRoomDirectoryView(state = state) setRoomDirectoryView(state = state)
eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomDirectoryView(
state: RoomDirectoryState, state: RoomDirectoryState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(), onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.roommembermoderation.impl package io.element.android.features.roommembermoderation.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.ModerationActionState
@ -24,21 +27,17 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams
import io.element.android.tests.testutils.pressTag import io.element.android.tests.testutils.pressTag
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomMemberModerationViewTest { class RoomMemberModerationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on display profile action calls onSelectAction`() { fun `clicking on display profile action calls onSelectAction`() = runAndroidComposeUiTest {
val user = anAlice() val user = anAlice()
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.DisplayProfile, user) { callback -> ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.DisplayProfile, user) { callback ->
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = user, selectedUser = user,
actions = listOf( actions = listOf(
@ -48,16 +47,16 @@ class RoomMemberModerationViewTest {
), ),
onSelectAction = callback onSelectAction = callback
) )
rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info)
} }
} }
@Test @Test
fun `clicking on kick user action calls onSelectAction`() { fun `clicking on kick user action calls onSelectAction`() = runAndroidComposeUiTest {
val user = anAlice() val user = anAlice()
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.KickUser, user) { callback -> ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.KickUser, user) { callback ->
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = user, selectedUser = user,
actions = listOf( actions = listOf(
@ -67,18 +66,18 @@ class RoomMemberModerationViewTest {
), ),
onSelectAction = callback onSelectAction = callback
) )
rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) clickOn(R.string.screen_bottom_sheet_manage_room_member_remove)
// Gives time for bottomsheet to hide // Gives time for bottomsheet to hide
rule.mainClock.advanceTimeBy(1_000) mainClock.advanceTimeBy(1_000)
} }
} }
@Test @Test
fun `clicking on ban user action calls onSelectAction`() { fun `clicking on ban user action calls onSelectAction`() = runAndroidComposeUiTest {
val user = anAlice() val user = anAlice()
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.BanUser, user) { callback -> ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.BanUser, user) { callback ->
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = user, selectedUser = user,
actions = listOf( actions = listOf(
@ -88,18 +87,18 @@ class RoomMemberModerationViewTest {
), ),
onSelectAction = callback onSelectAction = callback
) )
rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) clickOn(R.string.screen_bottom_sheet_manage_room_member_ban)
// Gives time for bottomsheet to hide // Gives time for bottomsheet to hide
rule.mainClock.advanceTimeBy(1_000) mainClock.advanceTimeBy(1_000)
} }
} }
@Test @Test
fun `clicking on unban user action calls onSelectAction`() { fun `clicking on unban user action calls onSelectAction`() = runAndroidComposeUiTest {
val user = anAlice() val user = anAlice()
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.UnbanUser, user) { callback -> ensureCalledOnceWithTwoParams<ModerationAction, MatrixUser>(ModerationAction.UnbanUser, user) { callback ->
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = user, selectedUser = user,
actions = listOf( actions = listOf(
@ -109,100 +108,100 @@ class RoomMemberModerationViewTest {
), ),
onSelectAction = callback onSelectAction = callback
) )
rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) clickOn(R.string.screen_bottom_sheet_manage_room_member_unban)
// Gives time for bottomsheet to hide // Gives time for bottomsheet to hide
rule.mainClock.advanceTimeBy(1_000) mainClock.advanceTimeBy(1_000)
} }
} }
@Test @Test
fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { fun `clicking submit on kick confirmation dialog sends DoKickUser event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams, kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = ""))
} }
@Test @Test
fun `clicking dismiss on kick confirmation dialog sends Reset event`() { fun `clicking dismiss on kick confirmation dialog sends Reset event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams, kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogNegative.value) pressTag(TestTags.dialogNegative.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset)
} }
@Test @Test
fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { fun `clicking submit on ban confirmation dialog sends DoBanUser event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
banUserAsyncAction = AsyncAction.ConfirmingNoParams, banUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = ""))
} }
@Test @Test
fun `clicking dismiss on ban confirmation dialog sends Reset event`() { fun `clicking dismiss on ban confirmation dialog sends Reset event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
banUserAsyncAction = AsyncAction.ConfirmingNoParams, banUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogNegative.value) pressTag(TestTags.dialogNegative.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset)
} }
@Test @Test
fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, unbanUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogPositive.value) pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser(""))
} }
@Test @Test
fun `clicking dismiss on unban confirmation dialog sends Reset event`() { fun `clicking dismiss on unban confirmation dialog sends Reset event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>() val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>()
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, unbanUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressTag(TestTags.dialogNegative.value) pressTag(TestTags.dialogNegative.value)
eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset)
} }
@Test @Test
fun `disabled actions are not clickable`() { fun `disabled actions are not clickable`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomMemberModerationEvents>(expectEvents = false)
rule.setRoomMemberModerationView( setRoomMemberModerationView(
aRoomMembersModerationState( aRoomMembersModerationState(
selectedUser = anAlice(), selectedUser = anAlice(),
actions = listOf( actions = listOf(
@ -211,11 +210,11 @@ class RoomMemberModerationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) clickOn(R.string.screen_bottom_sheet_manage_room_member_remove)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomMemberModerationView( private fun AndroidComposeUiTest<ComponentActivity>.setRoomMemberModerationView(
state: InternalRoomMemberModerationState, state: InternalRoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(),
) { ) {

View file

@ -6,16 +6,19 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securebackup.impl.enter package io.element.android.features.securebackup.impl.enter
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -26,58 +29,54 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SecureBackupEnterRecoveryKeyViewTest { class SecureBackupEnterRecoveryKeyViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `back key pressed - calls onBackClick`() { fun `back key pressed - calls onBackClick`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(), aSecureBackupEnterRecoveryKeyState(),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `back button clicked - calls onBackClick`() { fun `back button clicked - calls onBackClick`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(), aSecureBackupEnterRecoveryKeyState(),
onBackClick = callback, onBackClick = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `tapping on Continue when key is valid - calls expected action`() { fun `tapping on Continue when key is valid - calls expected action`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>() val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder),
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit)
} }
@Test @Test
fun `entering a char emits the expected event`() { fun `entering a char emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>() val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
val keyValue = aFormattedRecoveryKey() val keyValue = aFormattedRecoveryKey()
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder),
) )
rule.onNodeWithText(keyValue).performTextInput("X") onNodeWithText(keyValue).performTextInput("X")
recorder.assertSingle( recorder.assertSingle(
SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue") SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue")
) )
@ -85,43 +84,43 @@ class SecureBackupEnterRecoveryKeyViewTest {
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `toggling the visibility of the textfield changes it`() { fun `toggling the visibility of the textfield changes it`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>() val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
val keyValue = aFormattedRecoveryKey() val keyValue = aFormattedRecoveryKey()
rule.setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder))
// Initially, the text field should be visible // Initially, the text field should be visible
rule.onNodeWithText(keyValue).assertExists() onNodeWithText(keyValue).assertExists()
rule.onNodeWithContentDescription(rule.activity.getString(CommonStrings.a11y_hide_password)).performClick() onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_hide_password)).performClick()
rule.waitForIdle() waitForIdle()
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false)) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false))
} }
@Test @Test
fun `validating from keyboard emits the expected event`() { fun `validating from keyboard emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>() val recorder = EventsRecorder<SecureBackupEnterRecoveryKeyEvents>()
val keyValue = aFormattedRecoveryKey() val keyValue = aFormattedRecoveryKey()
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder),
) )
rule.onNodeWithText(keyValue).performImeAction() onNodeWithText(keyValue).performImeAction()
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit)
} }
@Test @Test
fun `when submit action succeeds - calls onDone`() { fun `when submit action succeeds - calls onDone`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setSecureBackupEnterRecoveryKeyView( setSecureBackupEnterRecoveryKeyView(
aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)), aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)),
onDone = callback, onDone = callback,
) )
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecureBackupEnterRecoveryKeyView( private fun AndroidComposeUiTest<ComponentActivity>.setSecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState, state: SecureBackupEnterRecoveryKeyState,
onDone: () -> Unit = EnsureNeverCalled(), onDone: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securebackup.impl.reset.password package io.element.android.features.securebackup.impl.reset.password
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -22,64 +25,59 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ResetIdentityPasswordViewTest { class ResetIdentityPasswordViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `pressing the back HW button invokes the expected callback`() { fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setResetPasswordView( setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
onBack = it, onBack = it,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `clicking on the back navigation button invokes the expected callback`() { fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setResetPasswordView( setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
onBack = it, onBack = it,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking 'Reset identity' confirms the reset`() { fun `clicking 'Reset identity' confirms the reset`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>() val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
rule.setResetPasswordView( setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder),
) )
rule.onNodeWithText("Password").performTextInput("A password") onNodeWithText("Password").performTextInput("A password")
rule.clickOn(CommonStrings.action_reset_identity) clickOn(CommonStrings.action_reset_identity)
eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password"))
} }
@Test @Test
fun `modifying the password dismisses the error state`() { fun `modifying the password dismisses the error state`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>() val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
rule.setResetPasswordView( setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder),
) )
rule.onNodeWithText("Password").performTextInput("A password") onNodeWithText("Password").performTextInput("A password")
eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetPasswordView( private fun AndroidComposeUiTest<ComponentActivity>.setResetPasswordView(
state: ResetIdentityPasswordState, state: ResetIdentityPasswordState,
onBack: () -> Unit = EnsureNeverCalled(), onBack: () -> Unit = EnsureNeverCalled(),
) { ) {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securebackup.impl.reset.root package io.element.android.features.securebackup.impl.reset.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -20,76 +23,71 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ResetIdentityRootViewTest { class ResetIdentityRootViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `pressing the back HW button invokes the expected callback`() { fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setResetRootView( setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
onBack = it, onBack = it,
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `clicking on the back navigation button invokes the expected callback`() { fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setResetRootView( setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
onBack = it, onBack = it,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
@Config(qualifiers = "h720dp") @Config(qualifiers = "h720dp")
fun `clicking Continue displays the confirmation dialog`() { fun `clicking Continue displays the confirmation dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>() val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
rule.setResetRootView( setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
) )
rule.clickOn(R.string.screen_encryption_reset_action_continue_reset) clickOn(R.string.screen_encryption_reset_action_continue_reset)
eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
} }
@Test @Test
fun `clicking 'Yes, reset now' confirms the reset`() { fun `clicking 'Yes, reset now' confirms the reset`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setResetRootView( setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}),
onContinue = it, onContinue = it,
) )
rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) clickOn(R.string.screen_reset_encryption_confirmation_alert_action)
} }
} }
@Test @Test
fun `clicking Cancel dismisses the dialog`() { fun `clicking Cancel dismisses the dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>() val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
rule.setResetRootView( setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetRootView( private fun AndroidComposeUiTest<ComponentActivity>.setResetRootView(
state: ResetIdentityRootState, state: ResetIdentityRootState,
onBack: () -> Unit = EnsureNeverCalled(), onBack: () -> Unit = EnsureNeverCalled(),
onContinue: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securityandprivacy.impl.editroomaddress package io.element.android.features.securityandprivacy.impl.editroomaddress
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
@ -23,86 +26,82 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class EditRoomAddressViewTest { class EditRoomAddressViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `click on back invokes expected callback`() { fun `click on back invokes expected callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setEditRoomAddressView(onBackClick = callback) setEditRoomAddressView(onBackClick = callback)
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `click on disabled save doesn't emit event`() { fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<EditRoomAddressEvents>(expectEvents = false) val recorder = EventsRecorder<EditRoomAddressEvents>(expectEvents = false)
val state = anEditRoomAddressState(eventSink = recorder) val state = anEditRoomAddressState(eventSink = recorder)
rule.setEditRoomAddressView(state) setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
recorder.assertEmpty() recorder.assertEmpty()
} }
@Test @Test
fun `click on enabled save emits the expected event`() { fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<EditRoomAddressEvents>() val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState( val state = anEditRoomAddressState(
roomAddress = "room", roomAddress = "room",
roomAddressValidity = RoomAddressValidity.Valid, roomAddressValidity = RoomAddressValidity.Valid,
eventSink = recorder eventSink = recorder
) )
rule.setEditRoomAddressView(state) setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
recorder.assertSingle(EditRoomAddressEvents.Save) recorder.assertSingle(EditRoomAddressEvents.Save)
} }
@Test @Test
fun `text changes on text field emits the expected event`() { fun `text changes on text field emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<EditRoomAddressEvents>() val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState( val state = anEditRoomAddressState(
roomAddress = "", roomAddress = "",
eventSink = recorder eventSink = recorder
) )
rule.setEditRoomAddressView(state) setEditRoomAddressView(state)
rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias")
recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias"))
} }
@Test @Test
fun `click on dismiss error emits the expected event`() { fun `click on dismiss error emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<EditRoomAddressEvents>() val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState( val state = anEditRoomAddressState(
roomAddress = "", roomAddress = "",
saveAction = AsyncAction.Failure(IllegalStateException()), saveAction = AsyncAction.Failure(IllegalStateException()),
eventSink = recorder eventSink = recorder
) )
rule.setEditRoomAddressView(state) setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
recorder.assertSingle(EditRoomAddressEvents.DismissError) recorder.assertSingle(EditRoomAddressEvents.DismissError)
} }
@Test @Test
fun `click on retry error emits the expected event`() { fun `click on retry error emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<EditRoomAddressEvents>() val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState( val state = anEditRoomAddressState(
roomAddress = "", roomAddress = "",
saveAction = AsyncAction.Failure(IllegalStateException()), saveAction = AsyncAction.Failure(IllegalStateException()),
eventSink = recorder eventSink = recorder
) )
rule.setEditRoomAddressView(state) setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
recorder.assertSingle(EditRoomAddressEvents.Save) recorder.assertSingle(EditRoomAddressEvents.Save)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setEditRoomAddressView( private fun AndroidComposeUiTest<ComponentActivity>.setEditRoomAddressView(
state: EditRoomAddressState = anEditRoomAddressState( state: EditRoomAddressState = anEditRoomAddressState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
@ -24,26 +27,22 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ManageAuthorizedSpacesViewTest { class ManageAuthorizedSpacesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking back emits Cancel event`() { fun `clicking back emits Cancel event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>() val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
val state = aManageAuthorizedSpacesState(eventSink = recorder) val state = aManageAuthorizedSpacesState(eventSink = recorder)
rule.setManageAuthorizedSpacesView(state) setManageAuthorizedSpacesView(state)
rule.pressBack() pressBack()
recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel)
} }
@Test @Test
fun `clicking space checkbox emits ToggleSpace event`() { fun `clicking space checkbox emits ToggleSpace event`() = runAndroidComposeUiTest {
val roomId = A_ROOM_ID val roomId = A_ROOM_ID
val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") val space = aSpaceRoom(roomId = roomId, displayName = "Test Space")
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>() val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
@ -51,37 +50,37 @@ class ManageAuthorizedSpacesViewTest {
selectableSpaces = listOf(space), selectableSpaces = listOf(space),
eventSink = recorder eventSink = recorder
) )
rule.setManageAuthorizedSpacesView(state) setManageAuthorizedSpacesView(state)
rule.onNodeWithText("Test Space").performClick() onNodeWithText("Test Space").performClick()
recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
} }
@Test @Test
fun `clicking done button emits Done event`() { fun `clicking done button emits Done event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>() val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
val state = aManageAuthorizedSpacesState( val state = aManageAuthorizedSpacesState(
selectedIds = listOf(A_ROOM_ID), selectedIds = listOf(A_ROOM_ID),
eventSink = recorder eventSink = recorder
) )
rule.setManageAuthorizedSpacesView(state) setManageAuthorizedSpacesView(state)
rule.clickOn(CommonStrings.action_done) clickOn(CommonStrings.action_done)
recorder.assertSingle(ManageAuthorizedSpacesEvent.Done) recorder.assertSingle(ManageAuthorizedSpacesEvent.Done)
} }
@Test @Test
fun `done button is disabled when no spaces selected`() { fun `done button is disabled when no spaces selected`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>(expectEvents = false) val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>(expectEvents = false)
val state = aManageAuthorizedSpacesState( val state = aManageAuthorizedSpacesState(
selectedIds = emptyList(), selectedIds = emptyList(),
eventSink = recorder eventSink = recorder
) )
rule.setManageAuthorizedSpacesView(state) setManageAuthorizedSpacesView(state)
rule.clickOn(CommonStrings.action_done) clickOn(CommonStrings.action_done)
recorder.assertEmpty() recorder.assertEmpty()
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setManageAuthorizedSpacesView( private fun AndroidComposeUiTest<ComponentActivity>.setManageAuthorizedSpacesView(
state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState( state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState(
eventSink = EventsRecorder(expectEvents = false) eventSink = EventsRecorder(expectEvents = false)
), ),

View file

@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.securityandprivacy.impl.root package io.element.android.features.securityandprivacy.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.securityandprivacy.impl.R import io.element.android.features.securityandprivacy.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -23,73 +26,69 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SecurityAndPrivacyViewTest { class SecurityAndPrivacyViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `click on back invokes emits the expected event`() { fun `click on back invokes emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.pressBack() pressBack()
recorder.assertSingle(SecurityAndPrivacyEvent.Exit) recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
} }
@Test @Test
fun `discard cancellation emits the expected event`() { fun `discard cancellation emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_discard) clickOn(CommonStrings.action_discard)
recorder.assertSingle(SecurityAndPrivacyEvent.Exit) recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
} }
@Test @Test
fun `save cancellation confirmation emits the expected event`() { fun `save cancellation confirmation emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_save, inDialog = true) clickOn(CommonStrings.action_save, inDialog = true)
recorder.assertSingle(SecurityAndPrivacyEvent.Save) recorder.assertSingle(SecurityAndPrivacyEvent.Save)
} }
@Test @Test
fun `click on room access item emits the expected event`() { fun `click on room access item emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
} }
@Test @Test
fun `click on disabled save doesn't emit event`() { fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false) val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false)
val state = aSecurityAndPrivacyState(eventSink = recorder) val state = aSecurityAndPrivacyState(eventSink = recorder)
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
recorder.assertEmpty() recorder.assertEmpty()
} }
@Test @Test
fun `click on enabled save emits the expected event`() { fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
@ -97,14 +96,14 @@ class SecurityAndPrivacyViewTest {
roomAccess = SecurityAndPrivacyRoomAccess.Anyone, roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
) )
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
recorder.assertSingle(SecurityAndPrivacyEvent.Save) recorder.assertSingle(SecurityAndPrivacyEvent.Save)
} }
@Test @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `click on room address item emits the expected event`() { fun `click on room address item emits the expected event`() = runAndroidComposeUiTest {
val address = "@alias:matrix.org" val address = "@alias:matrix.org"
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
@ -114,14 +113,14 @@ class SecurityAndPrivacyViewTest {
roomAccess = SecurityAndPrivacyRoomAccess.Anyone, roomAccess = SecurityAndPrivacyRoomAccess.Anyone,
), ),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.onNodeWithText(address).performClick() onNodeWithText(address).performClick()
recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress) recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on room visibility item emits the expected event`() { fun `click on room visibility item emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
@ -130,14 +129,14 @@ class SecurityAndPrivacyViewTest {
isVisibleInRoomDirectory = AsyncData.Success(false), isVisibleInRoomDirectory = AsyncData.Success(false),
), ),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on history visibility item emits the expected event`() { fun `click on history visibility item emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
@ -145,65 +144,65 @@ class SecurityAndPrivacyViewTest {
historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited, historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited,
), ),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited)) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited))
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on encryption item emits the expected event`() { fun `click on encryption item emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), savedSettings = aSecurityAndPrivacySettings(isEncrypted = false),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) clickOn(R.string.screen_security_and_privacy_encryption_toggle_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState)
} }
@Test @Test
fun `click on encryption confirm emits the expected event`() { fun `click on encryption confirm emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
showEncryptionConfirmation = true, showEncryptionConfirmation = true,
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title)
recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption) recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on space member access emits the expected event`() { fun `click on space member access emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on ask to join with space members emits the expected event`() { fun `click on ask to join with space members emits the expected event`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title)
recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `manage spaces footer is shown when space member access is selected`() { fun `manage spaces footer is shown when space member access is selected`() = runAndroidComposeUiTest {
val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false) val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false)
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
@ -212,15 +211,16 @@ class SecurityAndPrivacyViewTest {
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)), roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)),
), ),
) )
rule.setSecurityAndPrivacyView(state) setSecurityAndPrivacyView(state)
// The footer text uses AnnotatedString with a link. Verify the footer text is displayed. // The footer text uses AnnotatedString with a link. Verify the footer text is displayed.
val actionFooterText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) val resources = activity!!.resources
val footerText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) val actionFooterText = resources.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action)
rule.onNodeWithText(footerText).assertExists() val footerText = resources.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText)
onNodeWithText(footerText).assertExists()
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecurityAndPrivacyView( private fun AndroidComposeUiTest<ComponentActivity>.setSecurityAndPrivacyView(
state: SecurityAndPrivacyState = aSecurityAndPrivacyState( state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
testCommonDependencies(libs) testCommonDependencies(libs)

View file

@ -5,13 +5,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.space.impl.addroom package io.element.android.features.space.impl.addroom
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -22,77 +25,73 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AddRoomToSpaceViewTest { class AddRoomToSpaceViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() { fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>() val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
ensureCalledOnce { ensureCalledOnce {
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
isSearchActive = false, isSearchActive = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it, onBackClick = it,
) )
rule.pressBack() pressBack()
} }
eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss) eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss)
} }
@Test @Test
fun `clicking back when search active emits CloseSearch event`() { fun `clicking back when search active emits CloseSearch event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>() val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
isSearchActive = true, isSearchActive = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBack() pressBack()
eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
} }
@Test @Test
fun `clicking save emits Save event`() { fun `clicking save emits Save event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>() val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_save) clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save) eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking room in suggestions emits ToggleRoom event`() { fun `clicking room in suggestions emits ToggleRoom event`() = runAndroidComposeUiTest {
val suggestions = aSelectRoomInfoList() val suggestions = aSelectRoomInfoList()
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>() val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
suggestions = suggestions, suggestions = suggestions,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.onNodeWithText(suggestions.first().name!!).performClick() onNodeWithText(suggestions.first().name!!).performClick()
eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first())) eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first()))
} }
@Test @Test
fun `onRoomsAdded called when saveAction is Success`() { fun `onRoomsAdded called when saveAction is Success`() = runAndroidComposeUiTest {
ensureCalledOnce { ensureCalledOnce {
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
saveAction = AsyncAction.Success(Unit), saveAction = AsyncAction.Success(Unit),
), ),
@ -103,10 +102,10 @@ class AddRoomToSpaceViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `displaying search results sends UpdateSearchVisibleRange event`() { fun `displaying search results sends UpdateSearchVisibleRange event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>() val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
val rooms = aSelectRoomInfoList() val rooms = aSelectRoomInfoList()
rule.setAddRoomToSpaceView( setAddRoomToSpaceView(
anAddRoomToSpaceState( anAddRoomToSpaceState(
isSearchActive = true, isSearchActive = true,
searchResults = SearchBarResultState.Results(rooms), searchResults = SearchBarResultState.Results(rooms),
@ -117,7 +116,7 @@ class AddRoomToSpaceViewTest {
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddRoomToSpaceView( private fun AndroidComposeUiTest<ComponentActivity>.setAddRoomToSpaceView(
state: AddRoomToSpaceState, state: AddRoomToSpaceState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomsAdded: () -> Unit = EnsureNeverCalled(), onRoomsAdded: () -> Unit = EnsureNeverCalled(),

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.space.impl.root package io.element.android.features.space.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -33,37 +36,33 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SpaceViewTest { class SpaceViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
hasMoreToLoad = false, hasMoreToLoad = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onBackClick = it, onBackClick = it,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on a room name invokes the expected callback`() { fun `clicking on a room name invokes the expected callback`() = runAndroidComposeUiTest {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<SpaceEvents>(expectEvents = false)
ensureCalledOnceWithParam(aSpaceRoom) { ensureCalledOnceWithParam(aSpaceRoom) {
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = listOf(aSpaceRoom), children = listOf(aSpaceRoom),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -71,91 +70,91 @@ class SpaceViewTest {
), ),
onRoomClick = it, onRoomClick = it,
) )
rule.onNodeWithText(A_ROOM_NAME).performClick() onNodeWithText(A_ROOM_NAME).performClick()
} }
} }
@Test @Test
fun `clicking on Join room emits the expected Event`() { fun `clicking on Join room emits the expected Event`() = runAndroidComposeUiTest {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null)
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = listOf(aSpaceRoom), children = listOf(aSpaceRoom),
hasMoreToLoad = false, hasMoreToLoad = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_join) clickOn(CommonStrings.action_join)
eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom))
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on accept invite emits the expected Event`() { fun `clicking on accept invite emits the expected Event`() = runAndroidComposeUiTest {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
hasMoreToLoad = false, hasMoreToLoad = false,
children = listOf(aSpaceRoom), children = listOf(aSpaceRoom),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_accept) clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom))
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on decline invite emits the expected Event`() { fun `clicking on decline invite emits the expected Event`() = runAndroidComposeUiTest {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED)
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
hasMoreToLoad = false, hasMoreToLoad = false,
children = listOf(aSpaceRoom), children = listOf(aSpaceRoom),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_decline) clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom))
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on topic emits the expected Event`() { fun `clicking on topic emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC),
hasMoreToLoad = false, hasMoreToLoad = false,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.onNodeWithText(A_ROOM_TOPIC).performClick() onNodeWithText(A_ROOM_TOPIC).performClick()
eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC))
} }
@Test @Test
fun `clicking back in manage mode emits ExitManageMode event`() { fun `clicking back in manage mode emits ExitManageMode event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
hasMoreToLoad = false, hasMoreToLoad = false,
isManageMode = true, isManageMode = true,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) eventsRecorder.assertSingle(SpaceEvents.ExitManageMode)
} }
@Test @Test
fun `clicking on room in manage mode emits ToggleRoomSelection event`() { fun `clicking on room in manage mode emits ToggleRoomSelection event`() = runAndroidComposeUiTest {
val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME)
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = listOf(aSpaceRoom), children = listOf(aSpaceRoom),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -163,14 +162,14 @@ class SpaceViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.onNodeWithText(A_ROOM_NAME).performClick() onNodeWithText(A_ROOM_NAME).performClick()
eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
} }
@Test @Test
fun `clicking remove button emits RemoveSelectedRooms event`() { fun `clicking remove button emits RemoveSelectedRooms event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -179,15 +178,15 @@ class SpaceViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_remove) clickOn(CommonStrings.action_remove)
eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<SpaceEvents>() val eventsRecorder = EventsRecorder<SpaceEvents>()
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), children = listOf(aSpaceRoom(roomId = A_ROOM_ID)),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -198,14 +197,14 @@ class SpaceViewTest {
) )
) )
// Click on the Remove button in the confirmation dialog // Click on the Remove button in the confirmation dialog
rule.clickOn(CommonStrings.action_remove, inDialog = true) clickOn(CommonStrings.action_remove, inDialog = true)
eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval)
} }
@Test @Test
fun `clicking create room button calls the expected callback`() { fun `clicking create room button calls the expected callback`() = runAndroidComposeUiTest {
val onCreateRoomClick = lambdaRecorder<Unit> { } val onCreateRoomClick = lambdaRecorder<Unit> { }
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = emptyList(), children = emptyList(),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -214,14 +213,14 @@ class SpaceViewTest {
), ),
onCreateRoomClick = onCreateRoomClick, onCreateRoomClick = onCreateRoomClick,
) )
rule.clickOn(CommonStrings.action_create_room) clickOn(CommonStrings.action_create_room)
onCreateRoomClick.assertions().isCalledOnce() onCreateRoomClick.assertions().isCalledOnce()
} }
@Test @Test
fun `clicking add existing room button calls the expected callback`() { fun `clicking add existing room button calls the expected callback`() = runAndroidComposeUiTest {
val onAddRoomClick = lambdaRecorder<Unit> { } val onAddRoomClick = lambdaRecorder<Unit> { }
rule.setSpaceView( setSpaceView(
aSpaceState( aSpaceState(
children = emptyList(), children = emptyList(),
hasMoreToLoad = false, hasMoreToLoad = false,
@ -230,12 +229,12 @@ class SpaceViewTest {
), ),
onAddRoomClick = onAddRoomClick, onAddRoomClick = onAddRoomClick,
) )
rule.clickOn(CommonStrings.action_add_existing_rooms) clickOn(CommonStrings.action_add_existing_rooms)
onAddRoomClick.assertions().isCalledOnce() onAddRoomClick.assertions().isCalledOnce()
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView( private fun AndroidComposeUiTest<ComponentActivity>.setSpaceView(
state: SpaceState, state: SpaceState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(), onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,56 +6,54 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.startchat.impl.joinbyaddress package io.element.android.features.startchat.impl.joinbyaddress
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.R
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class JoinBaseRoomByAddressViewTest { class JoinBaseRoomByAddressViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `entering text emits the expected event`() { fun `entering text emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomByAddressEvent>() val eventsRecorder = EventsRecorder<JoinRoomByAddressEvent>()
rule.setJoinRoomByAddressView( setJoinRoomByAddressView(
aJoinRoomByAddressState( aJoinRoomByAddressState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action) val text = activity!!.getString(R.string.screen_start_chat_join_room_by_address_action)
rule.onNodeWithText(text).performTextInput("#address:matrix.org") onNodeWithText(text).performTextInput("#address:matrix.org")
eventsRecorder.assertSingle(JoinRoomByAddressEvent.UpdateAddress("#address:matrix.org")) eventsRecorder.assertSingle(JoinRoomByAddressEvent.UpdateAddress("#address:matrix.org"))
} }
@Test @Test
fun `clicking on continue emits the expected event`() { fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomByAddressEvent>() val eventsRecorder = EventsRecorder<JoinRoomByAddressEvent>()
rule.setJoinRoomByAddressView( setJoinRoomByAddressView(
aJoinRoomByAddressState( aJoinRoomByAddressState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(JoinRoomByAddressEvent.Continue) eventsRecorder.assertSingle(JoinRoomByAddressEvent.Continue)
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomByAddressView( private fun AndroidComposeUiTest<ComponentActivity>.setJoinRoomByAddressView(
state: JoinRoomByAddressState, state: JoinRoomByAddressState,
) { ) {
setSafeContent { setSafeContent {

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.startchat.impl.root package io.element.android.features.startchat.impl.root
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.R
import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList
@ -27,70 +30,65 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class StartChatViewTest { class StartChatViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `clicking on back invokes the expected callback`() { fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onCloseClick = it onCloseClick = it
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `clicking on New room invokes the expected callback`() { fun `clicking on New room invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onNewRoomClick = it onNewRoomClick = it
) )
rule.clickOn(R.string.screen_create_room_action_create_room) clickOn(R.string.screen_create_room_action_create_room)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on Invite people invokes the expected callback`() { fun `clicking on Invite people invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
applicationName = "test", applicationName = "test",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onInviteFriendsClick = it onInviteFriendsClick = it
) )
val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") val text = activity!!.getString(CommonStrings.action_invite_friends_to_app, "test")
rule.onNodeWithText(text).performClick() onNodeWithText(text).performClick()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on a user suggestion invokes the expected callback`() { fun `clicking on a user suggestion invokes the expected callback`() = runAndroidComposeUiTest {
val recentDirectRoomList = aRecentDirectRoomList() val recentDirectRoomList = aRecentDirectRoomList()
val firstRoom = recentDirectRoomList[0] val firstRoom = recentDirectRoomList[0]
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnceWithParam(firstRoom.roomId) { ensureCalledOnceWithParam(firstRoom.roomId) {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
userListState = aUserListState( userListState = aUserListState(
recentDirectRooms = recentDirectRoomList recentDirectRooms = recentDirectRoomList
@ -99,42 +97,42 @@ class StartChatViewTest {
), ),
onOpenDM = it onOpenDM = it
) )
rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() onNodeWithText(firstRoom.matrixUser.getBestName()).performClick()
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on Join room by address invokes the expected callback`() { fun `clicking on Join room by address invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onJoinRoomByAddressClick = it onJoinRoomByAddressClick = it
) )
rule.clickOn(R.string.screen_start_chat_join_room_by_address_action) clickOn(R.string.screen_start_chat_join_room_by_address_action)
} }
} }
@Test @Test
fun `clicking on room directory invokes the expected callback`() { fun `clicking on room directory invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<StartChatEvents>(expectEvents = false)
ensureCalledOnce { ensureCalledOnce {
rule.setStartChatView( setStartChatView(
aCreateRoomRootState( aCreateRoomRootState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
isRoomDirectorySearchEnabled = true isRoomDirectorySearchEnabled = true
), ),
onRoomDirectorySearchClick = it onRoomDirectorySearchClick = it
) )
rule.clickOn(R.string.screen_room_directory_search_title) clickOn(R.string.screen_room_directory_search_title)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setStartChatView( private fun AndroidComposeUiTest<ComponentActivity>.setStartChatView(
state: StartChatState, state: StartChatState,
onCloseClick: () -> Unit = EnsureNeverCalled(), onCloseClick: () -> Unit = EnsureNeverCalled(),
onNewRoomClick: () -> Unit = EnsureNeverCalled(), onNewRoomClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.userprofile package io.element.android.features.userprofile
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileState
@ -39,193 +42,188 @@ import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class UserProfileViewTest { class UserProfileViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `on back button click - the expected callback is called`() = runTest { fun `on back button click - the expected callback is called`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setUserProfileView( setUserProfileView(
goBack = callback, goBack = callback,
) )
rule.pressBack() pressBack()
} }
} }
@Test @Test
fun `on avatar clicked - the expected callback is called`() = runTest { fun `on avatar clicked - the expected callback is called`() = runAndroidComposeUiTest {
ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback -> ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback ->
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL), state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL),
openAvatarPreview = callback, openAvatarPreview = callback,
) )
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
} }
} }
@Test @Test
fun `on avatar clicked with no avatar - nothing happens`() = runTest { fun `on avatar clicked with no avatar - nothing happens`() = runAndroidComposeUiTest {
val callback = EnsureNeverCalledWithTwoParams<String, String>() val callback = EnsureNeverCalledWithTwoParams<String, String>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null), state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null),
openAvatarPreview = callback, openAvatarPreview = callback,
) )
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
} }
@Test @Test
fun `on Share clicked - the expected callback is called`() = runTest { fun `on Share clicked - the expected callback is called`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setUserProfileView( setUserProfileView(
onShareUser = callback, onShareUser = callback,
) )
rule.clickOn(CommonStrings.action_share) clickOn(CommonStrings.action_share)
} }
} }
@Test @Test
fun `on Message clicked - the StartDm event is emitted`() = runTest { fun `on Message clicked - the StartDm event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
dmRoomId = A_ROOM_ID, dmRoomId = A_ROOM_ID,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_message) clickOn(CommonStrings.action_message)
eventsRecorder.assertSingle(UserProfileEvents.StartDM) eventsRecorder.assertSingle(UserProfileEvents.StartDM)
} }
@Test @Test
fun `on Call clicked - the expected callback is called`() = runTest { fun `on Call clicked - the expected callback is called`() = runAndroidComposeUiTest {
ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback -> ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback ->
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
dmRoomId = A_ROOM_ID, dmRoomId = A_ROOM_ID,
canCall = true, canCall = true,
), ),
onStartCall = callback, onStartCall = callback,
) )
rule.clickOn(CommonStrings.action_call) clickOn(CommonStrings.action_call)
} }
} }
@Test @Test
fun `on Video Call clicked - the expected callback is called`() = runTest { fun `on Video Call clicked - the expected callback is called`() = runAndroidComposeUiTest {
ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback -> ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback ->
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
dmRoomId = A_ROOM_ID, dmRoomId = A_ROOM_ID,
canCall = true, canCall = true,
), ),
onStartCall = callback, onStartCall = callback,
) )
rule.clickOn(CommonStrings.common_video) clickOn(CommonStrings.common_video)
} }
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_dm_details_block_user) clickOn(R.string.screen_dm_details_block_user)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true)) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true))
} }
@Test @Test
fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest { fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_dm_details_block_alert_action) clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false)) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false))
} }
@Test @Test
fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest { fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest { fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
isBlocked = AsyncData.Success(true), isBlocked = AsyncData.Success(true),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_dm_details_unblock_user) clickOn(R.string.screen_dm_details_unblock_user)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true)) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true))
} }
@Test @Test
fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest { fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
isBlocked = AsyncData.Success(true), isBlocked = AsyncData.Success(true),
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(R.string.screen_dm_details_unblock_alert_action) clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false)) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false))
} }
@Test @Test
fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest { fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState( state = aUserProfileState(
isBlocked = AsyncData.Success(true), isBlocked = AsyncData.Success(true),
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
@Test @Test
fun `on verify user clicked - the right callback is called`() = runTest { fun `on verify user clicked - the right callback is called`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam(A_USER_ID) { callback -> ensureCalledOnceWithParam(A_USER_ID) { callback ->
rule.setUserProfileView( setUserProfileView(
state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED), state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED),
onVerifyClick = callback, onVerifyClick = callback,
) )
rule.clickOn(CommonStrings.common_verify_user) clickOn(CommonStrings.common_verify_user)
} }
} }
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserProfileView( private fun AndroidComposeUiTest<ComponentActivity>.setUserProfileView(
state: UserProfileState = aUserProfileState( state: UserProfileState = aUserProfileState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),

View file

@ -6,10 +6,12 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.userprofile.shared.blockuser package io.element.android.features.userprofile.shared.blockuser
import androidx.activity.ComponentActivity import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileState
@ -18,18 +20,15 @@ import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class BlockUserDialogsTest { class BlockUserDialogsTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `confirm block user emit expected Event`() { fun `confirm block user emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { setContent {
BlockUserDialogs( BlockUserDialogs(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
@ -37,14 +36,14 @@ class BlockUserDialogsTest {
) )
) )
} }
rule.clickOn(R.string.screen_dm_details_block_alert_action) clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false)) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false))
} }
@Test @Test
fun `cancel block user emit expected Event`() { fun `cancel block user emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { setContent {
BlockUserDialogs( BlockUserDialogs(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
@ -52,14 +51,14 @@ class BlockUserDialogsTest {
) )
) )
} }
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
@Test @Test
fun `confirm unblock user emit expected Event`() { fun `confirm unblock user emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { setContent {
BlockUserDialogs( BlockUserDialogs(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
@ -67,14 +66,14 @@ class BlockUserDialogsTest {
) )
) )
} }
rule.clickOn(R.string.screen_dm_details_unblock_alert_action) clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false)) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false))
} }
@Test @Test
fun `cancel unblock user emit expected Event`() { fun `cancel unblock user emit expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { setContent {
BlockUserDialogs( BlockUserDialogs(
state = aUserProfileState( state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
@ -82,7 +81,7 @@ class BlockUserDialogsTest {
) )
) )
} }
rule.clickOn(CommonStrings.action_cancel) clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
} }

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.verifysession.impl.incoming package io.element.android.features.verifysession.impl.incoming
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
@ -18,59 +21,55 @@ import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class IncomingVerificationViewTest { class IncomingVerificationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
// region step Initial // region step Initial
@Test @Test
fun `back key pressed - ignore the verification`() { fun `back key pressed - ignore the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = aStepInitial(), step = aStepInitial(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@Test @Test
fun `ignore incoming verification emits the expected event`() { fun `ignore incoming verification emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = aStepInitial(), step = aStepInitial(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_ignore) clickOn(CommonStrings.action_ignore)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification)
} }
@Test @Test
fun `start incoming verification emits the expected event`() { fun `start incoming verification emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = aStepInitial(), step = aStepInitial(),
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_start_verification) clickOn(CommonStrings.action_start_verification)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification)
} }
@Test @Test
fun `back key pressed - when awaiting response cancels the verification`() { fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = aStepInitial( step = aStepInitial(
isWaiting = true, isWaiting = true,
@ -78,16 +77,16 @@ class IncomingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
// endregion step Initial // endregion step Initial
// region step Verifying // region step Verifying
@Test @Test
fun `back key pressed - when ready to verify cancels the verification`() { fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying( step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -96,14 +95,14 @@ class IncomingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@Test @Test
fun `back key pressed - when verifying and loading emits the expected event`() { fun `back key pressed - when verifying and loading emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying( step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -112,14 +111,14 @@ class IncomingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@Test @Test
fun `clicking on they do not match emits the expected event`() { fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying( step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -128,14 +127,14 @@ class IncomingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_dont_match) clickOn(R.string.screen_session_verification_they_dont_match)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification)
} }
@Test @Test
fun `clicking on they match emits the expected event`() { fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying( step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -144,35 +143,35 @@ class IncomingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_match) clickOn(R.string.screen_session_verification_they_match)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification)
} }
// endregion // endregion
// region step Failure // region step Failure
@Test @Test
fun `back key pressed - when failure resets the flow`() { fun `back key pressed - when failure resets the flow`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Failure, step = IncomingVerificationState.Step.Failure,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@Test @Test
fun `click on done - when failure resets the flow`() { fun `click on done - when failure resets the flow`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Failure, step = IncomingVerificationState.Step.Failure,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_done) clickOn(CommonStrings.action_done)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@ -180,33 +179,33 @@ class IncomingVerificationViewTest {
// region step Completed // region step Completed
@Test @Test
fun `back key pressed - on Completed step emits the expected event`() { fun `back key pressed - on Completed step emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Completed, step = IncomingVerificationState.Step.Completed,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
@Test @Test
fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() { fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>() val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView( setIncomingVerificationView(
anIncomingVerificationState( anIncomingVerificationState(
step = IncomingVerificationState.Step.Completed, step = IncomingVerificationState.Step.Completed,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(CommonStrings.action_done) clickOn(CommonStrings.action_done)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
} }
// endregion // endregion
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setIncomingVerificationView( private fun AndroidComposeUiTest<ComponentActivity>.setIncomingVerificationView(
state: IncomingVerificationState, state: IncomingVerificationState,
) { ) {
setContent { setContent {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.verifysession.impl.outgoing package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
@ -21,58 +24,54 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OutgoingVerificationViewTest { class OutgoingVerificationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test
fun `back key pressed - when canceled resets the flow`() { fun `back key pressed - when canceled resets the flow`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Canceled, step = OutgoingVerificationState.Step.Canceled,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset)
} }
@Test @Test
fun `back key pressed - when awaiting response cancels the verification`() { fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
} }
@Test @Test
fun `back key pressed - when ready to verify cancels the verification`() { fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Ready, step = OutgoingVerificationState.Step.Ready,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel)
} }
@Test @Test
fun `back key pressed - when verifying and not loading declines the verification`() { fun `back key pressed - when verifying and not loading declines the verification`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -81,14 +80,14 @@ class OutgoingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification)
} }
@Test @Test
fun `back key pressed - when verifying and loading does nothing`() { fun `back key pressed - when verifying and loading does nothing`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -97,42 +96,42 @@ class OutgoingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBackKey() pressBackKey()
eventsRecorder.assertEmpty() eventsRecorder.assertEmpty()
} }
@Test @Test
fun `back key pressed - on Completed exits the flow`() { fun `back key pressed - on Completed exits the flow`() = runAndroidComposeUiTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOutgoingVerificationView( setOutgoingVerificationView(
onBack = callback, onBack = callback,
state = anOutgoingVerificationState( state = anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Completed, step = OutgoingVerificationState.Step.Completed,
), ),
) )
rule.pressBackKey() pressBackKey()
} }
} }
@Test @Test
fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Completed, step = OutgoingVerificationState.Step.Completed,
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
onFinished = callback, onFinished = callback,
) )
rule.clickOn(CommonStrings.action_continue) clickOn(CommonStrings.action_continue)
} }
} }
@Test @Test
fun `clicking on they match emits the expected event`() { fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -141,14 +140,14 @@ class OutgoingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_match) clickOn(R.string.screen_session_verification_they_match)
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification)
} }
@Test @Test
fun `clicking on they do not match emits the expected event`() { fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>() val eventsRecorder = EventsRecorder<OutgoingVerificationViewEvents>()
rule.setOutgoingVerificationView( setOutgoingVerificationView(
anOutgoingVerificationState( anOutgoingVerificationState(
step = OutgoingVerificationState.Step.Verifying( step = OutgoingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(), data = aEmojisSessionVerificationData(),
@ -157,11 +156,11 @@ class OutgoingVerificationViewTest {
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.clickOn(R.string.screen_session_verification_they_dont_match) clickOn(R.string.screen_session_verification_they_dont_match)
eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification)
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOutgoingVerificationView( private fun AndroidComposeUiTest<ComponentActivity>.setOutgoingVerificationView(
state: OutgoingVerificationState, state: OutgoingVerificationState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onFinished: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(),

View file

@ -5,9 +5,9 @@
# Project # Project
android_gradle_plugin = "8.13.2" android_gradle_plugin = "8.13.2"
# When updating this, please also update the version in the file ./idea/kotlinc.xml # When updating this, please also update the version in the file ./idea/kotlinc.xml
kotlin = "2.3.20" kotlin = "2.3.21"
kotlinpoet = "2.3.0" kotlinpoet = "2.3.0"
ksp = "2.3.6" ksp = "2.3.7"
firebaseAppDistribution = "5.2.1" firebaseAppDistribution = "5.2.1"
# AndroidX # AndroidX
@ -22,7 +22,7 @@ camera = "1.6.0"
work = "2.11.2" work = "2.11.2"
# Compose # Compose
compose_bom = "2026.03.01" compose_bom = "2026.04.01"
# Coroutines # Coroutines
coroutines = "1.10.2" coroutines = "1.10.2"

View file

@ -44,14 +44,12 @@ android {
} }
dependencies { dependencies {
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.services.analytics.api)
implementation(libs.serialization.json)
api(projects.libraries.sessionStorage.api)
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
api(projects.libraries.architecture) implementation(libs.serialization.json)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.analytics.api)
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)

View file

@ -32,10 +32,12 @@ dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.network) implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.workmanager.api) implementation(projects.libraries.workmanager.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)

View file

@ -19,6 +19,7 @@ dependencies {
api(projects.libraries.matrix.api) api(projects.libraries.matrix.api)
api(libs.coroutines.core) api(libs.coroutines.core)
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(projects.libraries.architecture)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)

View file

@ -19,8 +19,10 @@ android {
setupDependencyInjection() setupDependencyInjection()
dependencies { dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.gif) implementation(libs.coil.gif)

View file

@ -15,6 +15,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.coroutines.core)
api(projects.libraries.mediaupload.api) api(projects.libraries.mediaupload.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -27,10 +28,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
interface MediaGalleryDataSource { interface MediaGalleryDataSource {
fun start() fun start(coroutineScope: CoroutineScope)
fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>>
fun getLastData(): AsyncData<GroupedMediaItems> fun getLastData(): AsyncData<GroupedMediaItems>
suspend fun loadMore(direction: Timeline.PaginationDirection) suspend fun loadMore(direction: Timeline.PaginationDirection)
@ -58,7 +60,7 @@ class TimelineMediaGalleryDataSource(
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun start() { override fun start(coroutineScope: CoroutineScope) {
if (!isStarted.compareAndSet(false, true)) { if (!isStarted.compareAndSet(false, true)) {
return return
} }
@ -96,9 +98,12 @@ class TimelineMediaGalleryDataSource(
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
} }
.onCompletion { .onCompletion {
timeline?.close() timeline?.let {
Timber.d("Timeline media gallery data source flow completed for room ${room.roomId}, closing timeline")
it.close()
}
} }
.launchIn(room.roomCoroutineScope) .launchIn(coroutineScope)
} }
override suspend fun loadMore(direction: Timeline.PaginationDirection) { override suspend fun loadMore(direction: Timeline.PaginationDirection) {

View file

@ -78,7 +78,7 @@ class MediaGalleryPresenter(
.collectAsState(AsyncData.Uninitialized) .collectAsState(AsyncData.Uninitialized)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
mediaGalleryDataSource.start() mediaGalleryDataSource.start(this)
} }
val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms ->

View file

@ -35,6 +35,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -62,11 +63,12 @@ class MediaViewerDataSource(
private val localMediaStates: MutableMap<String, MutableState<AsyncData<LocalMedia>>> = private val localMediaStates: MutableMap<String, MutableState<AsyncData<LocalMedia>>> =
mutableMapOf() mutableMapOf()
fun setup() { fun setup(coroutineScope: CoroutineScope) {
galleryDataSource.start() galleryDataSource.start(coroutineScope)
} }
fun dispose() { fun dispose() {
Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files")
mediaFiles.forEach { it.close() } mediaFiles.forEach { it.close() }
mediaFiles.clear() mediaFiles.clear()
localMediaStates.clear() localMediaStates.clear()

View file

@ -88,7 +88,7 @@ class MediaViewerPresenter(
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) } var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
DisposableEffect(Unit) { DisposableEffect(Unit) {
dataSource.setup() dataSource.setup(coroutineScope)
onDispose { onDispose {
dataSource.dispose() dataSource.dispose()
} }

View file

@ -20,12 +20,13 @@ import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryData
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.MediaItem
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
class SingleMediaGalleryDataSource( class SingleMediaGalleryDataSource(
private val data: GroupedMediaItems, private val data: GroupedMediaItems,
) : MediaGalleryDataSource { ) : MediaGalleryDataSource {
override fun start() = Unit override fun start(coroutineScope: CoroutineScope) = Unit
override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data))
override fun getLastData(): AsyncData<GroupedMediaItems> = AsyncData.Success(data) override fun getLastData(): AsyncData<GroupedMediaItems> = AsyncData.Success(data)

Some files were not shown because too many files have changed in this diff Show more