4 files changed
+112
-1
No files matched your search
+47
@@ -0,0 +1,47 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChatBackgroundConnectionMenuItemTest {
|
||||
@get:Rule
|
||||
val compose = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun menuReflectsStateAndTogglesInBothDirections() {
|
||||
var enabled by mutableStateOf(false)
|
||||
compose.setContent {
|
||||
ChatBackgroundConnectionMenuItem(
|
||||
checked = enabled,
|
||||
onToggle = { enabled = it },
|
||||
)
|
||||
}
|
||||
|
||||
compose.onNodeWithTag("chat_menu_keep_connected").assertIsDisplayed()
|
||||
compose.onNodeWithTag("chat_menu_keep_connected_checkbox", useUnmergedTree = true).assertIsOff()
|
||||
compose.onNodeWithTag("chat_menu_keep_connected").performClick()
|
||||
compose.onNodeWithTag("chat_menu_keep_connected_checkbox", useUnmergedTree = true).assertIsOn()
|
||||
compose.runOnIdle { assertEquals(true, enabled) }
|
||||
|
||||
// The next opening reads the externally changed value rather than a stale menu snapshot.
|
||||
compose.runOnIdle { enabled = false }
|
||||
compose.onNodeWithTag("chat_menu_keep_connected_checkbox", useUnmergedTree = true).assertIsOff()
|
||||
compose.runOnIdle { enabled = true }
|
||||
compose.onNodeWithTag("chat_menu_keep_connected_checkbox", useUnmergedTree = true).performClick()
|
||||
compose.onNodeWithTag("chat_menu_keep_connected_checkbox", useUnmergedTree = true).assertIsOff()
|
||||
compose.runOnIdle { assertEquals(false, enabled) }
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -117,6 +118,7 @@ import com.m57.hermescontrol.ui.common.CredentialWarningBanner
|
||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
|
||||
import com.m57.hermescontrol.ui.settings.SettingsViewModel
|
||||
import com.m57.hermescontrol.util.ConnectorUrlValidator
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -160,6 +162,8 @@ fun ChatScreen(
|
||||
val connectorsState by connectorsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val actionProgressState by viewModel.actionProgress.state.collectAsStateWithLifecycle()
|
||||
val connectionOperationState by viewModel.connectionOperationState.collectAsStateWithLifecycle()
|
||||
val settingsViewModel: SettingsViewModel = viewModel()
|
||||
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
// Snapshot-backed search state — read directly so only the scopes that
|
||||
// read its fields recompose on search changes (bar, matched bubbles).
|
||||
val searchState = viewModel.searchState
|
||||
@@ -539,7 +543,10 @@ fun ChatScreen(
|
||||
var showSessionMenu by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showSessionMenu = true },
|
||||
onClick = {
|
||||
settingsViewModel.refreshKeepConnectedInBackground()
|
||||
showSessionMenu = true
|
||||
},
|
||||
modifier = Modifier.testTag("chat_session_menu_button"),
|
||||
) {
|
||||
Icon(
|
||||
@@ -609,6 +616,10 @@ fun ChatScreen(
|
||||
},
|
||||
modifier = Modifier.testTag("chat_menu_session_integrations"),
|
||||
)
|
||||
ChatBackgroundConnectionMenuItem(
|
||||
checked = settingsState.keepConnectedInBackground,
|
||||
onToggle = settingsViewModel::onKeepConnectedInBackgroundChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1029,3 +1040,22 @@ fun ChatScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ChatBackgroundConnectionMenuItem(
|
||||
checked: Boolean,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.settings_keep_connected_in_background_title)) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onToggle,
|
||||
modifier = Modifier.testTag("chat_menu_keep_connected_checkbox"),
|
||||
)
|
||||
},
|
||||
onClick = { onToggle(!checked) },
|
||||
modifier = Modifier.testTag("chat_menu_keep_connected"),
|
||||
)
|
||||
}
|
||||
@@ -376,6 +376,12 @@ class SettingsViewModel(
|
||||
AuthManager.setKeepConnectedInBackground(enabled)
|
||||
}
|
||||
|
||||
/** Reconcile the chat shortcut with changes made through Settings or another screen. */
|
||||
fun refreshKeepConnectedInBackground() {
|
||||
val enabled = AuthManager.isKeepConnectedInBackground()
|
||||
_uiState.update { it.copy(keepConnectedInBackground = enabled) }
|
||||
}
|
||||
|
||||
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||
fun logout() {
|
||||
AuthManager.setToken(null)
|
||||
|
||||
@@ -383,6 +383,34 @@ class SettingsViewModelTest {
|
||||
verify { AuthManager.setKeepConnectedInBackground(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKeepConnectedInBackground_refreshesBothDirectionsFromPersistedSetting() =
|
||||
runTest {
|
||||
var persisted = false
|
||||
every { AuthManager.isKeepConnectedInBackground() } answers { persisted }
|
||||
every { AuthManager.setKeepConnectedInBackground(any()) } answers {
|
||||
persisted = firstArg()
|
||||
}
|
||||
val viewModel = SettingsViewModel(ioDispatcher = testDispatcher)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
// An edit from Settings becomes visible when chat opens its menu.
|
||||
persisted = true
|
||||
viewModel.refreshKeepConnectedInBackground()
|
||||
assertEquals(true, viewModel.uiState.value.keepConnectedInBackground)
|
||||
|
||||
// Both menu directions use the existing Settings action and persistence path.
|
||||
viewModel.onKeepConnectedInBackgroundChange(false)
|
||||
assertEquals(false, persisted)
|
||||
assertEquals(false, viewModel.uiState.value.keepConnectedInBackground)
|
||||
viewModel.onKeepConnectedInBackgroundChange(true)
|
||||
assertEquals(true, persisted)
|
||||
|
||||
persisted = false
|
||||
viewModel.refreshKeepConnectedInBackground()
|
||||
assertEquals(false, viewModel.uiState.value.keepConnectedInBackground)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSave_persistsKeepConnectedInBackground() =
|
||||
runTest {
|
||||
|
||||
Reference in new issue
Block a user