Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.github.warnastrophy.core.ui.map.MockPermissionManager
import com.github.warnastrophy.core.util.AppConfig
import com.github.warnastrophy.core.util.BaseAndroidComposeTest
import com.github.warnastrophy.userPrefsDataStore
import kotlin.collections.isNotEmpty
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
Expand All @@ -35,10 +36,9 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
fun setup() {
val context = composeTestRule.activity.applicationContext
UserPreferencesRepositoryProvider.initLocal(context.userPrefsDataStore)
// Initialize the ActivityRepositoryProvider with mock for testing
mockActivityRepository = MockActivityRepository()
ActivityRepositoryProvider.useMock()
// Make sure user preferences are reset to defaults so tests are deterministic

runBlocking {
val repo = UserPreferencesRepositoryProvider.repository
repo.setAutoActionsEnabled(false)
Expand All @@ -49,12 +49,15 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
repo.setInactivityDetection(false)
repo.setAlertMode(false)
}
// Initialize services after prefs have been reset
InstrumentationRegistry.getInstrumentation().runOnMainSync { StateManagerService.init(context) }
StateManagerService.permissionManager =
MockPermissionManager(currentResult = PermissionResult.Granted)
StateManagerService.dangerModeService =
DangerModeService(permissionManager = StateManagerService.permissionManager)

val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.runOnMainSync { StateManagerService.init(context) }
instrumentation.runOnMainSync {
StateManagerService.permissionManager =
MockPermissionManager(currentResult = PermissionResult.Granted)
StateManagerService.dangerModeService =
DangerModeService(permissionManager = StateManagerService.permissionManager)
}
}

private fun createTestViewModel(repository: MockActivityRepository = mockActivityRepository) =
Expand Down Expand Up @@ -242,28 +245,26 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
val viewModel = createTestViewModel()
composeTestRule.setContent { MaterialTheme { DangerModeCard(viewModel = viewModel) } }

// Ensure the ViewModel starts with no capabilities enabled to make the test deterministic
viewModel.onCapabilitiesChanged(emptySet())
assert(viewModel.capabilitiesInternal.value.isEmpty())

// Advanced section should not be visible initially
composeTestRule
.onNodeWithTag(DangerModeTestTags.ADVANCED_SECTION, useUnmergedTree = true)
.assertDoesNotExist()

// Enable CALL capability deterministically via ViewModel
viewModel.onCapabilityToggled(DangerModeCapability.CALL)

// Wait for capabilities to be applied and advanced section to appear
val callCapabilityNode =
composeTestRule.onNodeWithTag(
DangerModeTestTags.capabilityTag(DangerModeCapability.CALL), useUnmergedTree = true)
callCapabilityNode.performClick()
composeTestRule.waitUntilWithTimeout {
viewModel.capabilitiesInternal.value.contains(DangerModeCapability.CALL)
}

composeTestRule
.onNodeWithTag(DangerModeTestTags.EXPAND_ARROW, useUnmergedTree = true)
.performClick()
composeTestRule
.onNodeWithTag(DangerModeTestTags.ADVANCED_SECTION, useUnmergedTree = true)
.assertExists()

// Wait for autoActions to be enabled by the ViewModel's logic and then assert confirmations are
// false
composeTestRule.waitUntilWithTimeout { viewModel.autoActionsEnabled.value }
assert(viewModel.autoActionsEnabled.value)
assert(!viewModel.confirmTouchRequired.value)
Expand All @@ -276,16 +277,17 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
val viewModel = createTestViewModel()
composeTestRule.setContent { MaterialTheme { DangerModeCard(viewModel = viewModel) } }

// Enable CALL capability to show advanced section deterministically via ViewModel
viewModel.onCapabilityToggled(DangerModeCapability.CALL)

// Wait until autoActions is enabled by the ViewModel
composeTestRule.waitUntilWithTimeout { viewModel.autoActionsEnabled.value }

composeTestRule
.onNodeWithTag(DangerModeTestTags.EXPAND_ARROW, useUnmergedTree = true)
.performClick()

assert(viewModel.autoActionsEnabled.value)
assert(!viewModel.confirmTouchRequired.value)
assert(!viewModel.confirmVoiceRequired.value)

// Toggle auto actions off and verify state updates correctly
viewModel.onAutoActionsEnabled(false)
composeTestRule.waitUntilWithTimeout { !viewModel.autoActionsEnabled.value }
assert(!viewModel.autoActionsEnabled.value)
Expand Down Expand Up @@ -318,11 +320,17 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
MaterialTheme { DangerModeCard(viewModel = viewModel) }
}

// Enable CALL capability to show advanced section
val callNode =
composeTestRule.onNodeWithTag(
DangerModeTestTags.capabilityTag(DangerModeCapability.CALL), useUnmergedTree = true)
callNode.performClick()
composeTestRule.waitUntilWithTimeout {
viewModel.capabilitiesInternal.value.contains(DangerModeCapability.CALL)
}

composeTestRule
.onNodeWithTag(DangerModeTestTags.EXPAND_ARROW, useUnmergedTree = true)
.performClick()

