From 529ab0bc83b065dfcd6f5376770afa2d9975040f Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:49:34 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20for=20ChatV?= =?UTF-8?q?iewModel=20attachment=20manipulation=20(#511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add unit tests for ChatViewModel attachment manipulation Adds tests covering addAttachment, removeAttachment (valid/invalid indices), and clearAttachments in ChatViewModelTest. This ensures the attachment state management logic works as expected. * test(#511): add mixed valid/invalid remove sequence + DRY attachment setup Folds in both Sourcery review notes: - New testRemoveAttachment_mixedValidAndInvalidSequence asserts list stays consistent across a valid remove, out-of-bounds + negative no-ops, then another valid remove on the shifted list. - addDummyAttachments() helper removes repeated VM/advance/add boilerplate. Verified: ktlintCheck clean + ChatViewModelTest 37/37 pass locally. --- .../ui/chat/ChatViewModelTest.kt | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt index 200e9fa6..5ab677b2 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt @@ -666,6 +666,127 @@ class ChatViewModelTest { } } + // ── Attachments ────────────────────────────────────────────────────────── + + /** Add [count] dummy attachments so a test starts with a populated list. */ + private fun TestScope.addDummyAttachments( + viewModel: ChatViewModel, + count: Int, + ) { + repeat(count) { i -> + viewModel.addAttachment("uri$i", "file$i.txt", "text/plain", (i + 1) * 100L) + } + advanceUntilIdle() + } + + @Test + fun testAddAttachment() = + runTest { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.addAttachment( + uri = "content://dummy/1", + name = "dummy.txt", + mimeType = "text/plain", + size = 1024L, + ) + advanceUntilIdle() + + val pending = viewModel.uiState.value.pendingAttachments + assertEquals(1, pending.size) + assertEquals("content://dummy/1", pending[0].uri) + assertEquals("dummy.txt", pending[0].name) + } + + @Test + fun testRemoveAttachment_validIndex() = + runTest { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.addAttachment("uri1", "file1.txt", "text/plain", 100) + viewModel.addAttachment("uri2", "file2.txt", "text/plain", 200) + advanceUntilIdle() + + assertEquals(2, viewModel.uiState.value.pendingAttachments.size) + + viewModel.removeAttachment(0) + advanceUntilIdle() + + val pending = viewModel.uiState.value.pendingAttachments + assertEquals(1, pending.size) + assertEquals("uri2", pending[0].uri) + } + + @Test + fun testRemoveAttachment_invalidIndex() = + runTest { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.addAttachment("uri1", "file1.txt", "text/plain", 100) + advanceUntilIdle() + + assertEquals(1, viewModel.uiState.value.pendingAttachments.size) + + // Out of bounds index should not crash or change list + viewModel.removeAttachment(5) + viewModel.removeAttachment(-1) + advanceUntilIdle() + + val pending = viewModel.uiState.value.pendingAttachments + assertEquals(1, pending.size) + assertEquals("uri1", pending[0].uri) + } + + @Test + fun testRemoveAttachment_mixedValidAndInvalidSequence() = + runTest { + val viewModel = createViewModel() + advanceUntilIdle() + addDummyAttachments(viewModel, 3) // [uri0, uri1, uri2] + + // 1. Remove a valid index (the middle item) → list shrinks correctly. + viewModel.removeAttachment(1) + advanceUntilIdle() + assertEquals(2, viewModel.uiState.value.pendingAttachments.size) + assertEquals("uri0", viewModel.uiState.value.pendingAttachments[0].uri) + assertEquals("uri2", viewModel.uiState.value.pendingAttachments[1].uri) + + // 2. Fire invalid removals (out of bounds + negative) — must be no-ops. + viewModel.removeAttachment(99) + viewModel.removeAttachment(-1) + advanceUntilIdle() + assertEquals(2, viewModel.uiState.value.pendingAttachments.size) + + // 3. Another valid removal on the shifted list → still consistent. + viewModel.removeAttachment(1) + advanceUntilIdle() + val pending = viewModel.uiState.value.pendingAttachments + assertEquals(1, pending.size) + assertEquals("uri0", pending[0].uri) + } + + @Test + fun testClearAttachments() = + runTest { + val viewModel = createViewModel() + advanceUntilIdle() + + viewModel.addAttachment("uri1", "file1.txt", "text/plain", 100) + viewModel.addAttachment("uri2", "file2.txt", "text/plain", 200) + advanceUntilIdle() + + assertEquals(2, viewModel.uiState.value.pendingAttachments.size) + + viewModel.clearAttachments() + advanceUntilIdle() + + val pending = viewModel.uiState.value.pendingAttachments + assertTrue(pending.isEmpty()) + } + // ── Send message ───────────────────────────────────────────────────────── @Test