ScanCarteModelesManquantsTest.kt 143 lignes · 5143 octets
package fr.ebii.card2vcf.ui

import android.content.Context
import android.graphics.Bitmap
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import fr.ebii.card2vcf.data.ContactImageStore
import fr.ebii.card2vcf.data.ContactRepository
import fr.ebii.card2vcf.data.CrmDatabase
import fr.ebii.card2vcf.ocr.ModelesManquantsException
import fr.ebii.card2vcf.ocr.OcrEngine
import fr.ebii.card2vcf.ocr.OcrException
import fr.ebii.card2vcf.ocr.OcrResult
import fr.ebii.card2vcf.scan.ScanEngine
import fr.ebii.card2vcf.scan.ScanResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertSame
import kotlin.test.assertTrue

/**
 * Sans modèle OCR, la capture aboutit quand même : la carte est photographiée et
 * redressée, seul le remplissage des champs manque. Le scan ne doit donc pas finir
 * en erreur générique mais proposer le téléchargement — ou la saisie manuelle,
 * en conservant l'image redressée.
 */
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [31])
class ScanCarteModelesManquantsTest {

    private lateinit var db: CrmDatabase
    private lateinit var repo: ContactRepository
    private lateinit var source: Bitmap
    private lateinit var redressee: Bitmap

    private val dispatcher = StandardTestDispatcher()

    private class ScanFactice(private val resultat: Bitmap) : ScanEngine {
        override fun scan(source: Bitmap, manualCorners: fr.ebii.card2vcf.scan.ScanCorners?) =
            ScanResult(bitmap = resultat)
    }

    private class OcrQuiEchoue(private val erreur: OcrException) : OcrEngine {
        override fun recognize(bitmap: Bitmap): OcrResult = throw erreur
    }

    @Before
    fun setUp() {
        Dispatchers.setMain(dispatcher)
        val ctx = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(ctx, CrmDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        repo = ContactRepository(db.crmContactDao(), ContactImageStore(ctx))
        source = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888)
        redressee = Bitmap.createBitmap(8, 8, Bitmap.Config.ARGB_8888)
    }

    @After
    fun tearDown() {
        db.close()
        Dispatchers.resetMain()
    }

    private fun viewModel(erreur: OcrException) = ScanCarteViewModel(
        scanEngine = ScanFactice(redressee),
        ocrEngine = OcrQuiEchoue(erreur),
        repository = repo,
        calcul = dispatcher,
    )

    @Test
    fun `des modeles manquants donnent un etat dedie et non une erreur generique`() = runTest(dispatcher) {
        val vm = viewModel(ModelesManquantsException(listOf("fra", "eng")))

        vm.processCapture(source)
        testScheduler.advanceUntilIdle()

        val etat = vm.state.value
        assertIs<ScanUiState.ModelesManquants>(etat)
        assertEquals(listOf("fra", "eng"), etat.langues)
    }

    @Test
    fun `l'image redressee est conservee pour la saisie manuelle`() = runTest(dispatcher) {
        val vm = viewModel(ModelesManquantsException(listOf("fra")))

        vm.processCapture(source)
        testScheduler.advanceUntilIdle()

        val etat = assertIs<ScanUiState.ModelesManquants>(vm.state.value)
        assertSame(redressee, etat.preview, "la capture redressée ne doit pas être jetée")
    }

    @Test
    fun `saisir a la main ouvre un brouillon vide portant la photo de carte`() = runTest(dispatcher) {
        val vm = viewModel(ModelesManquantsException(listOf("fra")))
        vm.processCapture(source)
        testScheduler.advanceUntilIdle()

        vm.saisirAlaMain()

        val etat = assertIs<ScanUiState.DraftReady>(vm.state.value)
        assertSame(redressee, etat.draft.preview)
        assertEquals("", etat.draft.rawOcrText)
        assertTrue(etat.draft.card.fullName.isNullOrEmpty(), "aucun champ ne doit être pré-rempli")
        assertTrue(etat.draft.card.emails.isEmpty())
        assertTrue(etat.draft.card.phones.isEmpty())
    }

    @Test
    fun `saisir a la main est sans effet hors de l'etat modeles manquants`() = runTest(dispatcher) {
        val vm = viewModel(ModelesManquantsException(listOf("fra")))

        vm.saisirAlaMain()

        assertIs<ScanUiState.Idle>(vm.state.value)
    }

    @Test
    fun `une autre erreur OCR reste une erreur generique`() = runTest(dispatcher) {
        val vm = viewModel(OcrException("OCR vide — reprenez la photo"))

        vm.processCapture(source)
        testScheduler.advanceUntilIdle()

        val etat = assertIs<ScanUiState.Error>(vm.state.value)
        assertEquals("OCR vide — reprenez la photo", etat.message)
    }
}