Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added progress indicator on Insight Screen #3484

Merged
merged 22 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading