Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20442ee
feat: add microphone permission in danger mode preferences
ssidimoto Dec 14, 2025
c6634d2
feat: add microphone permission in repository and in local storage
ssidimoto Dec 14, 2025
a98ebda
refactor: adapt test given new permission
ssidimoto Dec 15, 2025
f55f2f6
fix: fix end2end tests
ssidimoto Dec 15, 2025
34a874b
merge: merge main
ssidimoto Dec 15, 2025
41371e3
fix: fix end2end tests
ssidimoto Dec 16, 2025
eec716b
merge: merge main
ssidimoto Dec 16, 2025
24fd8ee
merge: merge main
ssidimoto Dec 17, 2025
f3e7776
fix: fix end2end tests
ssidimoto Dec 17, 2025
41fa89e
refactor: display only hazards with article url in latest news
ssidimoto Dec 17, 2025
735f593
refactor: add downward arrow for additional danger mode card options
ssidimoto Dec 17, 2025
3364220
refactor: adapt test with changes in danger card
ssidimoto Dec 18, 2025
1cb2b29
fix: fix search bar bug where can't clear message
ssidimoto Dec 18, 2025
d9808be
fix: fix user id resolution in orchestrator to fetch contacts
ssidimoto Dec 18, 2025
e843498
Merge branch 'main' into refactor/small-improvements
ssidimoto Dec 18, 2025
0de1a8b
merge: merge main
ssidimoto Dec 18, 2025
00f5b2b
erge: merge main
ssidimoto Dec 18, 2025
ad375b1
merge: merge main
ssidimoto Dec 18, 2025
b68b9cd
fix: fix tests in dangercardviewmodel
ssidimoto Dec 18, 2025
6ae85ee
Merge branch 'main' into refactor/small-improvements
ssidimoto Dec 18, 2025
f748965
fix: fix onboarding screen
ssidimoto Dec 18, 2025
8e2529d
Merge branch 'refactor/small-improvements' into feature/microphone-au…
ssidimoto Dec 18, 2025
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
@@ -1,5 +1,6 @@
package com.github.warnastrophy.core.ui.profile.preferences

import android.content.Context
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
Expand All @@ -15,7 +16,14 @@ import androidx.compose.ui.test.performScrollToNode
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.warnastrophy.core.data.interfaces.UserPreferencesRepository
import com.github.warnastrophy.core.data.service.DangerModeOrchestrator
import com.github.warnastrophy.core.data.service.DangerModeService
import com.github.warnastrophy.core.data.service.GpsService
import com.github.warnastrophy.core.data.service.HazardsDataService
import com.github.warnastrophy.core.data.service.MovementService
import com.github.warnastrophy.core.data.service.StateManagerService
import com.github.warnastrophy.core.permissions.PermissionResult
import com.github.warnastrophy.core.ui.common.ErrorHandler
import com.github.warnastrophy.core.ui.components.FALLBACK_ACTIVITY_ERROR
import com.github.warnastrophy.core.ui.features.profile.preferences.DangerModePreferencesScreen
import com.github.warnastrophy.core.ui.features.profile.preferences.DangerModePreferencesScreenTestTags
Expand All @@ -24,6 +32,7 @@ import com.github.warnastrophy.core.ui.features.profile.preferences.PendingActio
import com.github.warnastrophy.core.ui.map.MockPermissionManager
import com.github.warnastrophy.core.ui.util.MockUserPreferencesRepository
import com.github.warnastrophy.core.util.BaseAndroidComposeTest
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNull
Expand All @@ -41,9 +50,23 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {

@Before
override fun setUp() {
val ctx: Context = mockk(relaxed = true)
super.setUp()
mockPermissionManager = MockPermissionManager()
userPreferencesRepository = MockUserPreferencesRepository()
val dangerModeService = mockk<DangerModeService>(relaxed = true)
val mouvementServiceMock = mockk<MovementService>(relaxed = true)
val errorHandlerMock = mockk<ErrorHandler>(relaxed = true)
val gpsServiceMock = mockk<GpsService>(relaxed = true)
val hazardServicemock = mockk<HazardsDataService>(relaxed = true)
val dangerModeOrchestrator = mockk<DangerModeOrchestrator>(relaxed = true)
StateManagerService.init(
gpsServiceMock,
hazardServicemock,
dangerModeService,
mouvementServiceMock,
errorHandlerMock,
dangerModeOrchestrator)
}

/** Helper function to set the content of the test rule with a configured ViewModel. */
Expand All @@ -67,6 +90,12 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {
.performScrollToNode(hasTestTag(DangerModePreferencesScreenTestTags.AUTOMATIC_CALLS_ITEM))
}

private fun scrollToMicrophoneAccess() {
composeTestRule
.onNodeWithTag(DangerModePreferencesScreenTestTags.SCROLL_CONTAINER)
.performScrollToNode(hasTestTag(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS))
}

/** Applies a given [PermissionResult] to the view model. */
private fun applyPerm(permissionResult: PermissionResult) {
mockPermissionManager.setPermissionResult(permissionResult)
Expand All @@ -87,6 +116,7 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_SMS_SWITCH).assertIsOff()
scrollToAutomaticCalls()
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_CALLS_SWITCH).assertIsOff()
nodeWithTag(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS_SWITCH).assertIsOff()
}