val confirmTouchSwitch =
composeTestRule.onNodeWithTag(
Expand All @@ -331,12 +339,10 @@ class DangerModeCardTest : BaseAndroidComposeTest() {
composeTestRule.onNodeWithTag(
DangerModeTestTags.CONFIRM_VOICE_SWITCH, useUnmergedTree = true)

// Toggle voice -> voice on, touch off
confirmVoiceSwitch.performClick()
confirmVoiceSwitch.assertIsOn()
confirmTouchSwitch.assertIsOff()

// Toggle touch -> touch on, voice off
confirmTouchSwitch.performClick()
confirmTouchSwitch.assertIsOn()
confirmVoiceSwitch.assertIsOff()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,13 @@ class LatestNewsCardTest : BaseAndroidComposeTest() {

Assert.assertEquals(expectedUrl, capturedUrl.get())
}

@Test
fun article_with_no_url_link_not_displayed() {
hazardService.setHazards(listOf(no_url_hazard))
composeTestRule.setContent { MaterialTheme { LatestNewsCard(hazardService) } }
composeTestRule
.onNodeWithTag(LatestNewsTestTags.LINK, useUnmergedTree = true)
.assertIsNotDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,29 @@ class MapScreenTest : BaseAndroidComposeTest() {
// We don’t have a direct tag, but rendering without crash covers all lines
composeTestRule.waitForIdle()
}

@Test
fun search_bar_cleared_when_clear_button_clicked() {
setContent()
applyPerm(PermissionResult.Granted)
waitForMapReadyAndAssertVisibility()

val testInput = "Test Location"
val textField = composeTestRule.onNodeWithTag(MapScreenTestTags.SEARCH_BAR_TEXT_FIELD)

textField.assertIsDisplayed()
textField.performClick()
textField.performTextInput(testInput)
textField.assert(hasText(testInput))

val clearBtn = composeTestRule.onNodeWithTag(MapScreenTestTags.SEARCH_BAR_CLEAR_BUTTON)
clearBtn.assertIsDisplayed()
clearBtn.performClick()
composeTestRule.waitForIdle()

// Vérifications après clear
textField.assert(hasText(""))
composeTestRule.onNodeWithTag(MapScreenTestTags.SEARCH_BAR_CLEAR_BUTTON).assertIsNotDisplayed()
composeTestRule.onNodeWithTag(MapScreenTestTags.SEARCH_BAR_DROPDOWN).assertIsNotDisplayed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ sealed class AppPermissions(val key: String, vararg permissionsWithNulls: String
object ForegroundServiceLocation :
AppPermissions(
key = "foreground_service_location",
if (isAtLeastSdkVersion(34)) {
Manifest.permission.FOREGROUND_SERVICE_LOCATION
Manifest.permission.POST_NOTIFICATIONS
} else {
null
})
// Add FOREGROUND_SERVICE_LOCATION on API 34+, and POST_NOTIFICATIONS on TIRAMISU+ as
// needed.
if (isAtLeastSdkVersion(34)) Manifest.permission.FOREGROUND_SERVICE_LOCATION else null,
if (isAtLeastSdkVersion(Build.VERSION_CODES.TIRAMISU))
Manifest.permission.POST_NOTIFICATIONS
else null)

/** Permissions required to send emergency messages */
object SendEmergencySms :
Expand All @@ -83,6 +83,9 @@ sealed class AppPermissions(val key: String, vararg permissionsWithNulls: String
Manifest.permission.ACCESS_FINE_LOCATION,
)

object MicrophonePermission :
AppPermissions(key = "microphone_permission", Manifest.permission.RECORD_AUDIO)

companion object {
/** A reusable check for SDK versions, annotated to help the compiler with smart casting. */
@ChecksSdkIntAtLeast(parameter = 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.github.warnastrophy.core.ui.features.dashboard

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -11,17 +14,19 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
Expand All @@ -46,6 +51,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.github.warnastrophy.R
import com.github.warnastrophy.core.data.service.DangerLevel
import com.github.warnastrophy.core.model.Activity
import com.github.warnastrophy.core.permissions.AppPermissions
import com.github.warnastrophy.core.ui.components.ActivityFallback
import com.github.warnastrophy.core.ui.components.StandardDashboardButton
import com.github.warnastrophy.core.ui.components.StandardDashboardCard
Expand All @@ -70,6 +76,7 @@ object DangerModeTestTags {
const val CONFIRM_TOUCH_SWITCH = "dangerModeConfirmTouchSwitch"
const val CONFIRM_VOICE_SWITCH = "dangerModeConfirmVoiceSwitch"
const val AUTO_CALL_SWITCH = "dangerModeAutoActionsSwitch"
const val EXPAND_ARROW = "dangerModeExpandArrow"

fun capabilityTag(capability: DangerModeCapability) = CAPABILITY_PREFIX + capability.label

Expand Down Expand Up @@ -108,17 +115,26 @@ fun DangerModeCard(
return
}

// Track which permission set is being requested
var pendingPermission by remember { mutableStateOf<AppPermissions?>(null) }

val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(),
onResult = { viewModel.onPermissionResult(activity = activity) })
onResult = {
pendingPermission?.let { permissionType ->
viewModel.onPermissionResult(activity = activity, permissionType = permissionType)
}
pendingPermission = null
})

LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
Effect.RequestLocationPermission -> {
viewModel.onPermissionsRequestStart()
launcher.launch(viewModel.alertModePermission.permissions)
is Effect.RequestPermissions -> {
pendingPermission = effect.permissionType
viewModel.onPermissionsRequestStart(effect.permissionType)
launcher.launch(effect.permissionType.permissions)
}
Effect.StartForegroundService -> startForegroundGpsService(activity)
Effect.StopForegroundService -> stopForegroundGpsService(context)
Expand All @@ -134,19 +150,27 @@ fun DangerModeCard(

val isDangerModeEnabled by viewModel.isDangerModeEnabled.collectAsState(false)

// State for expanding/collapsing advanced options
var isExpanded by remember { mutableStateOf(false) }

StandardDashboardCard(
modifier = modifier.fillMaxWidth().testTag(DangerModeTestTags.CARD),
backgroundColor = MaterialTheme.colorScheme.error,
borderColor = MaterialTheme.colorScheme.error,
) {
Column(modifier = Modifier.padding(16.dp)) {
// reduced vertical padding to make the card shorter on screen
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 0.dp)) {
DangerModeHeader(
isDangerModeEnabled = isDangerModeEnabled,
onCheckedChange = {
viewModel.handleToggle(it, viewModel.permissionUiState.value.alertModePermissionResult)
})
onCheckedChange = { viewModel.handleToggle(it, activity) })

DangerModeBody(
viewModel = viewModel,
onManageActivitiesClick = onManageActivitiesClick,
isExpanded = isExpanded)

DangerModeBody(viewModel, onManageActivitiesClick)
// Expand/Collapse arrow at the bottom
ExpandArrow(isExpanded = isExpanded, onToggle = { isExpanded = !isExpanded })
}
}
}
Expand All @@ -162,7 +186,7 @@ fun DangerModeCard(
@Composable
private fun DangerModeHeader(isDangerModeEnabled: Boolean, onCheckedChange: (Boolean) -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().offset(y = (-10).dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(
Expand Down Expand Up @@ -190,11 +214,14 @@ private fun DangerModeHeader(isDangerModeEnabled: Boolean, onCheckedChange: (Boo
* @param viewModel The ViewModel that provides state for the UI and handles user events.
* @param onManageActivitiesClick A lambda function to be invoked when the user clicks the "Manage"
* button, typically to navigate to a screen for managing activities.
* @param isExpanded A boolean state that controls whether the advanced options section is expanded
* or collapsed.
*/
@Composable
private fun DangerModeBody(
viewModel: DangerModeCardViewModel,
onManageActivitiesClick: () -> Unit
onManageActivitiesClick: () -> Unit,
isExpanded: Boolean
) {
val currentActivity by viewModel.currentActivity.collectAsState(null)
val activities by viewModel.activities.collectAsState()
Expand All @@ -219,19 +246,33 @@ private fun DangerModeBody(
CapabilitiesRow(
capabilities = capabilities, onCapabilityToggled = { viewModel.onCapabilityToggled(it) })

if (capabilities.contains(DangerModeCapability.CALL) ||
capabilities.contains(DangerModeCapability.SMS)) {
Spacer(modifier = Modifier.height(12.dp))
DangerModeAdvancedOptionsSection(
autoActionsEnabled = autoActionsEnabled,
confirmTouchRequired = confirmTouchRequired,
confirmVoiceRequired = confirmVoiceRequired,
onAutoActionsChanged = viewModel::onAutoActionsEnabled,
onConfirmTouchChanged = viewModel::onConfirmTouchChanged,
onConfirmVoiceChanged = viewModel::onConfirmVoiceChanged)
// Advanced section is shown when expanded
AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) {
Column {
// reduced spacer to shrink height around the expand arrow
DangerModeAdvancedOptionsSection(
autoActionsEnabled = autoActionsEnabled,
confirmTouchRequired = confirmTouchRequired,
confirmVoiceRequired = confirmVoiceRequired,
onAutoActionsChanged = viewModel::onAutoActionsEnabled,
onConfirmTouchChanged = viewModel::onConfirmTouchChanged,
onConfirmVoiceChanged = viewModel::onConfirmVoiceChanged)
}
}
}

Spacer(modifier = Modifier.height(4.dp))
/** A small arrow at the bottom of the card to expand/collapse advanced options. */
@Composable
private fun ExpandArrow(isExpanded: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = onToggle, modifier = Modifier.testTag(DangerModeTestTags.EXPAND_ARROW)) {
Icon(
imageVector =
if (isExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
contentDescription = if (isExpanded) "Collapse" else "Expand",
tint = MaterialTheme.colorScheme.onError)
}
}
}

/**
Expand Down
Loading
Loading