From 0eec16d664a8a835c97055bf036ec0cf3d0d976a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 7 Aug 2024 18:46:14 +0100 Subject: [PATCH 1/2] Show results --- composeApp/build.gradle.kts | 1 + .../src/androidMain/res/values/strings.xml | 2 - .../values/strings-common.xml | 5 +- .../ooni/probe/data/models/ResultListItem.kt | 9 ++ .../org/ooni/probe/data/models/ResultModel.kt | 6 +- .../org/ooni/probe/data/models/TestResult.kt | 9 -- .../data/repositories/NetworkRepository.kt | 20 ++--- .../data/repositories/ResultRepository.kt | 82 +++++++++++++++---- .../kotlin/org/ooni/probe/di/Dependencies.kt | 19 ++++- .../kotlin/org/ooni/probe/domain/GetResult.kt | 10 +++ .../org/ooni/probe/domain/GetResults.kt | 9 ++ .../org/ooni/probe/shared/DateTimeExt.kt | 23 ++++++ .../ui/navigation/BottomNavigationBar.kt | 5 +- .../ooni/probe/ui/navigation/Navigation.kt | 6 +- .../org/ooni/probe/ui/navigation/Screen.kt | 6 +- .../org/ooni/probe/ui/result/ResultScreen.kt | 6 +- .../ooni/probe/ui/result/ResultViewModel.kt | 18 +++- .../ooni/probe/ui/results/ResultsScreen.kt | 69 ++++++++++++++-- .../ooni/probe/ui/results/ResultsViewModel.kt | 40 ++++++--- .../sqldelight/org/ooni/probe/data/Result.sq | 13 ++- .../ooni/probe/ui/result/ResultScreenTest.kt | 50 ++++++----- .../probe/ui/result/ResultViewModelTest.kt | 34 ++++++-- .../probe/ui/results/ResultsScreenTest.kt | 65 +++++++++++++++ .../testing/factories/ResultModelFactory.kt | 11 ++- 24 files changed, 403 insertions(+), 115 deletions(-) delete mode 100644 composeApp/src/androidMain/res/values/strings.xml create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestResult.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt create mode 100644 composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f08b2dc2..95cf96c4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -103,6 +103,7 @@ kotlin { languageSettings { optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") optIn("androidx.compose.material3.ExperimentalMaterial3Api") + optIn("androidx.compose.foundation.ExperimentalFoundationApi") optIn("androidx.compose.ui.test.ExperimentalTestApi") } } diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml deleted file mode 100644 index 85420055..00000000 --- a/composeApp/src/androidMain/res/values/strings.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml index 3c51645b..272df4c9 100644 --- a/composeApp/src/commonMain/composeResources/values/strings-common.xml +++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml @@ -2,6 +2,9 @@ Run Test Back Dashboard - Test Results Settings + + Test Results + Test Results + Unknown 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 new file mode 100644 index 00000000..26c35feb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultListItem.kt @@ -0,0 +1,9 @@ +package org.ooni.probe.data.models + +data class ResultListItem( + val result: ResultModel, + val network: NetworkModel?, + val measurementsCount: Long +) { + val idOrThrow get() = result.idOrThrow +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt index fd164c4d..4d6c827c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ResultModel.kt @@ -1,11 +1,11 @@ package org.ooni.probe.data.models -import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime data class ResultModel( val id: Id? = null, val testGroupName: String?, - val startTime: Instant?, + val startTime: LocalDateTime, val isViewed: Boolean, val isDone: Boolean, val dataUsageUp: Long?, @@ -17,4 +17,6 @@ data class ResultModel( data class Id( val value: Long, ) + + val idOrThrow get() = id ?: throw IllegalStateException("Id no available") } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestResult.kt deleted file mode 100644 index e664502a..00000000 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/TestResult.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.ooni.probe.data.models - -data class TestResult( - val id: Id, -) { - data class Id( - val value: String, - ) -} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/NetworkRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/NetworkRepository.kt index acacc6e3..a57a050c 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/NetworkRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/NetworkRepository.kt @@ -33,14 +33,14 @@ class NetworkRepository( ) } } - - private fun Network.toModel(): NetworkModel = - NetworkModel( - id = NetworkModel.Id(id), - networkName = network_name, - ip = ip, - asn = asn, - countryCode = country_code, - networkType = network_type?.let(NetworkType::fromValue), - ) } + +fun Network.toModel(): NetworkModel = + NetworkModel( + id = NetworkModel.Id(id), + networkName = network_name, + ip = ip, + asn = asn, + countryCode = country_code, + networkType = network_type?.let(NetworkType::fromValue), + ) 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 60500adf..710b4bfe 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 @@ -3,32 +3,51 @@ package org.ooni.probe.data.repositories import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import kotlinx.datetime.Instant 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.models.NetworkModel +import org.ooni.probe.data.models.ResultListItem import org.ooni.probe.data.models.ResultModel import org.ooni.probe.data.models.TestDescriptorModel +import org.ooni.probe.shared.toEpoch +import org.ooni.probe.shared.toLocalDateTime class ResultRepository( private val database: Database, private val backgroundDispatcher: CoroutineDispatcher, ) { - fun list() = + fun list(): Flow> = database.resultQueries .selectAll() .asFlow() .mapToList(backgroundDispatcher) - .map { list -> list.map { it.toModel() } } + .map { list -> list.mapNotNull { it.toModel() } } + + fun listWithNetwork(): Flow> = + database.resultQueries + .selectAllWithNetwork() + .asFlow() + .mapToList(backgroundDispatcher) + .map { list -> list.mapNotNull { it.toModel() } } + + fun getById(resultId: ResultModel.Id): Flow = + database.resultQueries + .selectById(resultId.value) + .asFlow() + .mapToList(backgroundDispatcher) + .map { it.firstOrNull()?.toModel() } suspend fun create(model: ResultModel) { withContext(backgroundDispatcher) { database.resultQueries.insert( id = model.id?.value, test_group_name = model.testGroupName, - start_time = model.startTime?.toEpochMilliseconds(), + start_time = model.startTime.toEpoch(), is_viewed = if (model.isViewed) 1 else 0, is_done = if (model.isDone) 1 else 0, data_usage_up = model.dataUsageUp, @@ -39,18 +58,47 @@ class ResultRepository( ) } } +} + +private fun Result.toModel(): ResultModel? { + return ResultModel( + id = ResultModel.Id(id), + testGroupName = test_group_name, + startTime = start_time?.toLocalDateTime() ?: return null, + isViewed = is_viewed == 1L, + isDone = is_done == 1L, + dataUsageUp = data_usage_up, + dataUsageDown = data_usage_down, + failureMessage = failure_msg, + networkId = network_id?.let(NetworkModel::Id), + testDescriptorId = descriptor_runId?.let(TestDescriptorModel::Id), + ) +} - private fun Result.toModel(): ResultModel = - ResultModel( - id = ResultModel.Id(id), - testGroupName = test_group_name, - startTime = start_time?.let(Instant::fromEpochMilliseconds), - isViewed = is_viewed == 1L, - isDone = is_done == 1L, - dataUsageUp = data_usage_up, - dataUsageDown = data_usage_down, - failureMessage = failure_msg, - networkId = network_id?.let(NetworkModel::Id), - testDescriptorId = descriptor_runId?.let(TestDescriptorModel::Id), - ) +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() + }, + measurementsCount = measurementsCount + ) } 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 9f2ba9ef..3258bc96 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -10,7 +10,10 @@ import org.ooni.engine.NetworkTypeFinder import org.ooni.engine.OonimkallBridge import org.ooni.engine.TaskEventMapper import org.ooni.probe.Database -import org.ooni.probe.data.models.TestResult +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.ResultRepository +import org.ooni.probe.domain.GetResult +import org.ooni.probe.domain.GetResults import org.ooni.probe.shared.PlatformInfo import org.ooni.probe.ui.dashboard.DashboardViewModel import org.ooni.probe.ui.result.ResultViewModel @@ -32,6 +35,7 @@ class Dependencies( private val json by lazy { buildJson() } private val database by lazy { buildDatabase(databaseDriverFactory) } + private val resultRepository by lazy { ResultRepository(database, backgroundDispatcher) } // Engine @@ -50,16 +54,23 @@ class Dependencies( ) } + // Domain + + private val getResults by lazy { GetResults(resultRepository) } + private val getResult by lazy { GetResult(resultRepository) } + // ViewModels val dashboardViewModel get() = DashboardViewModel(engine) - fun resultsViewModel(goToResult: (TestResult.Id) -> Unit) = ResultsViewModel(goToResult) + fun resultsViewModel( + goToResult: (ResultModel.Id) -> Unit + ) = ResultsViewModel(goToResult, getResults::invoke) fun resultViewModel( - resultId: TestResult.Id, + resultId: ResultModel.Id, onBack: () -> Unit, - ) = ResultViewModel(resultId, onBack) + ) = ResultViewModel(resultId, onBack, getResult::invoke) companion object { @VisibleForTesting diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt new file mode 100644 index 00000000..e77466e3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResult.kt @@ -0,0 +1,10 @@ +package org.ooni.probe.domain + +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.data.repositories.ResultRepository + +class GetResult( + private val resultRepository: ResultRepository, +) { + operator fun invoke(resultId: ResultModel.Id) = resultRepository.getById(resultId) +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt new file mode 100644 index 00000000..446dd63e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetResults.kt @@ -0,0 +1,9 @@ +package org.ooni.probe.domain + +import org.ooni.probe.data.repositories.ResultRepository + +class GetResults( + private val resultRepository: ResultRepository, +) { + operator fun invoke() = resultRepository.listWithNetwork() +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt new file mode 100644 index 00000000..2bea6179 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt @@ -0,0 +1,23 @@ +package org.ooni.probe.shared + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.todayAt +import kotlinx.datetime.todayIn + +fun LocalDateTime.toEpoch() = + toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() + +fun Long.toLocalDateTime() = + Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()) + +fun LocalDate.Companion.today() = + Clock.System.todayIn(TimeZone.currentSystemDefault()) + +fun LocalDateTime.Companion.now() = + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) 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 dedb9c6c..a2751238 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 @@ -10,12 +10,13 @@ import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.currentBackStackEntryAsState import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Tab_Label +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title 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 ooniprobe.composeapp.generated.resources.test_results import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -64,7 +65,7 @@ private val Screen.titleRes get() = when (this) { Screen.Dashboard -> Res.string.dashboard - Screen.Results -> Res.string.test_results + Screen.Results -> Res.string.TestResults_Overview_Tab_Label Screen.Settings -> Res.string.settings else -> throw IllegalArgumentException("Only main screens allowed in bottom navigation") } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt index 569b0cd8..63367cf5 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import org.ooni.probe.data.models.TestResult +import org.ooni.probe.data.models.ResultModel import org.ooni.probe.di.Dependencies import org.ooni.probe.ui.dashboard.DashboardScreen import org.ooni.probe.ui.result.ResultScreen @@ -51,11 +51,11 @@ fun Navigation( route = Screen.Result.NAV_ROUTE, arguments = Screen.Result.ARGUMENTS, ) { entry -> - val resultId = entry.arguments?.getString("resultId") ?: return@composable + val resultId = entry.arguments?.getLong("resultId") ?: return@composable val viewModel = viewModel { dependencies.resultViewModel( - resultId = TestResult.Id(resultId), + resultId = ResultModel.Id(resultId), onBack = { navController.navigateUp() }, ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt index 24e34ab4..96650177 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt @@ -2,7 +2,7 @@ package org.ooni.probe.ui.navigation import androidx.navigation.NavType import androidx.navigation.navArgument -import org.ooni.probe.data.models.TestResult +import org.ooni.probe.data.models.ResultModel sealed class Screen( val route: String, @@ -14,11 +14,11 @@ sealed class Screen( data object Settings : Screen("settings") data class Result( - val resultId: TestResult.Id, + val resultId: ResultModel.Id, ) : Screen("results/${resultId.value}") { companion object { const val NAV_ROUTE = "results/{resultId}" - val ARGUMENTS = listOf(navArgument("resultId") { type = NavType.StringType }) + val ARGUMENTS = listOf(navArgument("resultId") { type = NavType.LongType }) } } } 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 a18e4f1c..e7273519 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 @@ -9,8 +9,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title import ooniprobe.composeapp.generated.resources.back -import ooniprobe.composeapp.generated.resources.test_results import org.jetbrains.compose.resources.stringResource @Composable @@ -21,7 +21,7 @@ fun ResultScreen( Column { TopAppBar( title = { - Text(stringResource(Res.string.test_results)) + Text(state.result?.testGroupName.orEmpty()) }, navigationIcon = { IconButton(onClick = { onEvent(ResultViewModel.Event.BackClicked) }) { @@ -32,7 +32,5 @@ fun ResultScreen( } }, ) - - Text(state.result.id.value) } } 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 32a30aa1..a7df0ac2 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 @@ -2,24 +2,34 @@ package org.ooni.probe.ui.result import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.ooni.probe.data.models.TestResult +import kotlinx.coroutines.flow.update +import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.domain.GetResult +import org.ooni.probe.domain.GetResults class ResultViewModel( - resultId: TestResult.Id, + resultId: ResultModel.Id, onBack: () -> Unit, + getResult: (ResultModel.Id) -> Flow, ) : ViewModel() { private val events = MutableSharedFlow(extraBufferCapacity = 1) - private val _state = MutableStateFlow(State(TestResult(resultId))) + private val _state = MutableStateFlow(State(result = null)) val state = _state.asStateFlow() init { + getResult(resultId) + .onEach { result -> _state.update { it.copy(result = result) } } + .launchIn(viewModelScope) + events .filterIsInstance() .onEach { onBack() } @@ -31,7 +41,7 @@ class ResultViewModel( } data class State( - val result: TestResult, + val result: ResultModel?, ) sealed interface 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 53f706ff..ebb424a6 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,13 +1,27 @@ 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.material3.Button +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDate.Companion.Format +import kotlinx.datetime.format +import kotlinx.datetime.format.char import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.test_results +import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title +import ooniprobe.composeapp.generated.resources.TestResults_UnknownASN import org.jetbrains.compose.resources.stringResource +import org.ooni.probe.data.models.ResultListItem @Composable fun ResultsScreen( @@ -17,14 +31,57 @@ fun ResultsScreen( Column { TopAppBar( title = { - Text(stringResource(Res.string.test_results)) + Text(stringResource(Res.string.TestResults_Overview_Title)) }, ) - state.results.forEach { result -> - Button(onClick = { onEvent(ResultsViewModel.Event.ResultClick(result)) }) { - Text(result.id.value) + LazyColumn { + state.results.forEach { (date, results) -> + stickyHeader(key = date) { + ResultDateHeader(date) + } + items(items = results, key = { it.idOrThrow }) { result -> + ResultItem( + item = result, + onResultClick = { onEvent(ResultsViewModel.Event.ResultClick(result)) } + ) + } } } } } + +@Composable +fun ResultDateHeader(date: LocalDate) { + Text( + date.format(Format { year(); char('-'); monthNumber() }), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(horizontal = 16.dp, vertical = 4.dp) + ) +} + +@Composable +fun ResultItem( + item: ResultListItem, + onResultClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { onResultClick() } + .background( + if (item.result.isViewed) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceVariant + } + ) + ) { + Text(item.result.testGroupName.orEmpty()) + Text(item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN)) + Text(item.result.startTime.toString()) + } +} 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 644daa8e..05ed0fce 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 @@ -2,29 +2,45 @@ package org.ooni.probe.ui.results import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.ooni.probe.data.models.TestResult +import kotlinx.coroutines.flow.update +import kotlinx.datetime.LocalDate +import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.data.models.ResultModel +import org.ooni.probe.domain.GetResults class ResultsViewModel( - goToResult: (TestResult.Id) -> Unit, + goToResult: (ResultModel.Id) -> Unit, + getResults: () -> Flow>, ) : ViewModel() { + private val events = MutableSharedFlow(extraBufferCapacity = 1) - private val _state = - MutableStateFlow( - State(results = listOf(TestResult(TestResult.Id("123456")))), + private val _state = MutableStateFlow( + State( + results = emptyMap(), + isLoading = true ) + ) val state = _state.asStateFlow() init { + getResults() + .onEach { results -> + val groupedResults = results.groupBy { it.monthAndYear } + _state.update { it.copy(results = groupedResults) } + } + .launchIn(viewModelScope) + events .filterIsInstance() - .onEach { goToResult(it.result.id) } + .onEach { goToResult(it.result.idOrThrow) } .launchIn(viewModelScope) } @@ -32,13 +48,17 @@ class ResultsViewModel( events.tryEmit(event) } + private val ResultListItem.monthAndYear + get() = result.startTime.let { startTime -> + LocalDate(year = startTime.year, month = startTime.month, dayOfMonth = 1) + } + data class State( - val results: List, + val results: Map>, + val isLoading: Boolean, ) sealed interface Event { - data class ResultClick( - val result: TestResult, - ) : Event + data class ResultClick(val result: ResultListItem) : Event } } 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 5ec97105..de6da2e4 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Result.sq @@ -27,4 +27,15 @@ INSERT INTO Result ( ) VALUES (?,?,?,?,?,?,?,?,?,?); selectAll: -SELECT * FROM Result; +SELECT * FROM Result ORDER BY start_time DESC; + +selectAllWithNetwork: +SELECT Result.*, Network.*, (SELECT COUNT(Measurement.id) FROM Measurement WHERE Measurement.result_id = Result.id) AS measurementsCount +FROM Result +LEFT JOIN Network ON Result.network_id = Network.id +LEFT JOIN Measurement ON Measurement.result_id = Result.id +ORDER BY Result.start_time DESC; + +selectById: +SELECT Result.* FROM Result WHERE 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 71e6f6c0..e330f165 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,39 +4,37 @@ 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.TestResult +import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test import kotlin.test.assertEquals class ResultScreenTest { @Test - fun showResult() = - runComposeUiTest { - val result = TestResult(TestResult.Id("ABCDEF")) - setContent { - ResultScreen( - state = ResultViewModel.State(result), - onEvent = {}, - ) - } - - onNodeWithText(result.id.value).assertExists() + fun showResult() = runComposeUiTest { + val result = ResultModelFactory.build() + setContent { + ResultScreen( + state = ResultViewModel.State(result), + onEvent = {}, + ) } - @Test - fun pressBack() = - runComposeUiTest { - val events = mutableListOf() - val result = TestResult(TestResult.Id("ABCDEF")) - setContent { - ResultScreen( - state = ResultViewModel.State(result), - onEvent = events::add, - ) - } + onNodeWithText(result.testGroupName!!).assertExists() + } - onNodeWithContentDescription("Back").performClick() - assertEquals(1, events.size) - assertEquals(ResultViewModel.Event.BackClicked, events.first()) + @Test + fun pressBack() = runComposeUiTest { + val events = mutableListOf() + val result = ResultModelFactory.build() + setContent { + ResultScreen( + state = ResultViewModel.State(result), + onEvent = events::add, + ) } + + onNodeWithContentDescription("Back").performClick() + assertEquals(1, events.size) + assertEquals(ResultViewModel.Event.BackClicked, events.first()) + } } 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 e6c08302..2cc1a626 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 @@ -1,21 +1,41 @@ package org.ooni.probe.ui.result -import org.ooni.probe.data.models.TestResult +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.ResultModel +import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class ResultViewModelTest { + @Test fun backClicked() { var backPressed = false - - val viewModel = - ResultViewModel( - resultId = TestResult.Id("1234"), - onBack = { backPressed = true }, - ) + val viewModel = buildViewModel(onBack = { backPressed = true }) viewModel.onEvent(ResultViewModel.Event.BackClicked) assertTrue(backPressed) } + + @Test + fun getResult() = runTest { + val result = ResultModelFactory.build() + val viewModel = buildViewModel(getResult = { flowOf(result) }) + + assertEquals(result, viewModel.state.first().result) + } + + private fun buildViewModel( + resultId: ResultModel.Id = ResultModel.Id(1234), + onBack: () -> Unit = {}, + getResult: (ResultModel.Id) -> Flow = { flowOf(null) } + ) = ResultViewModel( + resultId = resultId, + onBack = onBack, + getResult = getResult, + ) } 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 new file mode 100644 index 00000000..b770b2ab --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/results/ResultsScreenTest.kt @@ -0,0 +1,65 @@ +package org.ooni.probe.ui.results + +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 kotlinx.datetime.LocalDate +import org.ooni.probe.data.models.ResultListItem +import org.ooni.probe.ui.result.ResultScreen +import org.ooni.probe.ui.result.ResultViewModel +import org.ooni.testing.factories.NetworkModelFactory +import org.ooni.testing.factories.ResultModelFactory +import kotlin.test.Test +import kotlin.test.assertEquals + +class ResultsScreenTest { + @Test + fun showResults() = runComposeUiTest { + val item = ResultListItem( + result = ResultModelFactory.build(), + network = NetworkModelFactory.build(), + measurementsCount = 4 + ) + setContent { + ResultsScreen( + state = ResultsViewModel.State( + results = mapOf( + LocalDate(2024, 1, 1) to listOf(item) + ), + isLoading = false + ), + onEvent = {}, + ) + } + + onNodeWithText("2024-01").assertExists() + onNodeWithText(item.result.testGroupName!!).assertExists() + onNodeWithText(item.network!!.networkName!!).assertExists() + } + + @Test + fun recordClick() = runComposeUiTest { + val events = mutableListOf() + val item = ResultListItem( + result = ResultModelFactory.build(), + network = NetworkModelFactory.build(), + measurementsCount = 4 + ) + setContent { + ResultsScreen( + state = ResultsViewModel.State( + results = mapOf( + LocalDate(2024, 1, 1) to listOf(item) + ), + isLoading = false + ), + onEvent = events::add, + ) + } + + onNodeWithText(item.result.testGroupName!!).performClick() + assertEquals(1, events.size) + assertEquals(ResultsViewModel.Event.ResultClick(item), events.first()) + } +} diff --git a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt index 2baca511..6adacaf1 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ResultModelFactory.kt @@ -1,15 +1,18 @@ package org.ooni.testing.factories -import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.atTime import org.ooni.probe.data.models.NetworkModel import org.ooni.probe.data.models.ResultModel import org.ooni.probe.data.models.TestDescriptorModel +import org.ooni.probe.shared.today object ResultModelFactory { fun build( - id: ResultModel.Id? = null, - testGroupName: String? = null, - startTime: Instant? = null, + id: ResultModel.Id? = ResultModel.Id(1234L), + testGroupName: String? = "web_connectivity", + startTime: LocalDateTime = LocalDate.today().atTime(0, 0), isViewed: Boolean = false, isDone: Boolean = false, dataUsageUp: Long? = null, From 9450a46b958e68b4f7be0e45e3a53961d55c824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Mon, 12 Aug 2024 10:45:46 +0100 Subject: [PATCH 2/2] Fix kotlin lint --- .../ooni/probe/data/models/ResultListItem.kt | 2 +- .../data/repositories/ResultRepository.kt | 48 ++++----- .../kotlin/org/ooni/probe/di/Dependencies.kt | 4 +- .../org/ooni/probe/shared/DateTimeExt.kt | 13 +-- .../ui/navigation/BottomNavigationBar.kt | 1 - .../org/ooni/probe/ui/result/ResultScreen.kt | 1 - .../ooni/probe/ui/result/ResultViewModel.kt | 3 - .../ooni/probe/ui/results/ResultsScreen.kt | 42 ++++---- .../ooni/probe/ui/results/ResultsViewModel.kt | 20 ++-- .../ooni/probe/ui/result/ResultScreenTest.kt | 48 ++++----- .../probe/ui/result/ResultViewModelTest.kt | 14 +-- .../probe/ui/results/ResultsScreenTest.kt | 97 ++++++++++--------- 12 files changed, 149 insertions(+), 144 deletions(-) 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 26c35feb..f36e81dd 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 @@ -3,7 +3,7 @@ package org.ooni.probe.data.models data class ResultListItem( val result: ResultModel, val network: NetworkModel?, - val measurementsCount: Long + val measurementsCount: Long, ) { val idOrThrow get() = result.idOrThrow } 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 710b4bfe..8c890972 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 @@ -77,28 +77,30 @@ private fun Result.toModel(): ResultModel? { 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() - }, - measurementsCount = measurementsCount + 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, ) } 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 3258bc96..7090a9ba 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt @@ -63,9 +63,7 @@ class Dependencies( val dashboardViewModel get() = DashboardViewModel(engine) - fun resultsViewModel( - goToResult: (ResultModel.Id) -> Unit - ) = ResultsViewModel(goToResult, getResults::invoke) + fun resultsViewModel(goToResult: (ResultModel.Id) -> Unit) = ResultsViewModel(goToResult, getResults::invoke) fun resultViewModel( resultId: ResultModel.Id, diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt index 2bea6179..3beed461 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/shared/DateTimeExt.kt @@ -7,17 +7,12 @@ import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime -import kotlinx.datetime.todayAt import kotlinx.datetime.todayIn -fun LocalDateTime.toEpoch() = - toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() +fun LocalDateTime.toEpoch() = toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() -fun Long.toLocalDateTime() = - Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()) +fun Long.toLocalDateTime() = Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()) -fun LocalDate.Companion.today() = - Clock.System.todayIn(TimeZone.currentSystemDefault()) +fun LocalDate.Companion.today() = Clock.System.todayIn(TimeZone.currentSystemDefault()) -fun LocalDateTime.Companion.now() = - Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) +fun LocalDateTime.Companion.now() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) 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 a2751238..633fc229 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 @@ -11,7 +11,6 @@ import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.currentBackStackEntryAsState import ooniprobe.composeapp.generated.resources.Res import ooniprobe.composeapp.generated.resources.TestResults_Overview_Tab_Label -import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title import ooniprobe.composeapp.generated.resources.dashboard import ooniprobe.composeapp.generated.resources.ic_dashboard import ooniprobe.composeapp.generated.resources.ic_history 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 e7273519..d9364bf6 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 @@ -9,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import ooniprobe.composeapp.generated.resources.Res -import ooniprobe.composeapp.generated.resources.TestResults_Overview_Title import ooniprobe.composeapp.generated.resources.back import org.jetbrains.compose.resources.stringResource 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 a7df0ac2..f3c2ca4f 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,10 +10,7 @@ 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.ResultListItem import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.domain.GetResult -import org.ooni.probe.domain.GetResults class ResultViewModel( resultId: ResultModel.Id, 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 ebb424a6..7bbc6072 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 @@ -43,7 +43,7 @@ fun ResultsScreen( items(items = results, key = { it.idOrThrow }) { result -> ResultItem( item = result, - onResultClick = { onEvent(ResultsViewModel.Event.ResultClick(result)) } + onResultClick = { onEvent(ResultsViewModel.Event.ResultClick(result)) }, ) } } @@ -54,31 +54,39 @@ fun ResultsScreen( @Composable fun ResultDateHeader(date: LocalDate) { Text( - date.format(Format { year(); char('-'); monthNumber() }), + date.format( + Format { + year() + char('-') + monthNumber() + }, + ), style = MaterialTheme.typography.labelLarge, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(horizontal = 16.dp, vertical = 4.dp) + modifier = + Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(horizontal = 16.dp, vertical = 4.dp), ) } @Composable fun ResultItem( item: ResultListItem, - onResultClick: () -> Unit + onResultClick: () -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .clickable { onResultClick() } - .background( - if (item.result.isViewed) { - MaterialTheme.colorScheme.surface - } else { - MaterialTheme.colorScheme.surfaceVariant - } - ) + modifier = + Modifier + .fillMaxWidth() + .clickable { onResultClick() } + .background( + if (item.result.isViewed) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + ), ) { Text(item.result.testGroupName.orEmpty()) Text(item.network?.networkName ?: stringResource(Res.string.TestResults_UnknownASN)) 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 05ed0fce..8268eefe 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 @@ -13,21 +13,20 @@ import kotlinx.coroutines.flow.update import kotlinx.datetime.LocalDate import org.ooni.probe.data.models.ResultListItem import org.ooni.probe.data.models.ResultModel -import org.ooni.probe.domain.GetResults class ResultsViewModel( goToResult: (ResultModel.Id) -> Unit, getResults: () -> Flow>, ) : ViewModel() { - private val events = MutableSharedFlow(extraBufferCapacity = 1) - private val _state = MutableStateFlow( - State( - results = emptyMap(), - isLoading = true + private val _state = + MutableStateFlow( + State( + results = emptyMap(), + isLoading = true, + ), ) - ) val state = _state.asStateFlow() init { @@ -49,9 +48,10 @@ 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/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/ui/result/ResultScreenTest.kt index e330f165..85d47103 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 @@ -10,31 +10,33 @@ import kotlin.test.assertEquals class ResultScreenTest { @Test - fun showResult() = runComposeUiTest { - val result = ResultModelFactory.build() - setContent { - ResultScreen( - state = ResultViewModel.State(result), - onEvent = {}, - ) - } + fun showResult() = + runComposeUiTest { + val result = ResultModelFactory.build() + setContent { + ResultScreen( + state = ResultViewModel.State(result), + onEvent = {}, + ) + } - onNodeWithText(result.testGroupName!!).assertExists() - } + onNodeWithText(result.testGroupName!!).assertExists() + } @Test - fun pressBack() = runComposeUiTest { - val events = mutableListOf() - val result = ResultModelFactory.build() - setContent { - ResultScreen( - state = ResultViewModel.State(result), - onEvent = events::add, - ) - } + fun pressBack() = + runComposeUiTest { + val events = mutableListOf() + val result = ResultModelFactory.build() + setContent { + ResultScreen( + state = ResultViewModel.State(result), + onEvent = events::add, + ) + } - onNodeWithContentDescription("Back").performClick() - assertEquals(1, events.size) - assertEquals(ResultViewModel.Event.BackClicked, events.first()) - } + onNodeWithContentDescription("Back").performClick() + assertEquals(1, events.size) + assertEquals(ResultViewModel.Event.BackClicked, events.first()) + } } 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 2cc1a626..93b33a7a 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 @@ -11,7 +11,6 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class ResultViewModelTest { - @Test fun backClicked() { var backPressed = false @@ -22,17 +21,18 @@ class ResultViewModelTest { } @Test - fun getResult() = runTest { - val result = ResultModelFactory.build() - val viewModel = buildViewModel(getResult = { flowOf(result) }) + fun getResult() = + runTest { + val result = ResultModelFactory.build() + val viewModel = buildViewModel(getResult = { flowOf(result) }) - assertEquals(result, viewModel.state.first().result) - } + assertEquals(result, 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 b770b2ab..1542deff 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 @@ -1,13 +1,10 @@ package org.ooni.probe.ui.results -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 kotlinx.datetime.LocalDate import org.ooni.probe.data.models.ResultListItem -import org.ooni.probe.ui.result.ResultScreen -import org.ooni.probe.ui.result.ResultViewModel import org.ooni.testing.factories.NetworkModelFactory import org.ooni.testing.factories.ResultModelFactory import kotlin.test.Test @@ -15,51 +12,59 @@ import kotlin.test.assertEquals class ResultsScreenTest { @Test - fun showResults() = runComposeUiTest { - val item = ResultListItem( - result = ResultModelFactory.build(), - network = NetworkModelFactory.build(), - measurementsCount = 4 - ) - setContent { - ResultsScreen( - state = ResultsViewModel.State( - results = mapOf( - LocalDate(2024, 1, 1) to listOf(item) - ), - isLoading = false - ), - onEvent = {}, - ) - } + fun showResults() = + runComposeUiTest { + val item = + ResultListItem( + result = ResultModelFactory.build(), + network = NetworkModelFactory.build(), + measurementsCount = 4, + ) + setContent { + ResultsScreen( + state = + ResultsViewModel.State( + results = + mapOf( + LocalDate(2024, 1, 1) to listOf(item), + ), + isLoading = false, + ), + onEvent = {}, + ) + } - onNodeWithText("2024-01").assertExists() - onNodeWithText(item.result.testGroupName!!).assertExists() - onNodeWithText(item.network!!.networkName!!).assertExists() - } + onNodeWithText("2024-01").assertExists() + onNodeWithText(item.result.testGroupName!!).assertExists() + onNodeWithText(item.network!!.networkName!!).assertExists() + } @Test - fun recordClick() = runComposeUiTest { - val events = mutableListOf() - val item = ResultListItem( - result = ResultModelFactory.build(), - network = NetworkModelFactory.build(), - measurementsCount = 4 - ) - setContent { - ResultsScreen( - state = ResultsViewModel.State( - results = mapOf( - LocalDate(2024, 1, 1) to listOf(item) - ), - isLoading = false - ), - onEvent = events::add, - ) - } + fun recordClick() = + runComposeUiTest { + val events = mutableListOf() + val item = + ResultListItem( + result = ResultModelFactory.build(), + network = NetworkModelFactory.build(), + measurementsCount = 4, + ) + setContent { + ResultsScreen( + state = + ResultsViewModel.State( + results = + mapOf( + LocalDate(2024, 1, 1) to listOf(item), + ), + isLoading = false, + ), + onEvent = events::add, + ) + } - onNodeWithText(item.result.testGroupName!!).performClick() - assertEquals(1, events.size) - assertEquals(ResultsViewModel.Event.ResultClick(item), events.first()) - } + onNodeWithText(item.result.testGroupName!!).performClick() + assertEquals(1, events.size) + assertEquals(ResultsViewModel.Event.ResultClick(item), events.first()) + } }