quick wins + test cases
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.clean.scanner.testutil
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherRule(
|
||||
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
|
||||
) : TestWatcher() {
|
||||
override fun starting(description: Description) {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description) {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.clean.scanner.ui
|
||||
|
||||
import com.clean.scanner.domain.ScanResult
|
||||
import com.clean.scanner.testutil.MainDispatcherRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ScannerViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun onScan_firstSingleScan_updatesStateAndSaves() = runTest {
|
||||
val saved = mutableListOf<Pair<String, String>>()
|
||||
var now = 1_000L
|
||||
val viewModel = ScannerViewModel(
|
||||
saveScan = { content, type -> saved += content to type },
|
||||
nowProvider = { now }
|
||||
)
|
||||
|
||||
viewModel.onScan(ScanResult(content = "https://example.com", type = "URL"))
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals("https://example.com", state.lastResult?.content)
|
||||
assertEquals(false, state.analysisEnabled)
|
||||
assertEquals(1, state.scanFeedbackNonce)
|
||||
assertEquals(0, state.duplicateFeedbackNonce)
|
||||
assertEquals(listOf("URL|https://example.com"), state.recentScanKeys)
|
||||
assertEquals(listOf("https://example.com" to "URL"), saved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateScan_afterResume_incrementsDuplicateFeedback() = runTest {
|
||||
val saved = mutableListOf<Pair<String, String>>()
|
||||
var now = 1_000L
|
||||
val viewModel = ScannerViewModel(
|
||||
saveScan = { content, type -> saved += content to type },
|
||||
nowProvider = { now }
|
||||
)
|
||||
|
||||
val result = ScanResult(content = "ABC", type = "Text")
|
||||
viewModel.onScan(result)
|
||||
viewModel.resumeScanning()
|
||||
|
||||
now = 2_000L
|
||||
viewModel.onScan(result)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals(2, state.scanFeedbackNonce)
|
||||
assertEquals(1, state.duplicateFeedbackNonce)
|
||||
assertEquals(2, saved.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun batchMode_addsUniqueKeepsNewestFirst_andClearWorks() = runTest {
|
||||
val saved = mutableListOf<Pair<String, String>>()
|
||||
var now = 1_000L
|
||||
val viewModel = ScannerViewModel(
|
||||
saveScan = { content, type -> saved += content to type },
|
||||
nowProvider = { now }
|
||||
)
|
||||
|
||||
viewModel.setBatchMode(true)
|
||||
viewModel.onScan(ScanResult(content = "A", type = "Text"))
|
||||
|
||||
now = 2_000L
|
||||
viewModel.onScan(ScanResult(content = "B", type = "Text"))
|
||||
|
||||
now = 3_000L
|
||||
viewModel.onScan(ScanResult(content = "A", type = "Text"))
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals(2, state.batchResults.size)
|
||||
assertEquals("B", state.batchResults[0].result.content)
|
||||
assertEquals(2_000L, state.batchResults[0].timestamp)
|
||||
assertEquals("A", state.batchResults[1].result.content)
|
||||
assertEquals(1, state.duplicateFeedbackNonce)
|
||||
assertEquals(3, state.scanFeedbackNonce)
|
||||
assertTrue(state.analysisEnabled)
|
||||
|
||||
viewModel.clearBatchResults()
|
||||
assertTrue(viewModel.uiState.value.batchResults.isEmpty())
|
||||
assertEquals(3, saved.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.clean.scanner.util
|
||||
|
||||
import com.clean.scanner.domain.ScanRecord
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class HistoryExportFormatterTest {
|
||||
|
||||
@Test
|
||||
fun formatText_empty_returnsEmptyString() {
|
||||
assertEquals("", HistoryExportFormatter.formatText(emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatCsv_quotesAndEscapesFields() {
|
||||
val history = listOf(
|
||||
ScanRecord(
|
||||
id = 1,
|
||||
timestamp = 0L,
|
||||
type = "Te,xt",
|
||||
content = "hello \"world\""
|
||||
)
|
||||
)
|
||||
|
||||
val csv = HistoryExportFormatter.formatCsv(history)
|
||||
|
||||
assertTrue(csv.startsWith("timestamp,type,content\n"))
|
||||
assertTrue(csv.contains("\"Te,xt\""))
|
||||
assertTrue(csv.contains("\"hello \"\"world\"\"\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatJson_escapesQuotesBackslashAndNewline() {
|
||||
val history = listOf(
|
||||
ScanRecord(
|
||||
id = 7,
|
||||
timestamp = 123L,
|
||||
type = "Type\"A",
|
||||
content = "line1\\line2\nnext"
|
||||
)
|
||||
)
|
||||
|
||||
val json = HistoryExportFormatter.formatJson(history)
|
||||
|
||||
assertTrue(json.contains("\"id\":7"))
|
||||
assertTrue(json.contains("\"timestamp\":123"))
|
||||
assertTrue(json.contains("\"type\":\"Type\\\"A\""))
|
||||
assertTrue(json.contains("\"content\":\"line1\\\\line2\\nnext\""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.clean.scanner.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ScanContentParsersTest {
|
||||
|
||||
@Test
|
||||
fun extractPhoneNumber_handlesTelUri() {
|
||||
val phone = ScanContentParsers.extractPhoneNumber("tel:+123456789?foo=bar")
|
||||
assertEquals("+123456789", phone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseSms_handlesSmstoWithBody() {
|
||||
val (number, body) = ScanContentParsers.parseSms("SMSTO:+49123456:hello there")
|
||||
assertEquals("+49123456", number)
|
||||
assertEquals("hello there", body)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extractEmail_handlesMailtoAndPlainText() {
|
||||
assertEquals("x@y.com", ScanContentParsers.extractEmail("mailto:x@y.com?subject=hi"))
|
||||
assertEquals("a.b+c@d.dev", ScanContentParsers.extractEmail("contact me at a.b+c@d.dev now"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user