Fix and add tests related to location

This commit is contained in:
ganfra
2026-03-12 12:31:51 +01:00
parent 392fa9de9b
commit 8274ef220d
5 changed files with 404 additions and 291 deletions

View File

@@ -57,12 +57,14 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
)
}
private fun aShareLocationState(
fun aShareLocationState(
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
dialogState: ShareLocationState.Dialog,
trackUserPosition: Boolean,
hasLocationPermission: Boolean,
dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None,
trackUserPosition: Boolean = false,
hasLocationPermission: Boolean = false,
canShareLiveLocation: Boolean = false,
appName: String = APP_NAME,
eventSink: (ShareLocationEvent) -> Unit = {},
): ShareLocationState {
return ShareLocationState(
currentUser = currentUser,
@@ -70,7 +72,7 @@ private fun aShareLocationState(
trackUserLocation = trackUserPosition,
hasLocationPermission = hasLocationPermission,
canShareLiveLocation = canShareLiveLocation,
appName = APP_NAME,
eventSink = {}
appName = appName,
eventSink = eventSink
)
}

View File

@@ -20,22 +20,25 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.location.AssetType
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.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -49,9 +52,12 @@ class ShareLocationPresenterTest {
private val fakeMessageComposerContext = FakeMessageComposerContext()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val fakeFeatureFlagService = FakeFeatureFlagService()
private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID)
private fun createShareLocationPresenter(
joinedRoom: JoinedRoom = FakeJoinedRoom(),
locationActions: FakeLocationActions = fakeLocationActions,
): ShareLocationPresenter = ShareLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
@@ -60,13 +66,14 @@ class ShareLocationPresenterTest {
timelineMode = Timeline.Mode.Live,
analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext,
locationActions = fakeLocationActions,
locationActions = locationActions,
buildMeta = fakeBuildMeta,
featureFlagService = fakeFeatureFlagService,
client = fakeMatrixClient,
)
@Test
fun `initial state with permissions granted`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fun `initial state with permissions granted and location enabled`() = runTest {
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
@@ -74,25 +81,18 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(initialState.mode).isEqualTo(ShareLocationState.Mode.SenderLocation)
assertThat(initialState.hasLocationPermission).isTrue()
// Swipe the map to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToPinLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isTrue()
val shareLocationPresenter = createShareLocationPresenter()
shareLocationPresenter.test {
skipItems(1)
val state = awaitItem()
assertThat(state.trackUserLocation).isTrue()
assertThat(state.hasLocationPermission).isTrue()
assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
}
}
@Test
fun `initial state with permissions partially granted`() = runTest {
fun `initial state with permissions partially granted and location enabled`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
@@ -104,17 +104,11 @@ class ShareLocationPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(initialState.mode).isEqualTo(ShareLocationState.Mode.SenderLocation)
assertThat(initialState.trackUserLocation).isTrue()
assertThat(initialState.hasLocationPermission).isTrue()
// Swipe the map to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToPinLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isTrue()
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None))
}
}
@@ -131,22 +125,18 @@ class ShareLocationPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(initialState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(initialState.trackUserLocation).isFalse()
assertThat(initialState.hasLocationPermission).isFalse()
// Click on the button to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.PermissionDenied)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isFalse()
assertThat(initialState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied)
)
}
}
@Test
fun `initial state with permissions denied once`() = runTest {
fun `initial state with permissions denied with rationale`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
@@ -155,25 +145,62 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(initialState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(initialState.trackUserLocation).isFalse()
assertThat(initialState.hasLocationPermission).isFalse()
// Click on the button to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.PermissionRationale)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isFalse()
assertThat(initialState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale)
)
}
}
@Test
fun `rationale dialog dismiss`() = runTest {
fun `initial state with location services disabled`() = runTest {
val locationActions = FakeLocationActions(isLocationEnabled = false)
val shareLocationPresenter = createShareLocationPresenter(locationActions = locationActions)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
)
)
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.trackUserLocation).isFalse()
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled)
)
}
}
@Test
fun `StopTrackingUserLocation event sets trackUserLocation to false`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
)
)
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.trackUserLocation).isTrue()
initialState.eventSink(ShareLocationEvent.StopTrackingUserLocation)
val stoppedState = awaitItem()
assertThat(stoppedState.trackUserLocation).isFalse()
}
}
@Test
fun `DismissDialog event clears dialog state`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
@@ -182,30 +209,21 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale)
)
// Click on the button to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.PermissionRationale)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isFalse()
// Dismiss the dialog
myLocationState.eventSink(ShareLocationEvent.DismissDialog)
val dialogDismissedState = awaitItem()
assertThat(dialogDismissedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(dialogDismissedState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
initialState.eventSink(ShareLocationEvent.DismissDialog)
val dismissedState = awaitItem()
assertThat(dismissedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
}
}
@Test
fun `rationale dialog continue`() = runTest {
fun `RequestPermissions event triggers permission request`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
@@ -214,27 +232,20 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
shareLocationPresenter.test {
val initialState = awaitItem()
initialState.eventSink(ShareLocationEvent.RequestPermissions)
// Click on the button to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.PermissionRationale)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isFalse()
// Wait for dialog to be dismissed
awaitItem()
// Continue the dialog sends permission request to the permissions presenter
myLocationState.eventSink(ShareLocationEvent.StartTrackingUserLocation)
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `permission denied dialog dismiss`() = runTest {
fun `OpenAppSettings event opens settings and clears dialog`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
@@ -243,31 +254,94 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink(ShareLocationEvent.OpenAppSettings)
val settingsOpenedState = awaitItem()
// Click on the button to switch mode
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val myLocationState = awaitItem()
assertThat(myLocationState.dialogState).isEqualTo(ShareLocationState.Dialog.PermissionDenied)
assertThat(myLocationState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(myLocationState.hasLocationPermission).isFalse()
// Dismiss the dialog
myLocationState.eventSink(ShareLocationEvent.DismissDialog)
val dialogDismissedState = awaitItem()
assertThat(dialogDismissedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(dialogDismissedState.mode).isEqualTo(ShareLocationState.Mode.PinLocation)
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
assertThat(settingsOpenedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
}
}
@Test
fun `share sender location`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, EventId?, Result<Unit>> { _, _, _, _, _, _ ->
fun `OpenLocationSettings event opens location settings and clears dialog`() = runTest {
val locationActions = FakeLocationActions(isLocationEnabled = false)
val shareLocationPresenter = createShareLocationPresenter(locationActions = locationActions)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
)
)
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled)
)
initialState.eventSink(ShareLocationEvent.OpenLocationSettings)
val settingsOpenedState = awaitItem()
assertThat(settingsOpenedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(locationActions.openLocationSettingsInvocationsCount).isEqualTo(1)
}
}
@Test
fun `ShowLiveLocationDurationPicker shows duration dialog when constraints pass`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
)
)
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
val durationDialogState = awaitItem()
assertThat(durationDialogState.dialogState).isEqualTo(ShareLocationState.Dialog.LiveLocationDuration)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `ShowLiveLocationDurationPicker shows constraint dialog when permissions denied`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
// Dismiss initial dialog
initialState.eventSink(ShareLocationEvent.DismissDialog)
val dismissedState = awaitItem()
dismissedState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
val constraintDialogState = awaitItem()
assertThat(constraintDialogState.dialogState).isEqualTo(
ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied)
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `ShareStaticLocation sends user location`() = runTest {
val sendLocationResult = lambdaRecorder { _: String, _: String, _: String?, _: Int?, _: AssetType?, _: EventId? ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
@@ -283,29 +357,18 @@ class ShareLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
shareLocationPresenter.test {
skipItems(1)
val initialState = awaitItem()
// Send location
initialState.eventSink(
ShareLocationEvent.ShareStaticLocation(
cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition(
lat = 0.0,
lon = 1.0,
zoom = 2.0,
),
location = Location(
lat = 3.0,
lon = 4.0,
accuracy = 5.0f,
)
location = Location(lat = 3.0, lon = 4.0, accuracy = 5.0f),
isPinned = false,
)
)
delay(1) // Wait for the coroutine to finish
advanceUntilIdle()
sendLocationResult.assertions().isCalledOnce()
.with(
@@ -326,12 +389,13 @@ class ShareLocationPresenterTest {
messageType = Composer.MessageType.LocationUser,
)
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `share pin location`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, EventId?, Result<Unit>> { _, _, _, _, _, _ ->
fun `ShareStaticLocation sends pinned location`() = runTest {
val sendLocationResult = lambdaRecorder { _: String, _: String, _: String?, _: Int?, _: AssetType?, _: EventId? ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
@@ -342,39 +406,26 @@ class ShareLocationPresenterTest {
val shareLocationPresenter = createShareLocationPresenter(joinedRoom)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
)
)
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
shareLocationPresenter.test {
val initialState = awaitItem()
// Send location
initialState.eventSink(
ShareLocationEvent.ShareStaticLocation(
cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition(
lat = 0.0,
lon = 1.0,
zoom = 2.0,
),
location = Location(
lat = 3.0,
lon = 4.0,
accuracy = 5.0f,
)
location = Location(lat = 1.0, lon = 2.0, accuracy = 3.0f),
isPinned = true,
)
)
delay(1) // Wait for the coroutine to finish
advanceUntilIdle()
sendLocationResult.assertions().isCalledOnce()
.with(
value("Location was shared at geo:0.0,1.0"),
value("geo:0.0,1.0"),
value("Location was shared at geo:1.0,2.0;u=3.0"),
value("geo:1.0,2.0;u=3.0"),
value(null),
value(15),
value(AssetType.PIN),
@@ -390,107 +441,7 @@ class ShareLocationPresenterTest {
messageType = Composer.MessageType.LocationPin,
)
)
}
}
@Test
fun `composer context passes through analytics`() = runTest {
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, EventId?, Result<Unit>> { _, _, _, _, _, _ ->
Result.success(Unit)
}
val joinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
sendLocationLambda = sendLocationResult
},
)
val shareLocationPresenter = createShareLocationPresenter(joinedRoom)
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit(
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
content = ""
)
}
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
// Send location
initialState.eventSink(
ShareLocationEvent.ShareStaticLocation(
cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition(
lat = 0.0,
lon = 1.0,
zoom = 2.0,
),
location = null
)
)
delay(1) // Wait for the coroutine to finish
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.LocationPin,
)
)
}
}
@Test
fun `open settings activity`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
fakeMessageComposerContext.apply {
composerMode = MessageComposerMode.Edit(
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
content = ""
)
}
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
initialState.eventSink(ShareLocationEvent.SwitchToMyLocationMode)
val dialogShownState = awaitItem()
// Open settings
dialogShownState.eventSink(ShareLocationEvent.OpenAppSettings)
val settingsOpenedState = awaitItem()
assertThat(settingsOpenedState.dialogState).isEqualTo(ShareLocationState.Dialog.None)
assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
}
}
@Test
fun `application name is in state`() = runTest {
val shareLocationPresenter = createShareLocationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
shareLocationPresenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo("app name")
cancelAndIgnoreRemainingEvents()
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.location.impl.share
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ShareLocationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `test back action`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setShareLocationView(
state = aShareLocationState(
eventSink = eventsRecorder
),
navigateUp = callback,
)
rule.pressBack()
}
}
@Test
fun `test fab click`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick()
eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation)
}
@Test
fun `when permission denied is displayed user can open the settings`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings)
}
@Test
fun `when permission denied is displayed user can close the dialog`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
}
@Test
fun `when permission rationale is displayed user can request permissions`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions)
}
@Test
fun `when permission rationale is displayed user can close the dialog`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
}
@Test
fun `when location service disabled is displayed user can open location settings`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
hasLocationPermission = true,
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings)
}
@Test
fun `when location service disabled is displayed user can close the dialog`() {
val eventsRecorder = EventsRecorder<ShareLocationEvent>()
rule.setShareLocationView(
aShareLocationState(
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
hasLocationPermission = true,
eventSink = eventsRecorder
),
navigateUp = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShareLocationView(
state: ShareLocationState,
navigateUp: () -> Unit = EnsureNeverCalled(),
) {
setContent {
// Simulate a LocalInspectionMode for MapLibreMap
CompositionLocalProvider(LocalInspectionMode provides true) {
ShareLocationView(
state = state,
navigateUp = navigateUp,
)
}
}
}

View File

@@ -13,8 +13,11 @@ import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationEntryPoint
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.node.TestParentNode
@@ -32,21 +35,27 @@ class DefaultShowLocationEntryPointTest {
ShowLocationNode(
buildContext = buildContext,
plugins = plugins,
presenterFactory = { location: Location, description: String? ->
ShowLocationPresenter(
presenterFactory = object : ShowLocationPresenter.Factory {
override fun create(mode: ShowLocationMode) = ShowLocationPresenter(
mode = mode,
permissionsPresenterFactory = { FakePermissionsPresenter() },
locationActions = FakeLocationActions(),
buildMeta = aBuildMeta(),
location = location,
description = description,
dateFormatter = FakeDateFormatter(),
)
},
analyticsService = FakeAnalyticsService(),
)
}
val inputs = ShowLocationEntryPoint.Inputs(
location = Location(37.4219983, -122.084, 10f),
description = "My location",
mode = ShowLocationMode.Static(
location = Location(37.4219983, -122.084, 10f),
senderName = "Alice",
senderId = UserId("@alice:matrix.org"),
senderAvatarUrl = null,
timestamp = System.currentTimeMillis(),
assetType = null,
),
)
val result = entryPoint.createNode(
parentNode = parentNode,

View File

@@ -24,6 +24,7 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -66,9 +67,8 @@ class ShowLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isFalse()
assertThat(initialState.isTrackMyLocation).isFalse()
@@ -84,9 +84,8 @@ class ShowLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isFalse()
assertThat(initialState.isTrackMyLocation).isFalse()
@@ -97,9 +96,8 @@ class ShowLocationPresenterTest {
fun `emits initial state with location permission`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.isTrackMyLocation).isFalse()
@@ -110,9 +108,8 @@ class ShowLocationPresenterTest {
fun `emits initial state with partial location permission`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.isTrackMyLocation).isFalse()
@@ -121,9 +118,8 @@ class ShowLocationPresenterTest {
@Test
fun `uses action to share location`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.Share(location))
@@ -135,9 +131,8 @@ class ShowLocationPresenterTest {
fun `centers on user location`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.isTrackMyLocation).isFalse()
@@ -168,9 +163,8 @@ class ShowLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
// Skip initial state
val initialState = awaitItem()
@@ -198,10 +192,8 @@ class ShowLocationPresenterTest {
shouldShowRationale = true,
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
// Skip initial state
val initialState = awaitItem()
@@ -227,9 +219,8 @@ class ShowLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
// Skip initial state
val initialState = awaitItem()
@@ -258,9 +249,8 @@ class ShowLocationPresenterTest {
)
)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter().present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
// Skip initial state
val initialState = awaitItem()
@@ -291,9 +281,8 @@ class ShowLocationPresenterTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakeLocationActions.givenLocationEnabled(false)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter(locationActions = fakeLocationActions).present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
@@ -311,9 +300,8 @@ class ShowLocationPresenterTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakeLocationActions.givenLocationEnabled(false)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter(locationActions = fakeLocationActions).present()
}.test {
val presenter = createShowLocationPresenter()
presenter.test {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))