1package com.peachpasswords.ui.vault23import androidx.lifecycle.SavedStateHandle4import com.peachpasswords.ui.PeachNav5import com.peachpasswords.vault.StoredVault6import com.peachpasswords.vault.VaultEntry7import com.peachpasswords.vault.VaultRepository8import com.peachpasswords.vault.VaultSettings9import com.peachpasswords.vault.VaultStorage10import kotlinx.coroutines.Dispatchers11import kotlinx.coroutines.ExperimentalCoroutinesApi12import kotlinx.coroutines.test.StandardTestDispatcher13import kotlinx.coroutines.test.advanceTimeBy14import kotlinx.coroutines.test.advanceUntilIdle15import kotlinx.coroutines.test.resetMain16import kotlinx.coroutines.test.runCurrent17import kotlinx.coroutines.test.runTest18import kotlinx.coroutines.test.setMain19import org.junit.After20import org.junit.Assert.assertEquals21import org.junit.Assert.assertNull22import org.junit.Before23import org.junit.Test2425@OptIn(ExperimentalCoroutinesApi::class)26class NotesModeTest {2728 private val dispatcher = StandardTestDispatcher()2930 @Before fun setUp() = Dispatchers.setMain(dispatcher)31 @After fun tearDown() = Dispatchers.resetMain()3233 private class FakeStorage : VaultStorage {34 var slotA: StoredVault? = null35 var slotB: String? = null36 private var ver: Long = 037 private var settings = VaultSettings()38 override fun loadSlotA() = slotA39 override fun hasVault() = slotA != null40 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }41 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }42 var decoy: StoredVault? = null43 override fun loadDecoySlot() = decoy44 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }45 override fun hasDecoySlot() = decoy != null46 override fun clearDecoySlot() { decoy = null }47 override fun version() = ver48 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }49 override fun loadSettings() = settings50 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }51 override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }52 }5354 private fun note(title: String = "n", notes: String? = null) =55 VaultEntry(id = "", title = title, type = "note", notes = notes, createdAt = 0, modifiedAt = 0)5657 private fun login(title: String = "l") =58 VaultEntry(id = "", title = title, type = "login", password = "pw", createdAt = 0, modifiedAt = 0)5960 6162 @Test fun note_entry_navigates_to_notes_mode() {63 val e = VaultEntry(id = "abc", title = "Diary", type = "note", createdAt = 0, modifiedAt = 0)64 assertEquals(PeachNav.NotesMode("abc"), entryTapDestination(e))65 }6667 @Test fun non_note_entry_navigates_to_entry_detail() {68 val e = VaultEntry(id = "xyz", title = "GitHub", type = "login", createdAt = 0, modifiedAt = 0)69 assertEquals(PeachNav.EntryDetail("xyz"), entryTapDestination(e))70 }7172 7374 @Test fun notes_filter_shows_only_note_entries() {75 val list = listOf(login("a"), note("b"), login("c"), note("d"))76 assertEquals(listOf("b", "d"), filterNotesOnly(list, noteOnly = true).map { it.title })77 78 assertEquals(listOf("a", "b", "c", "d"), filterNotesOnly(list, noteOnly = false).map { it.title })79 }8081 8283 @Test fun notes_auto_save_debounces_800ms() = runTest(dispatcher) {84 val repo = VaultRepository(FakeStorage())85 repo.createVault("correct horse battery")86 val saved = repo.addEntry(note("My note"))87 val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id)))8889 vm.onContentChanged("hello")90 advanceTimeBy(799)91 runCurrent()92 assertNull("must not save before 800ms", repo.getEntry(saved.id)?.notes)9394 advanceTimeBy(2) 95 advanceUntilIdle()96 assertEquals("hello", repo.getEntry(saved.id)?.notes)97 }9899 @Test fun notes_auto_save_coalesces_rapid_keystrokes() = runTest(dispatcher) {100 val repo = VaultRepository(FakeStorage())101 repo.createVault("correct horse battery")102 val saved = repo.addEntry(note("My note"))103 val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id)))104105 vm.onContentChanged("h")106 advanceTimeBy(400)107 vm.onContentChanged("he") 108 advanceTimeBy(400)109 runCurrent()110 assertNull("timer reset by second keystroke", repo.getEntry(saved.id)?.notes)111 advanceUntilIdle()112 assertEquals("he", repo.getEntry(saved.id)?.notes)113 }114115 @Test fun notes_title_edit_saves_on_blur() = runTest(dispatcher) {116 val repo = VaultRepository(FakeStorage())117 repo.createVault("correct horse battery")118 val saved = repo.addEntry(note("untitled"))119 val vm = NotesModeViewModel(repo, SavedStateHandle(mapOf("id" to saved.id)))120121 122 vm.saveTitleNow("Trip plans")123 advanceUntilIdle()124 assertEquals("Trip plans", repo.getEntry(saved.id)?.title)125 }126}