NotesModeTest.kt

5.6 KB · raw

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    // ── Navigation decision (the VaultListScreen row-tap mapping) ──────────────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    // ── NOTES filter pill ─────────────────────────────────────────────────────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        // Off: everything passes through, order preserved.78        assertEquals(listOf("a", "b", "c", "d"), filterNotesOnly(list, noteOnly = false).map { it.title })79    }8081    // ── Auto-save debounce (NotesModeViewModel) ───────────────────────────────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) // cross the 800ms threshold95        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") // resets the timer108        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        // saveTitleNow bypasses the 800ms debounce (fired on field blur / back press).122        vm.saveTitleNow("Trip plans")123        advanceUntilIdle()124        assertEquals("Trip plans", repo.getEntry(saved.id)?.title)125    }126}