/** Verifies that a fallback error message is shown when the context is not an Activity. */
Expand Down Expand Up @@ -120,6 +150,7 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_SMS_SWITCH).assertIsNotEnabled()
scrollToAutomaticCalls()
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_CALLS_SWITCH).assertIsNotEnabled()
nodeWithTag(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS_SWITCH).assertIsNotEnabled()
}

@Test
Expand All @@ -135,6 +166,7 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_SMS_SWITCH).assertIsNotEnabled()
scrollToAutomaticCalls()
nodeWithTag(DangerModePreferencesScreenTestTags.AUTOMATIC_CALLS_SWITCH).assertIsNotEnabled()
nodeWithTag(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS_SWITCH).assertIsNotEnabled()
}

/**
Expand Down Expand Up @@ -286,5 +318,32 @@ class DangerModePreferencesScreenTest : BaseAndroidComposeTest() {
}
}

@Test
fun microphoneAccessToggle_requestsPermission_whenNotGranted() {
mockPermissionManager.setPermissionResult(PermissionResult.Granted)
setContent()

toggleSwitch(
DangerModePreferencesScreenTestTags.ALERT_MODE_SWITCH,
{ viewModel.uiState.value.alertModeAutomaticEnabled })
toggleSwitch(
DangerModePreferencesScreenTestTags.INACTIVITY_DETECTION_SWITCH,
{ viewModel.uiState.value.inactivityDetectionEnabled })

mockPermissionManager.setPermissionResult(PermissionResult.Denied(listOf("FAKE_PERMISSION")))

// Simulate toggle the switch
nodeWithTag(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS_SWITCH)
.assertIsEnabled()
.assertIsOff()

applyPerm(PermissionResult.Granted)

scrollToMicrophoneAccess()
toggleSwitch(DangerModePreferencesScreenTestTags.MICROPHONE_ACCESS_SWITCH) {
viewModel.uiState.value.microphoneAccessEnabled
}
}

private fun nodeWithTag(tag: String) = composeTestRule.onNodeWithTag(tag, useUnmergedTree = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class MockUserPreferencesRepository : UserPreferencesRepository {
inactivityDetection = false,
automaticSms = false,
automaticCalls = false,
microphoneAccess = false,
autoActionsEnabled = false,
touchConfirmationRequired = false,
voiceConfirmationEnabled = false),
Expand Down Expand Up @@ -55,6 +56,13 @@ class MockUserPreferencesRepository : UserPreferencesRepository {
dangerModePreferences = current.dangerModePreferences.copy(automaticCalls = enabled))
}

override suspend fun setMicrophoneAccess(enabled: Boolean) {
val current = _preferences.value
_preferences.value =
current.copy(
dangerModePreferences = current.dangerModePreferences.copy(microphoneAccess = enabled))
}

override suspend fun setAutoActionsEnabled(enabled: Boolean) {
val current = _preferences.value
_preferences.value =
Expand All @@ -79,5 +87,8 @@ class MockUserPreferencesRepository : UserPreferencesRepository {
current.dangerModePreferences.copy(voiceConfirmationEnabled = enabled))
}

override suspend fun setDarkMode(isDark: Boolean) {}
override suspend fun setDarkMode(isDark: Boolean) {
val current = _preferences.value
_preferences.value = current.copy(themePreferences = isDark)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ interface UserPreferencesRepository {
*/
suspend fun setAutomaticCalls(enabled: Boolean)

/*
* Enables or disables microphone access for the app.
*/
suspend fun setMicrophoneAccess(enabled: Boolean)

/** Enables or disables automatic emergency actions as a whole. */
suspend fun setAutoActionsEnabled(enabled: Boolean)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class HybridUserPreferencesRepository(
updateBothRepositories { setVoiceConfirmationEnabled(enabled) }
}

override suspend fun setMicrophoneAccess(enabled: Boolean) {
updateBothRepositories { setMicrophoneAccess(enabled) }
}
/**
* Updates both local and remote repositories to set the dark mode preference.
*
Expand Down Expand Up @@ -148,6 +151,7 @@ class HybridUserPreferencesRepository(
remotePrefs.dangerModePreferences.touchConfirmationRequired)
setVoiceConfirmationEnabled(remotePrefs.dangerModePreferences.voiceConfirmationEnabled)
setDarkMode(remotePrefs.themePreferences)
setMicrophoneAccess(remotePrefs.dangerModePreferences.microphoneAccess)
}
}
isRemoteAvailable = true
Expand Down
Loading
Loading