diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.xml b/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.xml new file mode 100644 index 00000000..555e1f72 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_cloud_off.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 32fc6c04..16c349df 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -1,15 +1,12 @@ - Run Test - Back - Dashboard - Settings - + Dashboard OONI Tests OONI Run Links Stopping test… Finishing the currently pending tests, please wait… + Run Stop test Websites @@ -35,6 +32,7 @@ Unknown N/A + Settings Notifications Enabled Interested in running OONI Probe tests during emergent censorship events? Enable notifications to receive a message when we hear of internet censorship near you. @@ -131,5 +129,14 @@ Intergovernmental organizations including The United Nations Sites that haven\'t been categorized yet + Not uploaded + Unable to download URL list. Please try again. + + + Back + + %1$d measurement + %1$d measurements + diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt index 4015fa55..09a4ba99 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt @@ -98,15 +98,10 @@ class TaskEventMapper( TaskEvent.MeasurementDone(index = value?.idx ?: 0) "status.measurement_start" -> - value?.input?.ifEmpty { null }?.let { url -> - TaskEvent.MeasurementStart( - index = value.idx, - url = url, - ) - } ?: run { - Logger.d("Task Event $key missing 'input'") - null - } + TaskEvent.MeasurementStart( + index = value?.idx ?: 0, + url = value?.input, + ) "status.measurement_submission" -> TaskEvent.MeasurementSubmissionSuccessful(index = value?.idx ?: 0) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt index cad4bd91..3b35dfa0 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt @@ -35,7 +35,7 @@ sealed interface TaskEvent { data class MeasurementStart( val index: Int, - val url: String, + val url: String?, ) : TaskEvent data class MeasurementSubmissionSuccessful( diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt index 69707d1f..45b72e35 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TestType.kt @@ -139,19 +139,22 @@ sealed class TestType { } companion object { - private val ALL_NAMED = listOf( - Dash, - FacebookMessenger, - HttpHeaderFieldManipulation, - HttpInvalidRequestLine, - Ndt, - Psiphon, - Signal, - Telegram, - Tor, - WebConnectivity, - Whatsapp, - ) + // Lazy due to https://youtrack.jetbrains.com/issue/KT-8970/Object-is-uninitialized-null-when-accessed-from-static-context-ex.-companion-object-with-initialization-loop + private val ALL_NAMED by lazy { + listOf( + Dash, + FacebookMessenger, + HttpHeaderFieldManipulation, + HttpInvalidRequestLine, + Ndt, + Psiphon, + Signal, + Telegram, + Tor, + WebConnectivity, + Whatsapp, + ) + } fun fromName(name: String) = ALL_NAMED.firstOrNull { it.name == name } ?: Experimental(name) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt index c4d71e9e..8cf5d415 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt @@ -1,6 +1,8 @@ package org.ooni.probe +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -37,11 +39,15 @@ fun App(dependencies: Dependencies) { Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { BottomNavigationBar(navController) }, - ) { - Navigation( - navController = navController, - dependencies = dependencies, - ) + ) { paddingValues -> + Box( + modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()), + ) { + Navigation( + navController = navController, + dependencies = dependencies, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt index e62da673..2c3549ea 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/Descriptor.kt @@ -27,4 +27,7 @@ data class Descriptor( } val isExpired get() = expirationDate != null && expirationDate < LocalDateTime.now() + + companion object { + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt index a64c1d46..c62a6612 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementModel.kt @@ -29,6 +29,8 @@ data class MeasurementModel( val value: Long, ) + val idOrThrow get() = id ?: throw IllegalStateException("Id no available") + val logFilePath: Path get() = logFilePath(resultId, test) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt new file mode 100644 index 00000000..8a21a3cb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/MeasurementWithUrl.kt @@ -0,0 +1,6 @@ +package org.ooni.probe.data.models + +data class MeasurementWithUrl( + val measurement: MeasurementModel, + val url: UrlModel?, +) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt new file mode 100644 index 00000000..44a478a4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultItem.kt @@ -0,0 +1,8 @@ +package org.ooni.probe.data.models + +data class ResultItem( + val result: ResultModel, + val descriptor: Descriptor, + val network: NetworkModel?, + val measurements: List, +) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt index f36e81dd..ff4c11dc 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt @@ -2,8 +2,10 @@ package org.ooni.probe.data.models data class ResultListItem( val result: ResultModel, + val descriptor: Descriptor, val network: NetworkModel?, val measurementsCount: Long, + val allMeasurementsUploaded: Boolean, ) { val idOrThrow get() = result.idOrThrow } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt new file mode 100644 index 00000000..e0e079e1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultWithNetworkAndAggregates.kt @@ -0,0 +1,8 @@ +package org.ooni.probe.data.models + +data class ResultWithNetworkAndAggregates( + val result: ResultModel, + val network: NetworkModel?, + val measurementsCount: Long, + val allMeasurementsUploaded: Boolean, +) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt index a440b053..e8ba2802 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/MeasurementRepository.kt @@ -9,7 +9,10 @@ import kotlinx.coroutines.withContext import org.ooni.engine.models.TestType import org.ooni.probe.Database import org.ooni.probe.data.Measurement +import org.ooni.probe.data.SelectByResultIdWithUrl +import org.ooni.probe.data.Url import org.ooni.probe.data.models.MeasurementModel +import org.ooni.probe.data.models.MeasurementWithUrl import org.ooni.probe.data.models.ResultModel import org.ooni.probe.data.models.UrlModel import org.ooni.probe.shared.toEpoch @@ -26,6 +29,13 @@ class MeasurementRepository( .mapToList(backgroundDispatcher) .map { list -> list.mapNotNull { it.toModel() } } + fun listByResultId(id: ResultModel.Id) = + database.measurementQueries + .selectByResultIdWithUrl(id.value) + .asFlow() + .mapToList(backgroundDispatcher) + .map { list -> list.mapNotNull { it.toModel() } } + suspend fun createOrUpdate(model: MeasurementModel): MeasurementModel.Id = withContext(backgroundDispatcher) { database.transactionWithResult { @@ -76,4 +86,36 @@ class MeasurementRepository( resultId = result_id?.let(ResultModel::Id) ?: return null, ) } + + private fun SelectByResultIdWithUrl.toModel(): MeasurementWithUrl? { + return MeasurementWithUrl( + measurement = Measurement( + id = id, + test_name = test_name, + start_time = start_time, + runtime = runtime, + is_done = is_done, + is_uploaded = is_uploaded, + is_failed = is_failed, + failure_msg = failure_msg, + is_upload_failed = is_upload_failed, + upload_failure_msg = upload_failure_msg, + is_rerun = is_rerun, + is_anomaly = is_anomaly, + report_id = report_id, + test_keys = test_keys, + rerun_network = rerun_network, + url_id = url_id, + result_id = result_id, + ).toModel() ?: return null, + url = id_?.let { urlId -> + Url( + id = urlId, + url = url, + country_code = country_code, + category_code = category_code, + ).toModel() + }, + ) + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt index 82d406a7..8f9d98f8 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ResultRepository.kt @@ -2,6 +2,7 @@ package org.ooni.probe.data.repositories import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList +import app.cash.sqldelight.coroutines.mapToOneOrNull import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -10,10 +11,11 @@ import org.ooni.probe.Database import org.ooni.probe.data.Network import org.ooni.probe.data.Result import org.ooni.probe.data.SelectAllWithNetwork +import org.ooni.probe.data.SelectByIdWithNetwork import org.ooni.probe.data.models.InstalledTestDescriptorModel import org.ooni.probe.data.models.NetworkModel -import org.ooni.probe.data.models.ResultListItem import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.models.ResultWithNetworkAndAggregates import org.ooni.probe.shared.toEpoch import org.ooni.probe.shared.toLocalDateTime @@ -28,19 +30,19 @@ class ResultRepository( .mapToList(backgroundDispatcher) .map { list -> list.mapNotNull { it.toModel() } } - fun listWithNetwork(): Flow> = + fun listWithNetwork(): Flow> = database.resultQueries .selectAllWithNetwork() .asFlow() .mapToList(backgroundDispatcher) .map { list -> list.mapNotNull { it.toModel() } } - fun getById(resultId: ResultModel.Id): Flow = + fun getById(resultId: ResultModel.Id): Flow?> = database.resultQueries - .selectById(resultId.value) + .selectByIdWithNetwork(resultId.value) .asFlow() - .mapToList(backgroundDispatcher) - .map { it.firstOrNull()?.toModel() } + .mapToOneOrNull(backgroundDispatcher) + .map { it?.toModel() } suspend fun createOrUpdate(model: ResultModel): ResultModel.Id = withContext(backgroundDispatcher) { @@ -79,33 +81,59 @@ class ResultRepository( ) } - private fun SelectAllWithNetwork.toModel(): ResultListItem? { - return ResultListItem( - result = - Result( - id = id, - test_group_name = test_group_name, - start_time = start_time, - is_viewed = is_viewed, - is_done = is_done, - data_usage_up = data_usage_up, - data_usage_down = data_usage_down, - failure_msg = failure_msg, - network_id = network_id, - descriptor_runId = descriptor_runId, - ).toModel() ?: return null, - network = - id_?.let { networkId -> - Network( - id = networkId, - network_name = network_name, - ip = ip, - asn = asn, - country_code = country_code, - network_type = network_type, - ).toModel() - }, + private fun SelectAllWithNetwork.toModel(): ResultWithNetworkAndAggregates? { + return ResultWithNetworkAndAggregates( + result = Result( + id = id, + test_group_name = test_group_name, + start_time = start_time, + is_viewed = is_viewed, + is_done = is_done, + data_usage_up = data_usage_up, + data_usage_down = data_usage_down, + failure_msg = failure_msg, + network_id = network_id, + descriptor_runId = descriptor_runId, + ).toModel() ?: return null, + network = id_?.let { networkId -> + Network( + id = networkId, + network_name = network_name, + ip = ip, + asn = asn, + country_code = country_code, + network_type = network_type, + ).toModel() + }, measurementsCount = measurementsCount, + allMeasurementsUploaded = allMeasurementsUploaded, + ) + } + + private fun SelectByIdWithNetwork.toModel(): Pair? { + return Pair( + Result( + id = id, + test_group_name = test_group_name, + start_time = start_time, + is_viewed = is_viewed, + is_done = is_done, + data_usage_up = data_usage_up, + data_usage_down = data_usage_down, + failure_msg = failure_msg, + network_id = network_id, + descriptor_runId = descriptor_runId, + ).toModel() ?: return null, + id_?.let { networkId -> + Network( + id = networkId, + network_name = network_name, + ip = ip, + asn = asn, + country_code = country_code, + network_type = network_type, + ).toModel() + }, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt index 81bf86c1..af448d34 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/UrlRepository.kt @@ -82,13 +82,13 @@ class UrlRepository( fun getByUrl(url: String): Flow = listByUrls(listOf(url)) .map { it.firstOrNull() } +} - private fun Url.toModel(): UrlModel? { - return UrlModel( - id = UrlModel.Id(id), - url = url ?: return null, - countryCode = country_code, - category = category_code?.let(WebConnectivityCategory::fromCode), - ) - } +fun Url.toModel(): UrlModel? { + return UrlModel( + id = UrlModel.Id(id), + url = url ?: return null, + countryCode = country_code, + category = category_code?.let(WebConnectivityCategory::fromCode), + ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt index ac6fbaba..0eda2206 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -118,8 +118,19 @@ class Dependencies( GetBootstrapTestDescriptors(readAssetFile, json, backgroundDispatcher) } private val getDefaultTestDescriptors by lazy { GetDefaultTestDescriptors() } - private val getResults by lazy { GetResults(resultRepository) } - private val getResult by lazy { GetResult(resultRepository) } + private val getResults by lazy { + GetResults( + resultRepository.listWithNetwork(), + getTestDescriptors.invoke(), + ) + } + private val getResult by lazy { + GetResult( + getResultById = resultRepository::getById, + getTestDescriptors = getTestDescriptors.invoke(), + getMeasurementsByResultId = measurementRepository::listByResultId, + ) + } private val getTestDescriptors by lazy { GetTestDescriptors( getDefaultTestDescriptors = getDefaultTestDescriptors::invoke, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt index e77466e3..ddfed53d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt @@ -1,10 +1,30 @@ package org.ooni.probe.domain +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import org.ooni.probe.data.models.Descriptor +import org.ooni.probe.data.models.MeasurementWithUrl +import org.ooni.probe.data.models.NetworkModel +import org.ooni.probe.data.models.ResultItem import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.data.repositories.ResultRepository class GetResult( - private val resultRepository: ResultRepository, + private val getResultById: (ResultModel.Id) -> Flow?>, + private val getTestDescriptors: Flow>, + private val getMeasurementsByResultId: (ResultModel.Id) -> Flow>, ) { - operator fun invoke(resultId: ResultModel.Id) = resultRepository.getById(resultId) + operator fun invoke(resultId: ResultModel.Id): Flow = + combine( + getResultById(resultId), + getTestDescriptors, + getMeasurementsByResultId(resultId), + ) { resultWithNetwork, descriptors, measurements -> + val result = resultWithNetwork?.first ?: return@combine null + ResultItem( + result = result, + descriptor = descriptors.forResult(result) ?: return@combine null, + network = resultWithNetwork.second, + measurements = measurements, + ) + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt index 446dd63e..5f9883e8 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt @@ -1,9 +1,38 @@ package org.ooni.probe.domain -import org.ooni.probe.data.repositories.ResultRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import org.ooni.probe.data.models.Descriptor +import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.models.ResultWithNetworkAndAggregates class GetResults( - private val resultRepository: ResultRepository, + private val getResultsWithNetwork: Flow>, + private val getDescriptors: Flow>, ) { - operator fun invoke() = resultRepository.listWithNetwork() + operator fun invoke(): Flow> = + combine( + getResultsWithNetwork, + getDescriptors, + ) { results, descriptors -> + results.mapNotNull { item -> + ResultListItem( + result = item.result, + descriptor = descriptors.forResult(item.result) ?: return@mapNotNull null, + network = item.network, + measurementsCount = item.measurementsCount, + allMeasurementsUploaded = item.allMeasurementsUploaded, + ) + } + } } + +fun List.forResult(result: ResultModel): Descriptor? = + result.testDescriptorId + ?.let { descriptorId -> + firstOrNull { + it.source is Descriptor.Source.Installed && it.source.value.id == descriptorId + } + } + ?: firstOrNull { it.name == result.testGroupName } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt index 3368031b..d692772e 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/RunNetTest.kt @@ -93,12 +93,11 @@ class RunNetTest( test = spec.netTest.test, reportId = reportId, resultId = result.id ?: return, - urlId = - if (event.url.isNotEmpty()) { - getUrlByUrl(event.url).first()?.id - } else { - null - }, + urlId = if (event.url.isNullOrEmpty()) { + null + } else { + getUrlByUrl(event.url).first()?.id + }, ), ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt index 0e5bad24..c99562b7 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt @@ -22,10 +22,10 @@ import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Notic import ooniprobe.composeapp.generated.resources.Dashboard_Running_Stopping_Title import ooniprobe.composeapp.generated.resources.Modal_Error_CantDownloadURLs import ooniprobe.composeapp.generated.resources.Notification_StopTest +import ooniprobe.composeapp.generated.resources.OONIRun_Run import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.app_name import ooniprobe.composeapp.generated.resources.logo -import ooniprobe.composeapp.generated.resources.run_tests import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -58,7 +58,7 @@ fun DashboardScreen( Button( onClick = { onEvent(DashboardViewModel.Event.RunTestsClick) }, ) { - Text(stringResource(Res.string.run_tests)) + Text(stringResource(Res.string.OONIRun_Run)) } } @@ -113,7 +113,7 @@ fun DashboardScreen( state.tests.forEach { (type, tests) -> if (allSectionsHaveValues && tests.isNotEmpty()) { item(type) { - TestDescriptorItem(type) + TestDescriptorSection(type) } } items(tests) { test -> diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt index 7029077f..a8031a47 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorItem.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -15,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.ic_chevron_right -import ooniprobe.composeapp.generated.resources.ic_settings import org.jetbrains.compose.resources.painterResource import org.ooni.probe.data.models.Descriptor @@ -35,25 +33,8 @@ fun TestDescriptorItem(descriptor: Descriptor) { Column( modifier = Modifier.weight(1f), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 2.dp), - ) { - Icon( - // TODO: pick better fallback icon - painter = painterResource(descriptor.icon ?: Res.drawable.ic_settings), - contentDescription = null, - tint = descriptor.color ?: MaterialTheme.colorScheme.onSurface, - modifier = - Modifier - .size(24.dp) - .padding(end = 4.dp), - ) - Text( - descriptor.title(), - style = MaterialTheme.typography.titleMedium, - ) - } + TestDescriptorLabel(descriptor) + descriptor.shortDescription()?.let { shortDescription -> Text( shortDescription, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt index 390cf112..fa72daa1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorLabel.kt @@ -1,31 +1,41 @@ package org.ooni.probe.ui.dashboard -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Ooni_Title -import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Title import ooniprobe.composeapp.generated.resources.Res -import org.jetbrains.compose.resources.stringResource +import ooniprobe.composeapp.generated.resources.ic_settings +import org.jetbrains.compose.resources.painterResource +import org.ooni.probe.data.models.Descriptor @Composable -fun TestDescriptorItem(type: DashboardViewModel.DescriptorType) { - Text( - stringResource( - when (type) { - DashboardViewModel.DescriptorType.Default -> Res.string.Dashboard_RunV2_Ooni_Title - DashboardViewModel.DescriptorType.Installed -> Res.string.Dashboard_RunV2_Title - }, - ).uppercase(), - style = MaterialTheme.typography.labelLarge, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 4.dp), - ) +fun TestDescriptorLabel(descriptor: Descriptor) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 2.dp), + ) { + Icon( + // TODO: pick better fallback icon + painter = painterResource(descriptor.icon ?: Res.drawable.ic_settings), + contentDescription = null, + tint = descriptor.color ?: Color.Unspecified, + modifier = + Modifier + .size(24.dp) + .padding(end = 4.dp), + ) + Text( + descriptor.title(), + style = MaterialTheme.typography.titleMedium, + color = descriptor.color ?: Color.Unspecified, + ) + } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt new file mode 100644 index 00000000..32fcbf42 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/TestDescriptorSection.kt @@ -0,0 +1,31 @@ +package org.ooni.probe.ui.dashboard + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Ooni_Title +import ooniprobe.composeapp.generated.resources.Dashboard_RunV2_Title +import ooniprobe.composeapp.generated.resources.Res +import org.jetbrains.compose.resources.stringResource + +@Composable +fun TestDescriptorSection(type: DashboardViewModel.DescriptorType) { + Text( + stringResource( + when (type) { + DashboardViewModel.DescriptorType.Default -> Res.string.Dashboard_RunV2_Ooni_Title + DashboardViewModel.DescriptorType.Installed -> Res.string.Dashboard_RunV2_Title + }, + ).uppercase(), + style = MaterialTheme.typography.labelLarge, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 4.dp), + ) +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt index 633fc229..592c1219 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/BottomNavigationBar.kt @@ -9,13 +9,13 @@ import androidx.compose.runtime.getValue import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.currentBackStackEntryAsState +import ooniprobe.composeapp.generated.resources.Dashboard_Tab_Label import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_Title import ooniprobe.composeapp.generated.resources.TestResults_Overview_Tab_Label -import ooniprobe.composeapp.generated.resources.dashboard import ooniprobe.composeapp.generated.resources.ic_dashboard import ooniprobe.composeapp.generated.resources.ic_history import ooniprobe.composeapp.generated.resources.ic_settings -import ooniprobe.composeapp.generated.resources.settings import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -63,9 +63,9 @@ fun BottomNavigationBar(navController: NavController) { private val Screen.titleRes get() = when (this) { - Screen.Dashboard -> Res.string.dashboard + Screen.Dashboard -> Res.string.Dashboard_Tab_Label Screen.Results -> Res.string.TestResults_Overview_Tab_Label - Screen.Settings -> Res.string.settings + Screen.Settings -> Res.string.Settings_Title else -> throw IllegalArgumentException("Only main screens allowed in bottom navigation") } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt index d9364bf6..f017e80c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultScreen.kt @@ -1,16 +1,30 @@ package org.ooni.probe.ui.result import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.back +import ooniprobe.composeapp.generated.resources.ic_settings +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.ooni.engine.models.TestType +import org.ooni.probe.data.models.MeasurementWithUrl @Composable fun ResultScreen( @@ -20,7 +34,7 @@ fun ResultScreen( Column { TopAppBar( title = { - Text(state.result?.testGroupName.orEmpty()) + Text(state.result?.descriptor?.title?.invoke().orEmpty()) }, navigationIcon = { IconButton(onClick = { onEvent(ResultViewModel.Event.BackClicked) }) { @@ -30,6 +44,50 @@ fun ResultScreen( ) } }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = state.result?.descriptor?.color + ?: MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) + + if (state.result == null) return@Column + + LazyColumn { + items(state.result.measurements, key = { it.measurement.idOrThrow.value }) { item -> + ResultMeasurementItem(item) + } + } + } +} + +@Composable +fun ResultMeasurementItem(item: MeasurementWithUrl) { + val test = item.measurement.test + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp), + ) { + Icon( + // TODO: Better fallback for nettest icon + // TODO: Web categories icon + painterResource(test.iconRes ?: Res.drawable.ic_settings), + contentDescription = null, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp), + ) + Text( + text = if (test == TestType.WebConnectivity && item.url != null) { + item.url.url + } else if (test is TestType.Experimental) { + test.name + } else { + stringResource(test.labelRes) + }, + maxLines = 1, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt index f3c2ca4f..0ec06a91 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/result/ResultViewModel.kt @@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import org.ooni.probe.data.models.MeasurementModel +import org.ooni.probe.data.models.ResultItem import org.ooni.probe.data.models.ResultModel class ResultViewModel( resultId: ResultModel.Id, onBack: () -> Unit, - getResult: (ResultModel.Id) -> Flow, + getResult: (ResultModel.Id) -> Flow, ) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) @@ -38,10 +40,12 @@ class ResultViewModel( } data class State( - val result: ResultModel?, + val result: ResultItem?, ) sealed interface Event { data object BackClicked : Event + + data class MeasurementClicked(val measurementId: MeasurementModel.Id) : Event } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt index 15fd6652..7a7581d3 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsScreen.kt @@ -1,27 +1,40 @@ package org.ooni.probe.ui.results +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate.Companion.Format +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.format +import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.char import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Snackbar_ResultsNotUploaded_Text import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN +import ooniprobe.composeapp.generated.resources.ic_cloud_off +import ooniprobe.composeapp.generated.resources.measurements_count +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.ui.dashboard.TestDescriptorLabel @Composable fun ResultsScreen( @@ -56,15 +69,16 @@ fun ResultDateHeader(date: LocalDate) { Text( date.format( Format { + monthName(MonthNames.ENGLISH_FULL) // TODO: localize months + char(' ') year() - char('-') - monthNumber() }, ), style = MaterialTheme.typography.labelLarge, modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .padding(horizontal = 16.dp, vertical = 4.dp), ) } @@ -83,16 +97,69 @@ fun ResultItem( }, modifier = Modifier.padding(top = 1.dp), ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .clickable { onResultClick() } - .padding(horizontal = 16.dp, vertical = 8.dp), + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier + .fillMaxWidth() + .clickable { onResultClick() } + .padding(horizontal = 16.dp, vertical = 8.dp), ) { - Text(item.result.testGroupName.orEmpty()) - Text(item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN)) - Text(item.result.startTime.toString()) + Column( + modifier = Modifier.weight(0.66f), + ) { + TestDescriptorLabel(item.descriptor) + + Text( + item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN), + style = MaterialTheme.typography.titleLarge, + ) + + Text( + item.result.startTime.format( + LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour() + char(':') + minute() + char(':') + second() + }, + ), + ) + } + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier.weight(0.34f), + ) { + Text( + pluralStringResource( + Res.plurals.measurements_count, + item.measurementsCount.toInt(), + item.measurementsCount, + ), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(bottom = 2.dp), + ) + if (!item.allMeasurementsUploaded) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painterResource(Res.drawable.ic_cloud_off), + tint = MaterialTheme.typography.labelLarge.color, + contentDescription = null, + modifier = Modifier + .size(16.dp) + .padding(end = 2.dp), + ) + Text( + stringResource(Res.string.Snackbar_ResultsNotUploaded_Text).lowercase(), + style = MaterialTheme.typography.labelLarge, + ) + } + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt index 8268eefe..02a9c569 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/results/ResultsViewModel.kt @@ -48,10 +48,9 @@ class ResultsViewModel( } private val ResultListItem.monthAndYear - get() = - result.startTime.let { startTime -> - LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1) - } + get() = result.startTime.let { startTime -> + LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1) + } data class State( val results: Map>, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt index afa7580c..7ef971c4 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/SettingsScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.settings +import ooniprobe.composeapp.generated.resources.Settings_Title import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource @@ -24,7 +24,7 @@ fun SettingsScreen(onNavigateToSettingsCategory: (SettingsViewModel.Event) -> Un Column { TopAppBar( title = { - Text(stringResource(Res.string.settings)) + Text(stringResource(Res.string.Settings_Title)) }, ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt index 38ae604d..ba051157 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/settings/category/SettingsCategoryScreen.kt @@ -24,8 +24,8 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.Settings_Title import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.settings import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -101,7 +101,7 @@ fun SettingsCategoryScreen( Button( onClick = {}, ) { - Text(stringResource(Res.string.settings)) + Text(stringResource(Res.string.Settings_Title)) } }, ) diff --git a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq index 2e98bf26..23c34ee0 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Measurement.sq @@ -46,3 +46,8 @@ SELECT last_insert_rowid(); selectAll: SELECT * FROM Measurement; + +selectByResultIdWithUrl: +SELECT * FROM Measurement +LEFT JOIN Url ON Measurement.url_id = Url.id +WHERE Measurement.result_id = ?; diff --git a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq index 3ddeb3b9..d6d3c22d 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq @@ -30,14 +30,24 @@ selectLastInsertedRowId: SELECT last_insert_rowid(); selectAll: -SELECT * FROM Result ORDER BY start_time DESC; +SELECT * FROM Result +ORDER BY start_time DESC +LIMIT 100; selectAllWithNetwork: -SELECT Result.*, Network.*, (SELECT COUNT(Measurement.id) FROM Measurement WHERE Measurement.result_id = Result.id) AS measurementsCount +SELECT + Result.*, + Network.*, + (SELECT COUNT(Measurement.id) FROM Measurement WHERE Measurement.result_id = Result.id) AS measurementsCount, + (SELECT COUNT(Measurement.id) FROM Measurement WHERE Measurement.is_uploaded = 0) == 0 AS allMeasurementsUploaded FROM Result LEFT JOIN Network ON Result.network_id = Network.id -ORDER BY Result.start_time DESC; - -selectById: -SELECT Result.* FROM Result WHERE id = ? LIMIT 1; +ORDER BY Result.start_time DESC +LIMIT 100; +selectByIdWithNetwork: +SELECT Result.*, Network.* +FROM Result +LEFT JOIN Network ON Result.network_id = Network.id +WHERE Result.id = ? +LIMIT 1; diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt index 85d47103..e4ed44c7 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt @@ -4,6 +4,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest +import org.ooni.probe.data.models.ResultItem +import org.ooni.testing.factories.DescriptorFactory import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test import kotlin.test.assertEquals @@ -12,25 +14,38 @@ class ResultScreenTest { @Test fun showResult() = runComposeUiTest { - val result = ResultModelFactory.build() + val item = ResultItem( + result = ResultModelFactory.build(), + network = null, + descriptor = DescriptorFactory.buildDescriptorWithInstalled(), + measurements = emptyList(), + ) + var title: String? = null setContent { ResultScreen( - state = ResultViewModel.State(result), + state = ResultViewModel.State(item), onEvent = {}, ) + + title = item.descriptor.title() } - onNodeWithText(result.testGroupName!!).assertExists() + onNodeWithText(title!!).assertExists() } @Test fun pressBack() = runComposeUiTest { val events = mutableListOf() - val result = ResultModelFactory.build() + val item = ResultItem( + result = ResultModelFactory.build(), + network = null, + descriptor = DescriptorFactory.buildDescriptorWithInstalled(), + measurements = emptyList(), + ) setContent { ResultScreen( - state = ResultViewModel.State(result), + state = ResultViewModel.State(item), onEvent = events::add, ) } diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt index 93b33a7a..11a33e9f 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultViewModelTest.kt @@ -4,7 +4,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.ooni.probe.data.models.ResultItem import org.ooni.probe.data.models.ResultModel +import org.ooni.testing.factories.DescriptorFactory import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test import kotlin.test.assertEquals @@ -23,16 +25,21 @@ class ResultViewModelTest { @Test fun getResult() = runTest { - val result = ResultModelFactory.build() - val viewModel = buildViewModel(getResult = { flowOf(result) }) + val item = ResultItem( + result = ResultModelFactory.build(), + network = null, + descriptor = DescriptorFactory.buildDescriptorWithInstalled(), + measurements = emptyList(), + ) + val viewModel = buildViewModel(getResult = { flowOf(item) }) - assertEquals(result, viewModel.state.first().result) + assertEquals(item, viewModel.state.first().result) } private fun buildViewModel( resultId: ResultModel.Id = ResultModel.Id(1234), onBack: () -> Unit = {}, - getResult: (ResultModel.Id) -> Flow = { flowOf(null) }, + getResult: (ResultModel.Id) -> Flow = { flowOf(null) }, ) = ResultViewModel( resultId = resultId, onBack = onBack, diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt index 1542deff..10e906f5 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import kotlinx.datetime.LocalDate import org.ooni.probe.data.models.ResultListItem +import org.ooni.testing.factories.DescriptorFactory import org.ooni.testing.factories.NetworkModelFactory import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test @@ -14,28 +15,31 @@ class ResultsScreenTest { @Test fun showResults() = runComposeUiTest { - val item = - ResultListItem( - result = ResultModelFactory.build(), - network = NetworkModelFactory.build(), - measurementsCount = 4, - ) + val item = ResultListItem( + result = ResultModelFactory.build(), + descriptor = DescriptorFactory.buildDescriptorWithInstalled(), + network = NetworkModelFactory.build(), + measurementsCount = 4, + allMeasurementsUploaded = false, + ) + var title: String? = null + setContent { ResultsScreen( - state = - ResultsViewModel.State( - results = - mapOf( - LocalDate(2024, 1, 1) to listOf(item), - ), - isLoading = false, + state = ResultsViewModel.State( + results = mapOf( + LocalDate(2024, 1, 1) to listOf(item), ), + isLoading = false, + ), onEvent = {}, ) + + title = item.descriptor.title() } - onNodeWithText("2024-01").assertExists() - onNodeWithText(item.result.testGroupName!!).assertExists() + onNodeWithText("January 2024").assertExists() + onNodeWithText(title!!).assertExists() onNodeWithText(item.network!!.networkName!!).assertExists() } @@ -43,27 +47,30 @@ class ResultsScreenTest { fun recordClick() = runComposeUiTest { val events = mutableListOf() - val item = - ResultListItem( - result = ResultModelFactory.build(), - network = NetworkModelFactory.build(), - measurementsCount = 4, - ) + val item = ResultListItem( + result = ResultModelFactory.build(), + descriptor = DescriptorFactory.buildDescriptorWithInstalled(), + network = NetworkModelFactory.build(), + measurementsCount = 4, + allMeasurementsUploaded = false, + ) + var title: String? = null + setContent { ResultsScreen( - state = - ResultsViewModel.State( - results = - mapOf( - LocalDate(2024, 1, 1) to listOf(item), - ), - isLoading = false, + state = ResultsViewModel.State( + results = mapOf( + LocalDate(2024, 1, 1) to listOf(item), ), + isLoading = false, + ), onEvent = events::add, ) + + title = item.descriptor.title() } - onNodeWithText(item.result.testGroupName!!).performClick() + onNodeWithText(title!!).performClick() assertEquals(1, events.size) assertEquals(ResultsViewModel.Event.ResultClick(item), events.first()) }