spb/ka-android
Public
Kotlin 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// RefonteTests.kt — tests de la refonte v2 : clustering par grille (miroir3// iOS), historique consulté (dédupliqué, plafond 30), alertes honnêtes4// (markSeen), galerie multi-photos, garde Trouve-Ka (q obligatoire).5package com.groupeka.ka67import com.groupeka.ka.core.ClusterEngine8import com.groupeka.ka.core.Ecosystem9import com.groupeka.ka.core.KAItem10import com.groupeka.ka.core.RecentsStore11import com.groupeka.ka.core.SavedSearch12import com.groupeka.ka.core.UniverseService13import kotlinx.coroutines.runBlocking14import kotlinx.serialization.json.Json15import kotlinx.serialization.json.JsonObject16import org.junit.Assert.assertEquals17import org.junit.Assert.assertNull18import org.junit.Assert.assertTrue19import org.junit.Test20import java.io.File2122class RefonteTests {2324 private fun item(id: String, lat: Double, lon: Double) =25 KAItem(id = id, universeID = "lou-ka", title = id, latitude = lat, longitude = lon)2627 // MARK: clustering (miroir des 2 tests iOS)2829 @Test30 fun clusteringGroupsNearbyAndKeepsSingles() {31 val items = listOf(32 item("a", 46.805, -71.205),33 item("b", 46.806, -71.206), // même cellule que a34 item("c", 46.90, -71.40), // loin → seul35 )36 val clusters = ClusterEngine.clusterize(items, latSpan = 0.3, lonSpan = 0.3)37 assertEquals(2, clusters.size)38 val group = clusters.first { it.items.size == 2 }39 assertTrue(group.items.map { it.id }.containsAll(listOf("a", "b")))40 assertTrue(clusters.any { it.isSingle && it.item.id == "c" })41 // centroïde du groupe42 assertEquals(46.8055, group.latitude, 0.001)43 }4445 @Test46 fun clusteringCapsAt150Renderables() {47 val many = (0 until 400).map { i ->48 item("i$i", 40.0 + i * 0.5, -70.0 - i * 0.5) // tous éloignés → 400 cellules49 }50 val clusters = ClusterEngine.clusterize(many, latSpan = 250.0, lonSpan = 250.0)51 assertTrue("plafond de rendu 150", clusters.size <= 150)52 }5354 @Test55 fun clusteringIgnoresItemsWithoutCoordinates() {56 val items = listOf(57 KAItem(id = "x", universeID = "lou-ka", title = "sans coord"),58 item("y", 46.8, -71.2),59 )60 val clusters = ClusterEngine.clusterize(items, latSpan = 0.3, lonSpan = 0.3)61 assertEquals(1, clusters.size)62 assertEquals("y", clusters[0].item.id)63 }6465 // MARK: historique consulté6667 @Test68 fun historyDeduplicatesAndCapsAt30() {69 val file = File.createTempFile("ka-recents", ".json").also { it.delete() }70 val store = RecentsStore(file)71 for (i in 0 until 40) {72 store.record(KAItem(id = "it$i", universeID = "lou-ka", title = "It $i"))73 }74 // re-consulter un ancien → remonte en tête, sans doublon75 store.record(KAItem(id = "it35", universeID = "lou-ka", title = "It 35"))76 val viewed = store.viewed.value77 assertEquals(30, viewed.size)78 assertEquals("it35", viewed.first().id)79 assertEquals(1, viewed.count { it.id == "it35" })80 file.delete()81 }8283 @Test84 fun historyPersistsAcrossInstances() {85 val file = File.createTempFile("ka-recents", ".json").also { it.delete() }86 RecentsStore(file).record(KAItem(id = "p:1", universeID = "auto-ka", title = "Corolla"))87 val reloaded = RecentsStore(file)88 assertEquals("p:1", reloaded.viewed.value.first().id)89 file.delete()90 }9192 // MARK: alertes honnêtes9394 @Test95 fun markSeenFoldsDeltaIntoTotal() {96 val file = File.createTempFile("ka-recents", ".json").also { it.delete() }97 // injecter une recherche « déjà comptée » via la persistance98 file.writeText(99 """{"viewed":[],"searches":[{"id":"s1","name":"4½ Québec","universeID":"lou-ka","query":"4½","alertsOn":true,"lastTotal":100,"newCount":12}]}""",100 )101 val store2 = RecentsStore(file)102 assertEquals(12, store2.alertCount)103 store2.markSeen("s1")104 val s = store2.savedSearches.value.first()105 assertEquals(112, s.lastTotal)106 assertEquals(0, s.newCount)107 assertEquals(0, store2.alertCount)108 file.delete()109 }110111 @Test112 fun alertCountSumsOnlyEnabledAlerts() {113 val file = File.createTempFile("ka-recents", ".json").also { it.delete() }114 file.writeText(115 """{"viewed":[],"searches":[116 {"id":"a","name":"A","universeID":"lou-ka","query":"a","alertsOn":true,"newCount":5},117 {"id":"b","name":"B","universeID":"auto-ka","query":"b","alertsOn":false,"newCount":9}118 ]}""",119 )120 val store = RecentsStore(file)121 assertEquals(5, store.alertCount)122 file.delete()123 }124125 // MARK: galerie multi-photos126127 @Test128 fun galleryKeepsUpTo10HttpImages() {129 val u = Ecosystem.universe("auto-ka")!!130 val imgs = (1..14).joinToString(",") { "\"https://cdn.x.com/$it.jpg\"" }131 val o = Json.parseToJsonElement(132 """{"uid":"v:9","title":"Mazda 3","images":[$imgs,"pas-une-url"]}""",133 ) as JsonObject134 val item = u.map!!(o)!!135 assertEquals(10, item.imageURLs.size)136 assertEquals("https://cdn.x.com/1.jpg", item.imageURL)137 assertEquals(item.imageURL, item.imageURLs.first())138 }139140 // MARK: Trouve-Ka — recherche d'abord (l'API exige q, 422 sinon)141142 @Test143 fun trouveKaFetchWithoutQueryReturnsEmptyWithoutNetwork() = runBlocking {144 val u = Ecosystem.universe("trouve-ka")!!145 // pas de cacheDir ni de réseau nécessaire : la garde répond avant146 val items = UniverseService.fetch(u, query = null)147 assertTrue(items.isEmpty())148 assertNull(UniverseService.total(u, query = ""))149 }150151 // MARK: éditorial = requêtes réelles vers de vrais univers152153 @Test154 fun editorialCollectionsPointToRealUniverses() {155 val cols = com.groupeka.ka.core.Editorial.collections156 assertEquals(4, cols.size)157 cols.forEach { c ->158 assertTrue("univers inconnu ${c.universeID}", Ecosystem.universe(c.universeID) != null)159 }160 }161}162