Skip to content

Commit

Permalink
Added progress indicator on Insight Screen (#3484)
Browse files Browse the repository at this point in the history
* added progress indicator while quering/fetching the data on insight screen

* added tests

* feedback updated

* tests updated

* spotless ran

* Trigger CI

* Fix build ✅

* Fix broken test in RegisterScreenTest

* [wip] Log quest tests ran in ci

* Replace activityScenario with robolectric activityController

for unit tests failing with OOM in ci

* Update ci.yml

* Separate unit test from emulator tests

* Add timeout to fail for tests taking longer than 15 minutes

---------

Co-authored-by: Martin Ndegwa <[email protected]>
Co-authored-by: Martin Ndegwa <[email protected]>
Co-authored-by: Peter Lubell-Doughtie <[email protected]>
Co-authored-by: L≡ZRS <[email protected]>
  • Loading branch information
5 people authored Nov 21, 2024
1 parent 9293402 commit 8ee171a
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 160 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ jobs:

- name: Spotless check quest application
run: ./gradlew -PlocalPropertiesFile=local.properties :quest:spotlessCheck --stacktrace :quest:ktlintCheck --stacktrace
working-directory: android
working-directory: android

- name: Load AVD cache
uses: actions/cache@v4
Expand Down Expand Up @@ -272,7 +272,11 @@ jobs:
force-avd-creation: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew clean -PlocalPropertiesFile=local.properties :quest:fhircoreJacocoReport --info -Pandroid.testInstrumentationRunnerArguments.notPackage=org.smartregister.fhircore.quest.performance
script: ./gradlew clean -PlocalPropertiesFile=local.properties :quest:connectedOpensrpDebugAndroidTest --stacktrace -Pandroid.testInstrumentationRunnerArguments.notPackage=org.smartregister.fhircore.quest.performance

- name: Test UnitTest
run: ./gradlew clean -PlocalPropertiesFile=local.properties :quest:testOpensrpDebugUnitTest --stacktrace
working-directory: android

- name: Run Quest module unit and instrumentation tests and generate aggregated coverage report (Disabled)
if: false
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<string name="syncing_retry">Reintentar sincronización</string>
<string name="syncing_in_progress">Sincronización en curso</string>
<string name="loading">Cargando</string>
<string name="loading_ellipsis">Cargando…</string>
<string name="error_logging_out">Mensaje de error al cerrar sesión: %1$s</string>
<string name="cannot_logout_user">No se puede cerrar sesión: ya se ha cerrado sesión o el dispositivo está desconectado.</string>
<string name="error_loading_config_http_error">No se pudo cargar la configuración. Inténtalo de nuevo más tarde</string>
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<string name="syncing_retry">Réessayer la synchronisation</string>
<string name="syncing_in_progress">Synchronisation en cours</string>
<string name="loading">Chargement</string>
<string name="loading_ellipsis">Chargement…</string>
<string name="error_logging_out">Message d\'erreur de déconnexion %1$s</string>
<string name="cannot_logout_user">Impossible de se déconnecter : Déjà déconnecté ou l\'appareil est hors ligne.</string>
<string name="error_loading_config_http_error">Impossible de charger la configuration. Veuillez réessayer plus tard</string>
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values-in/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<string name="syncing_retry">Coba lagi sinkronisasi</string>
<string name="syncing_in_progress">Sinkronisasi sedang berlangsung</string>
<string name="loading">Memuat</string>
<string name="loading_ellipsis">Memuat…</string>
<string name="error_logging_out">Pesan kesalahan logout: %1$s</string>
<string name="cannot_logout_user">Tidak dapat logout: Sudah logout atau perangkat sedang offline.</string>
<string name="error_loading_config_http_error">Tidak dapat memuat konfigurasi. Silakan coba lagi nanti</string>
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values-sw/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<string name="syncing_retry">Jaribu kusawazisha tena</string>
<string name="syncing_in_progress">Kusawazisha inaendelea</string>
<string name="loading">Inapakia</string>
<string name="loading_ellipsis">Inapakia…</string>
<string name="error_loading_form">Hitilafu kounyesha fomu</string>
<string name="error_saving_form">Hitilafu imetokea, haiwezi kuhifadhi fomu</string>
<string name="replace_photo">Badilisha picha</string>
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<string name="syncing_retry">Retry sync</string>
<string name="syncing_in_progress">Sync in progress</string>
<string name="loading">Loading</string>
<string name="loading_ellipsis">Loading…</string>
<string name="error_logging_out">Logout error message: %1$s</string>
<string name="cannot_logout_user">Unable to logout: Already logged out or device is offline.</string>
<string name="error_loading_config_http_error">Could not load configuration. Please try again later</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.integration.ui.usersetting

import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
Expand All @@ -30,6 +31,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.util.extension.DEFAULT_FORMAT_SDF_DD_MM_YYYY
import org.smartregister.fhircore.quest.ui.usersetting.CIRCULAR_PROGRESS_INDICATOR
import org.smartregister.fhircore.quest.ui.usersetting.INSIGHT_UNSYNCED_DATA
import org.smartregister.fhircore.quest.ui.usersetting.UserSettingInsightScreen

Expand Down Expand Up @@ -100,8 +102,19 @@ class UserSettingInsightScreenTest {
composeRule.onNodeWithTag(INSIGHT_UNSYNCED_DATA).assertDoesNotExist()
}

@Test
fun testProgressIndicatorShowWhenFetchingTheData() {
val unsyncedResources = emptyList<Pair<String, Int>>()
initComposable(
unsyncedResourcesFlow = MutableStateFlow(unsyncedResources),
showProgressIndicator = true,
)
composeRule.onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists().assertIsDisplayed()
}

private fun initComposable(
unsyncedResourcesFlow: MutableSharedFlow<List<Pair<String, Int>>> = MutableSharedFlow(),
showProgressIndicator: Boolean = false,
) {
scenario.onActivity { activity ->
activity.setContent {
Expand All @@ -118,6 +131,7 @@ class UserSettingInsightScreenTest {
buildDate = "29 jan 2023",
unsyncedResourcesFlow = unsyncedResourcesFlow,
navController = rememberNavController(),
showProgressIndicator = showProgressIndicator,
onRefreshRequest = {},
dateFormat = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
Expand Down Expand Up @@ -56,6 +57,8 @@ class UserInsightScreenFragment : Fragment() {
buildDate = userSettingViewModel.getBuildDate(),
unsyncedResourcesFlow = userSettingViewModel.unsyncedResourcesMutableSharedFlow,
navController = findNavController(),
showProgressIndicator =
userSettingViewModel.showProgressIndicatorFlow.collectAsState().value,
onRefreshRequest = { userSettingViewModel.fetchUnsyncedResources() },
dateFormat = userSettingViewModel.getDateFormat(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
Expand Down Expand Up @@ -77,6 +78,7 @@ import org.smartregister.fhircore.engine.util.extension.formatDate

const val USER_INSIGHT_TOP_APP_BAR = "userInsightToAppBar"
const val INSIGHT_UNSYNCED_DATA = "insightUnsyncedData"
const val CIRCULAR_PROGRESS_INDICATOR = "progressIndicator"

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
Expand All @@ -94,6 +96,7 @@ fun UserSettingInsightScreen(
dividerColor: Color = DividerColor,
unsyncedResourcesFlow: MutableSharedFlow<List<Pair<String, Int>>>,
navController: NavController,
showProgressIndicator: Boolean = false,
onRefreshRequest: () -> Unit,
dateFormat: String = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
) {
Expand Down Expand Up @@ -123,7 +126,32 @@ fun UserSettingInsightScreen(
horizontalAlignment = Alignment.Start,
contentPadding = PaddingValues(vertical = 24.dp, horizontal = 16.dp),
) {
if (unsyncedResources.isNotEmpty()) {
if (showProgressIndicator) {
item {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(id = R.string.loading_ellipsis),
style = TextStyle(color = Color.Black, fontSize = 20.sp),
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.width(8.dp))
CircularProgressIndicator(
modifier =
Modifier.testTag(CIRCULAR_PROGRESS_INDICATOR).size(24.dp).wrapContentWidth(),
strokeWidth = 1.6.dp,
)
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
Divider(color = dividerColor)
Spacer(modifier = Modifier.height(24.dp))
}
} else if (unsyncedResources.isNotEmpty()) {
item {
Text(
text = stringResource(id = R.string.unsynced_resources),
Expand Down Expand Up @@ -368,6 +396,7 @@ fun UserSettingInsightScreenPreview() {
buildDate = "29 Jan 2023",
unsyncedResourcesFlow = MutableSharedFlow(),
navController = rememberNavController(),
showProgressIndicator = true,
onRefreshRequest = {},
dateFormat = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ import org.smartregister.fhircore.engine.ui.theme.LighterBlue
import org.smartregister.fhircore.engine.ui.theme.LoginDarkColor
import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated
import org.smartregister.fhircore.engine.util.extension.appVersion
import org.smartregister.fhircore.quest.ui.pin.CIRCULAR_PROGRESS_INDICATOR

const val RESET_DATABASE_DIALOG = "resetDatabaseDialog"
const val USER_SETTING_ROW_LOGOUT = "userSettingRowLogout"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import org.junit.Rule
import org.junit.runner.RunWith
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.junit.rules.TimeoutRule
import org.robolectric.util.ReflectionHelpers
import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD
import org.smartregister.fhircore.engine.util.extension.formatDate
Expand All @@ -69,6 +70,8 @@ abstract class RobolectricTest {

@get:Rule(order = 20) val fhirEngineProviderTestRule = FhirEngineProviderTestRule()

@get:Rule(order = 38) val globalTimeoutRule: TimeoutRule = TimeoutRule.seconds(900) // 15 minutes

/** Get the liveData value by observing but wait for 3 seconds if not ready then stop observing */
@Throws(InterruptedException::class)
fun <T> getLiveDataValue(liveData: LiveData<T>): T? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,124 +16,95 @@

package org.smartregister.fhircore.quest.ui.usersetting

import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.commitNow
import androidx.navigation.Navigation
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
import androidx.work.WorkManager
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.mockk.mockk
import io.mockk.spyk
import javax.inject.Inject
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.robolectric.Robolectric
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceService
import org.smartregister.fhircore.engine.datastore.PreferenceDataStore
import org.smartregister.fhircore.engine.sync.SyncBroadcaster
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.quest.app.AppConfigService
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.util.test.HiltActivityForTest
import org.smartregister.fhircore.quest.app.fakes.Faker
import org.smartregister.fhircore.quest.launchFragmentInHiltContainer
import org.smartregister.fhircore.quest.robolectric.RobolectricTest
import org.smartregister.fhircore.quest.ui.login.AccountAuthenticator

@HiltAndroidTest
class UserInsightScreenFragmentTest : RobolectricTest() {

@get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)

@BindValue var configurationRegistry = Faker.buildTestConfigurationRegistry()
@BindValue
val configurationRegistry: ConfigurationRegistry = Faker.buildTestConfigurationRegistry()

@Inject lateinit var testDispatcherProvider: DispatcherProvider
@BindValue lateinit var userSettingViewModel: UserSettingViewModel

@Inject lateinit var workManager: WorkManager

@Inject lateinit var preferenceDataStore: PreferenceDataStore
private val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
private val context = ApplicationProvider.getApplicationContext<HiltTestApplication>()
private val resourceService: FhirResourceService = mockk()
private val application: Context = ApplicationProvider.getApplicationContext()
private var sharedPreferencesHelper: SharedPreferencesHelper
private var configService: ConfigService
private var fhirResourceDataSource: FhirResourceDataSource
private lateinit var syncBroadcaster: SyncBroadcaster
private lateinit var userSettingViewModel: UserSettingViewModel
private lateinit var accountAuthenticator: AccountAuthenticator
private lateinit var secureSharedPreference: SecureSharedPreference

init {
sharedPreferencesHelper = SharedPreferencesHelper(context = context, gson = mockk())
configService = AppConfigService(context = context)
fhirResourceDataSource = spyk(FhirResourceDataSource(resourceService))
}
private val activityController = Robolectric.buildActivity(HiltActivityForTest::class.java)

@Before
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun setUp() {
hiltRule.inject()
accountAuthenticator = mockk()
secureSharedPreference = mockk()
sharedPreferencesHelper = mockk()
syncBroadcaster =
SyncBroadcaster(
configurationRegistry,
fhirEngine = mockk(),
dispatcherProvider = testDispatcherProvider,
syncListenerManager = mockk(relaxed = true),
workManager = workManager,
context = application,
)

userSettingViewModel =
UserSettingViewModel(
fhirEngine = mockk(),
syncBroadcaster = syncBroadcaster,
accountAuthenticator = accountAuthenticator,
secureSharedPreference = secureSharedPreference,
sharedPreferencesHelper = sharedPreferencesHelper,
preferenceDataStore = preferenceDataStore,
configurationRegistry = configurationRegistry,
workManager = mockk(relaxed = true),
dispatcherProvider = testDispatcherProvider,
)
userSettingViewModel = mockk(relaxed = true)
}

@Test
fun testUserSettingViewModelReturnsCorrectViewModelInstance() {
launchFragmentInHiltContainer<UserInsightScreenFragment>(
Bundle(),
R.style.AppTheme,
navController,
) {
Assert.assertNotNull(this)
Assert.assertNotNull((this as UserInsightScreenFragment).userSettingViewModel)
Assert.assertEquals(userSettingViewModel, userSettingViewModel)
activityController.create().resume()
val activity = activityController.get()
val navHostController = TestNavHostController(activity)
val fragment =
UserInsightScreenFragment().apply {
viewLifecycleOwnerLiveData.observeForever {
if (it != null) {
navHostController.setGraph(
org.smartregister.fhircore.quest.R.navigation.application_nav_graph,
)
Navigation.setViewNavController(requireView(), navHostController)
}
}
}
activity.supportFragmentManager.run {
commitNow {
add(android.R.id.content, fragment, UserInsightScreenFragment::class.java.simpleName)
}
executePendingTransactions()
}
Assert.assertEquals(userSettingViewModel, fragment.userSettingViewModel)
}

@Test
fun testUserSettinViewIsRenderedCorrectlyByOnCreateView() {
launchFragmentInHiltContainer<UserInsightScreenFragment>(
Bundle(),
R.style.AppTheme,
navController,
) {
this.view!!.findViewWithTag<View>(USER_INSIGHT_TOP_APP_BAR)?.let {
Assert.assertTrue(it.isVisible)
Assert.assertTrue(it.isShown)
fun testUserInsightScreenViewIsRenderedCorrectlyByOnCreateView() {
activityController.create().resume()
val activity = activityController.get()
val navHostController = TestNavHostController(activity)
val fragment =
UserInsightScreenFragment().apply {
viewLifecycleOwnerLiveData.observeForever {
if (it != null) {
navHostController.setGraph(
org.smartregister.fhircore.quest.R.navigation.application_nav_graph,
)
Navigation.setViewNavController(requireView(), navHostController)
}
}
}
activity.supportFragmentManager.run {
commitNow {
add(android.R.id.content, fragment, UserInsightScreenFragment::class.java.simpleName)
}
executePendingTransactions()
}
fragment.view!!.findViewWithTag<View>(USER_INSIGHT_TOP_APP_BAR)?.let {
Assert.assertTrue(it.isVisible)
Assert.assertTrue(it.isShown)
}
}
}
Loading

0 comments on commit 8ee171a

Please sign in to comment.