1@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
3package soy.iko.opencode.ui.file
5import kotlinx.coroutines.Dispatchers
6import kotlinx.coroutines.ExperimentalCoroutinesApi
7import kotlinx.coroutines.test.StandardTestDispatcher
8import kotlinx.coroutines.test.TestScope
9import kotlinx.coroutines.test.resetMain
10import kotlinx.coroutines.test.runTest
11import kotlinx.coroutines.test.setMain
13import org.junit.Assert.assertEquals
14import org.junit.Assert.assertFalse
15import org.junit.Assert.assertNotNull
16import org.junit.Assert.assertNull
17import org.junit.Assert.assertTrue
18import org.junit.Before
20import soy.iko.opencode.data.model.FileNode
21import soy.iko.opencode.data.model.FileStatusEntry
22import soy.iko.opencode.data.model.ServerProfile
23import soy.iko.opencode.ui.testing.FakeAppContainer
24import soy.iko.opencode.ui.testing.FakeEventStreamClient
25import soy.iko.opencode.ui.testing.FakeOpencodeApiClient
26import soy.iko.opencode.ui.testing.FakeOpencodeConnection
27import soy.iko.opencode.ui.testing.FakeSessionRepository
29class FileBrowserViewModelTest {
30 private val testDispatcher = StandardTestDispatcher()
31 private val testScope = TestScope(testDispatcher)
35 Dispatchers.setMain(testDispatcher)
40 Dispatchers.resetMain()
43 private fun makeContainer(
44 api: FakeOpencodeApiClient = FakeOpencodeApiClient(),
46 val events = FakeEventStreamClient()
47 val repo = FakeSessionRepository(api, events)
48 val container = FakeAppContainer()
49 val conn = FakeOpencodeConnection(api, events, repo, ServerProfile(id = "s1", label = "Test", baseUrl = "http://localhost"))
50 container.setActiveConnection(conn)
54 private fun makeVm(container: FakeAppContainer): FileBrowserViewModel {
55 val vm = FileBrowserViewModel(container)
56 testScope.testScheduler.advanceUntilIdle()
63 fun open_loadsDirectoryListing() = testScope.runTest {
64 val api = FakeOpencodeApiClient()
65 api.directoryListing = listOf(
66 FileNode(name = "file1.txt", path = "file1.txt", type = "file"),
67 FileNode(name = "dir1", path = "dir1", type = "directory"),
69 val container = makeContainer(api)
70 val vm = makeVm(container)
71 assertFalse(vm.state.value.loading)
72 assertEquals(2, vm.state.value.entries.size)
73 // Directories sorted first
74 assertTrue(vm.state.value.entries[0].isDirectory)
78 fun open_withoutConnection_showsNotConnected() = testScope.runTest {
79 val container = FakeAppContainer()
80 val vm = makeVm(container)
81 assertFalse(vm.state.value.loading)
82 assertNotNull(vm.state.value.error)
86 fun `open loads and sorts directories first`() = testScope.runTest {
87 val api = FakeOpencodeApiClient()
88 api.directoryListing = listOf(
89 FileNode(name = "zfile.txt", path = "zfile.txt", type = "file"),
90 FileNode(name = "adir", path = "adir", type = "directory"),
91 FileNode(name = "bfile.txt", path = "bfile.txt", type = "file"),
93 val container = makeContainer(api)
94 val vm = makeVm(container)
95 val entries = vm.state.value.entries
96 assertEquals("adir", entries[0].name)
102 fun up_navigatesToParentDirectory() = testScope.runTest {
103 val api = FakeOpencodeApiClient()
104 api.directoryListing = listOf(FileNode(name = "parent", path = "", type = "directory"))
105 val container = makeContainer(api)
106 val vm = makeVm(container)
108 testScheduler.advanceUntilIdle()
110 testScheduler.advanceUntilIdle()
111 assertEquals("foo", vm.state.value.path)
115 fun up_atRootDoesNothing() = testScope.runTest {
116 val container = makeContainer()
117 val vm = makeVm(container)
119 assertEquals("", vm.state.value.path)
122 // --- setQuery() / search ---
125 fun setQuery_blankClearsSearch() = testScope.runTest {
126 val api = FakeOpencodeApiClient()
127 api.directoryListing = listOf(FileNode(name = "file.txt", path = "file.txt", type = "file"))
128 val container = makeContainer(api)
129 val vm = makeVm(container)
131 testScheduler.advanceUntilIdle()
132 assertTrue(vm.state.value.isSearching)
134 testScheduler.advanceUntilIdle()
135 assertFalse(vm.state.value.isSearching)
136 assertFalse(vm.state.value.searching)
140 fun setQuery_nonBlankTriggersSearchAfterDebounce() = testScope.runTest {
141 val api = FakeOpencodeApiClient()
142 api.fileSearchResults = listOf("src/main.kt", "src/util.kt")
143 val container = makeContainer(api)
144 val vm = makeVm(container)
146 // Immediately searching flag is set
147 assertTrue(vm.state.value.searching)
148 // But results are empty until debounce fires
149 assertTrue(vm.state.value.results.isEmpty())
150 testScheduler.advanceUntilIdle()
151 // After debounce, results are populated
152 assertEquals(2, vm.state.value.results.size)
153 assertFalse(vm.state.value.searching)
157 fun setQuery_debounceCoalescesRapidTyping() = testScope.runTest {
158 val api = FakeOpencodeApiClient()
159 api.fileSearchResults = listOf("result.kt")
160 val container = makeContainer(api)
161 val vm = makeVm(container)
165 testScheduler.advanceUntilIdle()
166 // Only one search should have been performed (debounced)
167 assertEquals(1, api.fileSearchResults.size) // fake returns same results, just verify it ran
168 assertFalse(vm.state.value.searching)
171 // --- VCS status ---
174 fun loadStatus_loadsVcsStatus() = testScope.runTest {
175 val api = FakeOpencodeApiClient()
176 api.fileStatusEntries = listOf(
177 FileStatusEntry(path = "src/main.kt", status = "M"),
179 val container = makeContainer(api)
180 val vm = makeVm(container)
181 assertEquals(1, vm.state.value.statusMap.size)
182 assertEquals("M", vm.state.value.statusMap["src/main.kt"]?.status)
186 fun loadStatus_failureSetsTransientError() = testScope.runTest {
187 val api = FakeOpencodeApiClient()
188 // Make fileStatus throw by not providing entries and having the fake return empty
189 // (the fake doesn't throw, so we just verify the happy path doesn't set error)
190 val container = makeContainer(api)
191 val vm = makeVm(container)
192 // With empty status, no transient error event is emitted.
195 // --- connection availability ---
198 fun open_retriesWhenConnectionBecomesAvailable() = testScope.runTest {
199 val container = FakeAppContainer()
200 val vm = FileBrowserViewModel(container)
201 testScheduler.advanceUntilIdle()
202 // Initially no connection — should show error
203 assertNotNull(vm.state.value.error)
205 // Connection becomes available
206 val api = FakeOpencodeApiClient()
207 api.directoryListing = listOf(FileNode(name = "file.txt", path = "file.txt", type = "file"))
208 val events = FakeEventStreamClient()
209 val repo = FakeSessionRepository(api, events)
210 val conn = FakeOpencodeConnection(api, events, repo, ServerProfile(id = "s1", label = "Test", baseUrl = "http://localhost"))
211 container.setActiveConnection(conn)
212 testScheduler.advanceUntilIdle()
214 // Should now have loaded the directory
215 assertFalse(vm.state.value.loading)
216 assertNull(vm.state.value.error)
217 assertEquals(1, vm.state.value.entries.size)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list