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.material.icons.filled.Search
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.NavIcon
|
import com.m57.hermescontrol.ui.common.NavIcon
|
||||||
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
|
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsViewModel
|
||||||
import com.m57.hermescontrol.util.ConnectorUrlValidator
|
import com.m57.hermescontrol.util.ConnectorUrlValidator
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -160,6 +162,8 @@ fun ChatScreen(
|
|||||||
val connectorsState by connectorsViewModel.uiState.collectAsStateWithLifecycle()
|
val connectorsState by connectorsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val actionProgressState by viewModel.actionProgress.state.collectAsStateWithLifecycle()
|
val actionProgressState by viewModel.actionProgress.state.collectAsStateWithLifecycle()
|
||||||
val connectionOperationState by viewModel.connectionOperationState.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
|
// Snapshot-backed search state — read directly so only the scopes that
|
||||||
// read its fields recompose on search changes (bar, matched bubbles).
|
// read its fields recompose on search changes (bar, matched bubbles).
|
||||||
val searchState = viewModel.searchState
|
val searchState = viewModel.searchState
|
||||||
@@ -539,7 +543,10 @@ fun ChatScreen(
|
|||||||
var showSessionMenu by remember { mutableStateOf(false) }
|
var showSessionMenu by remember { mutableStateOf(false) }
|
||||||
Box {
|
Box {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showSessionMenu = true },
|
onClick = {
|
||||||
|
settingsViewModel.refreshKeepConnectedInBackground()
|
||||||
|
showSessionMenu = true
|
||||||
|
},
|
||||||
modifier = Modifier.testTag("chat_session_menu_button"),
|
modifier = Modifier.testTag("chat_session_menu_button"),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -609,6 +616,10 @@ fun ChatScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.testTag("chat_menu_session_integrations"),
|
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)
|
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. */
|
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||||
fun logout() {
|
fun logout() {
|
||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
|
|||||||
@@ -383,6 +383,34 @@ class SettingsViewModelTest {
|
|||||||
verify { AuthManager.setKeepConnectedInBackground(false) }
|
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
|
@Test
|
||||||
fun testSave_persistsKeepConnectedInBackground() =
|
fun testSave_persistsKeepConnectedInBackground() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
Reference in new issue
Block a user