feat(chat): stabilize multi-image attachment picker (#986)
This commit is contained in:
1 parent
c7d7abf4a1
commit
0ae5ed4db2
11 files changed
+432
-55
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package com.m57.hermescontrol
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ExternalActivityLifecycleGuardTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
ExternalActivityLifecycleGuard.resetForTest()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
ExternalActivityLifecycleGuard.resetForTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun externalActivityLaunch_acquiresConnectionLeaseBeforePreparingServiceAndLaunching() {
|
||||
val calls = mutableListOf<String>()
|
||||
|
||||
ExternalActivityLifecycleGuard.launchExternalActivity(
|
||||
acquireConnectionLease = { calls += "acquire" },
|
||||
releaseConnectionLease = { calls += "release" },
|
||||
prepareForBackground = { calls += "prepare" },
|
||||
cleanupAfterLaunchFailure = { calls += "cleanup" },
|
||||
scheduleTimeout = { _, _ -> },
|
||||
launch = { calls += "launch" },
|
||||
)
|
||||
|
||||
assertEquals(listOf("acquire", "prepare", "launch"), calls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transientResumeDuringPicker_doesNotReleaseLeaseUntilResultReturns() {
|
||||
var released = false
|
||||
|
||||
ExternalActivityLifecycleGuard.launchExternalActivity(
|
||||
acquireConnectionLease = {},
|
||||
releaseConnectionLease = { released = true },
|
||||
prepareForBackground = {},
|
||||
cleanupAfterLaunchFailure = {},
|
||||
scheduleTimeout = { _, _ -> },
|
||||
launch = {},
|
||||
)
|
||||
ExternalActivityLifecycleGuard.onHostPaused()
|
||||
ExternalActivityLifecycleGuard.onHostResumed()
|
||||
|
||||
assertFalse(released)
|
||||
|
||||
ExternalActivityLifecycleGuard.onHostPaused()
|
||||
ExternalActivityLifecycleGuard.externalActivityReturned()
|
||||
|
||||
assertFalse(released)
|
||||
|
||||
ExternalActivityLifecycleGuard.onHostResumed()
|
||||
|
||||
assertTrue(released)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun abandonedPicker_timeoutReleasesConnectionLease() {
|
||||
var timeoutAction: (() -> Unit)? = null
|
||||
var released = false
|
||||
|
||||
ExternalActivityLifecycleGuard.launchExternalActivity(
|
||||
acquireConnectionLease = {},
|
||||
releaseConnectionLease = { released = true },
|
||||
prepareForBackground = {},
|
||||
cleanupAfterLaunchFailure = {},
|
||||
scheduleTimeout = { _, action -> timeoutAction = action },
|
||||
launch = {},
|
||||
)
|
||||
ExternalActivityLifecycleGuard.onHostPaused()
|
||||
|
||||
timeoutAction?.invoke()
|
||||
|
||||
assertTrue(released)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedExternalActivityLaunch_cleansUpServiceAndConnectionLease() {
|
||||
val calls = mutableListOf<String>()
|
||||
|
||||
runCatching {
|
||||
ExternalActivityLifecycleGuard.launchExternalActivity(
|
||||
acquireConnectionLease = { calls += "acquire" },
|
||||
releaseConnectionLease = { calls += "release" },
|
||||
prepareForBackground = { calls += "prepare" },
|
||||
cleanupAfterLaunchFailure = { calls += "cleanup" },
|
||||
scheduleTimeout = { _, _ -> },
|
||||
launch = { error("picker unavailable") },
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(listOf("acquire", "prepare", "cleanup", "release"), calls)
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ class HermesWsClientTest {
|
||||
(queueField.get(HermesWsClient) as java.util.concurrent.ConcurrentLinkedQueue<String>).clear()
|
||||
|
||||
HermesWsClient.disconnect(clearPendingMessages = true) // Ensure it starts clean
|
||||
HermesWsClient.releaseExternalActivityConnectionLease()
|
||||
HermesWsClient.setAppForeground(true)
|
||||
val acceptQueuedMessagesField = HermesWsClient::class.java.getDeclaredField("acceptQueuedMessages")
|
||||
acceptQueuedMessagesField.isAccessible = true
|
||||
@@ -97,6 +98,7 @@ class HermesWsClientTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
HermesWsClient.releaseExternalActivityConnectionLease()
|
||||
HermesWsClient.disconnect(clearPendingMessages = true)
|
||||
// Wait a bit to allow internal OkHttp coroutines to clean up before shutting down MockWebServer
|
||||
// Increased from 100ms for OkHttp 5.x — needs more time for the WS close handshake
|
||||
@@ -837,6 +839,23 @@ class HermesWsClientTest {
|
||||
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternalActivityLeaseKeepsIdleBackgroundSocketUntilReleased() {
|
||||
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
|
||||
HermesWsClient.connect()
|
||||
runBlocking { withTimeout(5000) { HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED } } }
|
||||
|
||||
HermesWsClient.acquireExternalActivityConnectionLease()
|
||||
HermesWsClient.setAppForeground(false)
|
||||
|
||||
assertTrue(HermesWsClient.isConnected)
|
||||
|
||||
HermesWsClient.releaseExternalActivityConnectionLease()
|
||||
|
||||
assertFalse(HermesWsClient.isConnected)
|
||||
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBackgroundWithPendingReplyStaysConnected() {
|
||||
mockWebServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {}))
|
||||
|
||||
@@ -1305,6 +1305,24 @@ class ChatViewModelTest {
|
||||
assertEquals("dummy.txt", pending[0].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addAttachments_keepsEverySelectedImageInOrder() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
val selected =
|
||||
listOf(
|
||||
Attachment("content://image/1", "one.jpg", "image/jpeg", 100L),
|
||||
Attachment("content://image/2", "two.png", "image/png", 200L),
|
||||
Attachment("content://image/3", "three.webp", "image/webp", 300L),
|
||||
)
|
||||
|
||||
viewModel.addAttachments(selected)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(selected, viewModel.uiState.value.pendingAttachments)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveAttachment_validIndex() =
|
||||
runTest {
|
||||
|
||||
Reference in new issue
Block a user