diff --git a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt b/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt index d99fef53b..d8ca717d4 100644 --- a/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt +++ b/app/src/main/java/org/openedx/app/InDevelopmentFragment.kt @@ -11,15 +11,20 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.fragment.app.Fragment import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography class InDevelopmentFragment : Fragment() { + @OptIn(ExperimentalComposeUiApi::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -27,7 +32,11 @@ class InDevelopmentFragment : Fragment() { ) = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - Scaffold { + Scaffold( + modifier = Modifier.semantics { + testTagsAsResourceId = true + }, + ) { Box( modifier = Modifier .fillMaxSize() @@ -36,6 +45,7 @@ class InDevelopmentFragment : Fragment() { contentAlignment = Alignment.Center ) { Text( + modifier = Modifier.testTag("txt_in_development"), text = "Will be available soon", style = MaterialTheme.appTypography.headlineMedium ) @@ -43,4 +53,4 @@ class InDevelopmentFragment : Fragment() { } } } -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt index e875a4539..a16e77505 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt @@ -50,6 +50,7 @@ import org.openedx.auth.R import org.openedx.core.domain.model.RegistrationField import org.openedx.core.domain.model.RegistrationFieldType import org.openedx.core.extension.TextConverter +import org.openedx.core.extension.tagId import org.openedx.core.ui.HyperlinkText import org.openedx.core.ui.SheetContent import org.openedx.core.ui.noRippleClickable @@ -86,7 +87,7 @@ fun RequiredFields( val linkedText = TextConverter.htmlTextToLinkedText(field.label) HyperlinkText( - modifier = Modifier.testTag("txt_${field.name}"), + modifier = Modifier.testTag("txt_${field.name.tagId()}"), fullText = linkedText.text, hyperLinks = linkedText.links, linkTextColor = MaterialTheme.appColors.primary @@ -305,7 +306,7 @@ fun InputRegistrationField( Column { Text( modifier = Modifier - .testTag("txt_${registrationField.name}_label") + .testTag("txt_${registrationField.name.tagId()}_label") .fillMaxWidth(), text = registrationField.label, style = MaterialTheme.appTypography.labelLarge, @@ -329,7 +330,7 @@ fun InputRegistrationField( shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( - modifier = modifier.testTag("txt_${registrationField.name}_placeholder"), + modifier = modifier.testTag("txt_${registrationField.name.tagId()}_placeholder"), text = registrationField.label, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium @@ -345,11 +346,11 @@ fun InputRegistrationField( }, textStyle = MaterialTheme.appTypography.bodyMedium, singleLine = isSingleLine, - modifier = modifier.testTag("tf_${registrationField.name}") + modifier = modifier.testTag("tf_${registrationField.name.tagId()}") ) Spacer(modifier = Modifier.height(6.dp)) Text( - modifier = Modifier.testTag("txt_${registrationField.name}_description"), + modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_description"), text = helperText, style = MaterialTheme.appTypography.bodySmall, color = helperTextColor @@ -392,7 +393,7 @@ fun SelectableRegisterField( ) { Text( modifier = Modifier - .testTag("txt_${registrationField.name}_label") + .testTag("txt_${registrationField.name.tagId()}_label") .fillMaxWidth(), text = registrationField.label, style = MaterialTheme.appTypography.labelLarge, @@ -414,14 +415,14 @@ fun SelectableRegisterField( textStyle = MaterialTheme.appTypography.bodyMedium, onValueChange = { }, modifier = Modifier - .testTag("tf_${registrationField.name}") + .testTag("tf_${registrationField.name.tagId()}") .fillMaxWidth() .noRippleClickable { onClick(registrationField.name, registrationField.options) }, placeholder = { Text( - modifier = Modifier.testTag("txt_${registrationField.name}_placeholder"), + modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_placeholder"), text = registrationField.label, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium @@ -437,7 +438,7 @@ fun SelectableRegisterField( ) Spacer(modifier = Modifier.height(6.dp)) Text( - modifier = Modifier.testTag("txt_${registrationField.name}_description"), + modifier = Modifier.testTag("txt_${registrationField.name.tagId()}_description"), text = helperText, style = MaterialTheme.appTypography.bodySmall, color = helperTextColor diff --git a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt index 07241824b..eb9d6309b 100644 --- a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt +++ b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt @@ -11,11 +11,34 @@ data class VideoSettings( } } -enum class VideoQuality(val titleResId: Int, val width: Int, val height: Int) { - AUTO(R.string.auto_recommended_text, 0, 0), - OPTION_360P(R.string.video_quality_p360, 640, 360), - OPTION_540P(R.string.video_quality_p540, 960, 540), - OPTION_720P(R.string.video_quality_p720, 1280, 720); - - val value: String = this.name.replace("OPTION_", "").lowercase() +enum class VideoQuality( + val titleResId: Int, + val desResId: Int = 0, + val width: Int, + val height: Int +) { + AUTO( + titleResId = R.string.core_video_quality_auto, + desResId = R.string.core_video_quality_auto_description, + width = 0, + height = 0 + ), + OPTION_360P( + titleResId = R.string.core_video_quality_p360, + desResId = R.string.core_video_quality_p360_description, + width = 640, + height = 360 + ), + OPTION_540P( + titleResId = R.string.core_video_quality_p540, + desResId = 0, + width = 960, + height = 540 + ), + OPTION_720P( + titleResId = R.string.core_video_quality_p720, + desResId = R.string.core_video_quality_p720_description, + width = 1280, + height = 720 + ); } diff --git a/core/src/main/java/org/openedx/core/extension/StringExt.kt b/core/src/main/java/org/openedx/core/extension/StringExt.kt index 0d7281320..58a8eef26 100644 --- a/core/src/main/java/org/openedx/core/extension/StringExt.kt +++ b/core/src/main/java/org/openedx/core/extension/StringExt.kt @@ -1,6 +1,7 @@ package org.openedx.core.extension import android.util.Patterns +import java.util.Locale import java.util.regex.Pattern @@ -28,3 +29,7 @@ fun String.replaceLinkTags(isDarkTheme: Boolean): String { } return text } + +fun String.replaceSpace(target: String = ""): String = this.replace(" ", target) + +fun String.tagId(): String = this.replaceSpace("_").lowercase(Locale.getDefault()) diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt index cfa53da7c..f0502b49d 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt @@ -25,12 +25,16 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -55,6 +59,7 @@ fun AppUpgradeRequiredScreen( ) } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun AppUpgradeRequiredScreen( modifier: Modifier = Modifier, @@ -66,11 +71,13 @@ fun AppUpgradeRequiredScreen( modifier = modifier .fillMaxSize() .background(color = MaterialTheme.appColors.background) - .statusBarsInset(), + .statusBarsInset() + .semantics { testTagsAsResourceId = true }, contentAlignment = Alignment.TopCenter ) { Text( modifier = Modifier + .testTag("txt_app_upgrade_deprecated") .fillMaxWidth() .padding(top = 10.dp, bottom = 12.dp), text = stringResource(id = R.string.core_deprecated_app_version), @@ -92,6 +99,7 @@ fun AppUpgradeRequiredScreen( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun AppUpgradeRecommendDialog( modifier: Modifier = Modifier, @@ -106,11 +114,12 @@ fun AppUpgradeRecommendDialog( } Surface( - modifier = modifier, + modifier = modifier.semantics { testTagsAsResourceId = true }, color = Color.Transparent ) { Box( modifier = modifier + .testTag("btn_upgrade_dialog_not_now") .fillMaxSize() .padding(horizontal = 4.dp) .noRippleClickable { @@ -142,11 +151,13 @@ fun AppUpgradeRecommendDialog( contentDescription = null ) Text( + modifier = Modifier.testTag("txt_app_upgrade_title"), text = stringResource(id = R.string.core_app_upgrade_title), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) Text( + modifier = Modifier.testTag("txt_app_upgrade_description"), text = stringResource(id = R.string.core_app_upgrade_dialog_description), color = MaterialTheme.appColors.textPrimary, textAlign = TextAlign.Center, @@ -162,6 +173,7 @@ fun AppUpgradeRecommendDialog( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun AppUpgradeRequiredContent( modifier: Modifier = Modifier, @@ -170,7 +182,7 @@ fun AppUpgradeRequiredContent( onUpdateClick: () -> Unit ) { Column( - modifier = modifier, + modifier = modifier.semantics { testTagsAsResourceId = true }, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(32.dp) ) { @@ -183,11 +195,13 @@ fun AppUpgradeRequiredContent( verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( + modifier = Modifier.testTag("txt_app_upgrade_required_title"), text = stringResource(id = R.string.core_app_update_required_title), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) Text( + modifier = Modifier.testTag("txt_app_upgrade_required_description"), text = stringResource(id = R.string.core_app_update_required_description), color = MaterialTheme.appColors.textPrimary, textAlign = TextAlign.Center, @@ -250,6 +264,7 @@ fun TransparentTextButton( ) { Button( modifier = Modifier + .testTag("btn_secondary") .height(42.dp), colors = ButtonDefaults.buttonColors( backgroundColor = Color.Transparent @@ -259,6 +274,7 @@ fun TransparentTextButton( onClick = onClick ) { Text( + modifier = Modifier.testTag("txt_secondary"), color = MaterialTheme.appColors.textAccent, style = MaterialTheme.appTypography.labelLarge, text = text @@ -273,6 +289,7 @@ fun DefaultTextButton( ) { Button( modifier = Modifier + .testTag("btn_primary") .height(42.dp), colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.appColors.buttonBackground @@ -286,6 +303,7 @@ fun DefaultTextButton( horizontalArrangement = Arrangement.Center ) { Text( + modifier = Modifier.testTag("txt_primary"), text = text, color = MaterialTheme.appColors.buttonText, style = MaterialTheme.appTypography.labelLarge @@ -301,6 +319,7 @@ fun AppUpgradeRecommendedBox( ) { Card( modifier = modifier + .testTag("btn_upgrade_box") .fillMaxWidth() .padding(20.dp) .clickable { @@ -322,11 +341,13 @@ fun AppUpgradeRecommendedBox( ) Column { Text( + modifier = Modifier.testTag("txt_app_upgrade_title"), text = stringResource(id = R.string.core_app_upgrade_title), color = Color.White, style = MaterialTheme.appTypography.titleMedium ) Text( + modifier = Modifier.testTag("txt_app_upgrade_description"), text = stringResource(id = R.string.core_app_upgrade_box_description), color = Color.White, style = MaterialTheme.appTypography.bodyMedium diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index b5b4dca7c..8ccd75637 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -96,7 +96,9 @@ import org.openedx.core.UIMessage import org.openedx.core.domain.model.Course import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.LinkedImageText +import org.openedx.core.extension.tagId import org.openedx.core.extension.toastMessage +import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography @@ -108,19 +110,21 @@ fun StaticSearchBar( onClick: () -> Unit = {}, ) { Row( - modifier = modifier.then(Modifier - .background( - MaterialTheme.appColors.textFieldBackground, - MaterialTheme.appShapes.textFieldShape - ) - .clip(MaterialTheme.appShapes.textFieldShape) - .border( - 1.dp, - MaterialTheme.appColors.textFieldBorder, - MaterialTheme.appShapes.textFieldShape - ) - .clickable { onClick() } - .padding(horizontal = 20.dp)), + modifier = modifier + .testTag("tf_search") + .then(Modifier + .background( + MaterialTheme.appColors.textFieldBackground, + MaterialTheme.appShapes.textFieldShape + ) + .clip(MaterialTheme.appShapes.textFieldShape) + .border( + 1.dp, + MaterialTheme.appColors.textFieldBorder, + MaterialTheme.appShapes.textFieldShape + ) + .clickable { onClick() } + .padding(horizontal = 20.dp)), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -131,7 +135,9 @@ fun StaticSearchBar( Spacer(Modifier.width(10.dp)) Box { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_search") + .fillMaxWidth(), text = text, color = MaterialTheme.appColors.textFieldHint ) @@ -157,6 +163,7 @@ fun Toolbar( Text( modifier = Modifier + .testTag("txt_toolbar_title") .align(Alignment.Center), text = label, color = MaterialTheme.appColors.textPrimary, @@ -196,6 +203,7 @@ fun SearchBar( } OutlinedTextField( modifier = Modifier + .testTag("tf_search") .focusRequester(focusRequester) .onFocusChanged { isFocused = it.hasFocus @@ -617,7 +625,7 @@ fun SheetContent( }) { item -> Text( modifier = Modifier - .testTag("txt_${item.value}_title") + .testTag("txt_${item.value.tagId()}_title") .fillMaxWidth() .padding(horizontal = 16.dp) .clickable { @@ -717,7 +725,9 @@ fun OpenEdXOutlinedTextField( Column { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_${title.tagId()}_label") + .fillMaxWidth(), text = buildAnnotatedString { if (withRequiredMark) { append(title) @@ -763,11 +773,12 @@ fun OpenEdXOutlinedTextField( textStyle = MaterialTheme.appTypography.bodyMedium, singleLine = isSingleLine, isError = !errorText.isNullOrEmpty(), - modifier = modifier + modifier = modifier.testTag("tf_${title.tagId()}_input") ) if (!errorText.isNullOrEmpty()) { Spacer(modifier = Modifier.height(6.dp)) Text( + modifier = Modifier.testTag("txt_${title.tagId()}_error"), text = errorText, style = MaterialTheme.appTypography.bodySmall, color = MaterialTheme.appColors.error @@ -829,6 +840,7 @@ fun DiscoveryCourseItem( val imageUrl = apiHostUrl.dropLast(1) + course.media.courseImage?.uri Surface( modifier = Modifier + .testTag("btn_course_card") .fillMaxWidth() .height(140.dp) .clickable { onClick(course.courseId) } @@ -860,12 +872,15 @@ fun DiscoveryCourseItem( .height(105.dp), ) { Text( - modifier = Modifier.padding(top = 12.dp), + modifier = Modifier + .testTag("txt_course_org") + .padding(top = 12.dp), text = course.org, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.labelMedium ) Text( modifier = Modifier + .testTag("txt_course_title") .fillMaxWidth() .padding(top = 8.dp), text = course.name, @@ -891,7 +906,9 @@ fun IconText( val modifierClickable = if (onClick == null) { Modifier } else { - Modifier.noRippleClickable { onClick.invoke() } + Modifier + .testTag("btn_${text.tagId()}") + .noRippleClickable { onClick.invoke() } } Row( modifier = modifier.then(modifierClickable), @@ -899,12 +916,19 @@ fun IconText( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( - modifier = Modifier.size((textStyle.fontSize.value + 4).dp), + modifier = Modifier + .testTag("ic_${text.tagId()}") + .size((textStyle.fontSize.value + 4).dp), imageVector = icon, contentDescription = null, tint = color ) - Text(text = text, color = color, style = textStyle) + Text( + modifier = Modifier.testTag("txt_${text.tagId()}"), + text = text, + color = color, + style = textStyle + ) } } @@ -920,7 +944,9 @@ fun IconText( val modifierClickable = if (onClick == null) { Modifier } else { - Modifier.noRippleClickable { onClick.invoke() } + Modifier + .testTag("btn_${text.tagId()}") + .noRippleClickable { onClick.invoke() } } Row( modifier = modifier.then(modifierClickable), @@ -928,12 +954,19 @@ fun IconText( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( - modifier = Modifier.size((textStyle.fontSize.value + 4).dp), + modifier = Modifier + .testTag("ic_${text.tagId()}") + .size((textStyle.fontSize.value + 4).dp), painter = painter, contentDescription = null, tint = color ) - Text(text = text, color = color, style = textStyle) + Text( + modifier = Modifier.testTag("txt_${text.tagId()}"), + text = text, + color = color, + style = textStyle + ) } } @@ -1014,19 +1047,24 @@ fun OfflineModeDialog( horizontalArrangement = Arrangement.SpaceBetween ) { Text( + modifier = Modifier.testTag("txt_offline_label"), text = stringResource(id = R.string.core_offline), style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textDark ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text( - modifier = Modifier.clickable { onDismissCLick() }, + modifier = Modifier + .testTag("txt_dismiss") + .clickable { onDismissCLick() }, text = stringResource(id = R.string.core_dismiss), style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.primary ) Text( - modifier = Modifier.clickable { onReloadClick() }, + modifier = Modifier + .testTag("txt_reload") + .clickable { onReloadClick() }, text = stringResource(id = R.string.core_reload), style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.primary @@ -1047,6 +1085,7 @@ fun OpenEdXButton( ) { Button( modifier = Modifier + .testTag("btn_${text.tagId()}") .then(width) .height(42.dp), shape = MaterialTheme.appShapes.buttonShape, @@ -1058,6 +1097,7 @@ fun OpenEdXButton( ) { if (content == null) { Text( + modifier = Modifier.testTag("txt_${text.tagId()}"), text = text, color = MaterialTheme.appColors.buttonText, style = MaterialTheme.appTypography.labelLarge @@ -1080,6 +1120,7 @@ fun OpenEdXOutlinedButton( ) { OutlinedButton( modifier = Modifier + .testTag("btn_${text.tagId()}") .then(modifier) .height(42.dp), onClick = onClick, @@ -1089,6 +1130,7 @@ fun OpenEdXOutlinedButton( ) { if (content == null) { Text( + modifier = Modifier.testTag("txt_${text.tagId()}"), text = text, style = MaterialTheme.appTypography.labelLarge, color = textColor @@ -1133,7 +1175,9 @@ fun ConnectionErrorView( ) Spacer(Modifier.height(16.dp)) Text( - modifier = Modifier.fillMaxWidth(0.6f), + modifier = Modifier + .testTag("txt_connection_error_label") + .fillMaxWidth(0.6f), text = stringResource(id = R.string.core_not_connected_to_internet), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium, @@ -1219,3 +1263,39 @@ private fun ToolbarPreview() { private fun AuthButtonsPanelPreview() { AuthButtonsPanel(onRegisterClick = {}, onSignInClick = {}) } + +@Preview +@Composable +private fun OpenEdXOutlinedTextFieldPreview() { + OpenEdXTheme(darkTheme = true) { + OpenEdXOutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + title = "OpenEdXOutlinedTextField", + onValueChanged = {}, + keyboardActions = {}, + ) + } +} + +@Preview +@Composable +private fun IconTextPreview() { + IconText( + text = "IconText", + icon = Icons.Filled.Close, + color = MaterialTheme.appColors.primary + ) +} + +@Preview +@Composable +private fun ConnectionErrorViewPreview() { + OpenEdXTheme(darkTheme = true) { + ConnectionErrorView( + modifier = Modifier + .fillMaxSize(), + onReloadClick = {} + ) + } +} diff --git a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt index e58879326..a79200111 100644 --- a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt +++ b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -30,11 +29,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -42,10 +42,10 @@ import androidx.compose.ui.zIndex import org.openedx.core.extension.isEmailValid import org.openedx.core.extension.replaceLinkTags import org.openedx.core.ui.theme.appColors -import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.EmailUtil import java.nio.charset.StandardCharsets +@OptIn(ExperimentalComposeUiApi::class) @Composable fun WebContentScreen( windowSize: WindowSize, @@ -59,7 +59,10 @@ fun WebContentScreen( Scaffold( modifier = Modifier .fillMaxSize() - .padding(bottom = 16.dp), + .padding(bottom = 16.dp) + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { @@ -88,20 +91,10 @@ fun WebContentScreen( .zIndex(1f), contentAlignment = Alignment.CenterStart ) { - BackBtn { - onBackClick() - } - - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 56.dp), - text = title, - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center + Toolbar( + label = title, + canShowBackBtn = true, + onBackClick = onBackClick ) } Spacer(Modifier.height(6.dp)) diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index 827211c40..ff7fa8fd6 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -20,10 +20,10 @@ Термін дії курсу минув %1$s Пароль незабаром - Авто (Рекомендовано) - 360p (Менше використання трафіку) - 540p - 720p (Найкраща якість) + Авто + Рекомендовано + Менше використання трафіку + Найкраща якість Офлайн Закрити Перезавантажити diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 6b77b5b11..c493da626 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -29,10 +29,13 @@ Dismiss Reload Downloading in progress - Auto (Recommended) - 360p (Lower data usage) - 540p - 720p (Best quality) + Auto + Recommended + 360p + Lower data usage + 540p + 720p translatable="false" + Best quality User account is not activated. Please activate your account first. Send email using… No e-mail clients installed diff --git a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt index cc7a500f4..8b031fc4f 100644 --- a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.shadow @@ -26,8 +27,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -149,6 +150,7 @@ class CourseDetailsFragment : Fragment() { } +@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun CourseDetailsScreen( windowSize: WindowSize, @@ -174,7 +176,10 @@ internal fun CourseDetailsScreen( Scaffold( modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background, bottomBar = { @@ -224,27 +229,14 @@ internal fun CourseDetailsScreen( Column( screenWidth ) { - Box( - Modifier + Toolbar( + modifier = Modifier .fillMaxWidth() .zIndex(1f), - contentAlignment = Alignment.CenterStart - ) { - BackBtn { - onBackClick() - } - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 56.dp), - text = stringResource(id = courseR.string.course_details), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center - ) - } + label = stringResource(id = courseR.string.course_details), + canShowBackBtn = true, + onBackClick = onBackClick + ) Spacer(Modifier.height(6.dp)) Box( Modifier @@ -286,7 +278,12 @@ internal fun CourseDetailsScreen( ) } if (isPreview) { - Text(htmlBody, Modifier.padding(all = 20.dp)) + Text( + text = htmlBody, + modifier = Modifier + .testTag("txt_course_description") + .padding(all = 20.dp), + ) } else { var webViewAlpha by remember { mutableStateOf(0f) } if (webViewAlpha == 0f) { @@ -384,6 +381,7 @@ private fun CourseDetailNativeContent( ) if (!course.media.courseVideo?.uri.isNullOrEmpty()) { IconButton( + modifier = Modifier.testTag("ib_play_video"), onClick = { uriHandler.openUri(course.media.courseVideo?.uri!!) } @@ -409,18 +407,21 @@ private fun CourseDetailNativeContent( Spacer(Modifier.height(24.dp)) } Text( + modifier = Modifier.testTag("txt_course_short_description"), text = course.shortDescription, style = MaterialTheme.appTypography.labelSmall, color = MaterialTheme.appColors.textPrimaryVariant ) Spacer(Modifier.height(16.dp)) Text( + modifier = Modifier.testTag("txt_course_name"), text = course.name, style = MaterialTheme.appTypography.titleLarge, color = MaterialTheme.appColors.textPrimary ) Spacer(Modifier.height(12.dp)) Text( + modifier = Modifier.testTag("txt_course_org"), text = course.org, style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textAccent @@ -473,18 +474,21 @@ private fun CourseDetailNativeContentLandscape( ) { Column { Text( + modifier = Modifier.testTag("txt_course_short_description"), text = course.shortDescription, style = MaterialTheme.appTypography.labelSmall, color = MaterialTheme.appColors.textPrimaryVariant ) Spacer(Modifier.height(16.dp)) Text( + modifier = Modifier.testTag("txt_course_name"), text = course.name, style = MaterialTheme.appTypography.titleLarge, color = MaterialTheme.appColors.textPrimary ) Spacer(Modifier.height(12.dp)) Text( + modifier = Modifier.testTag("txt_course_org"), text = course.org, style = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textAccent @@ -516,6 +520,7 @@ private fun CourseDetailNativeContentLandscape( ) if (!course.media.courseVideo?.uri.isNullOrEmpty()) { IconButton( + modifier = Modifier.testTag("ib_play_video"), onClick = { uriHandler.openUri(course.media.courseVideo?.uri!!) } @@ -572,6 +577,7 @@ private fun EnrollOverLabel() { ) Spacer(Modifier.width(12.dp)) Text( + modifier = Modifier.testTag("txt_enroll_error"), text = stringResource(id = courseR.string.course_you_cant_enroll), color = MaterialTheme.appColors.textPrimaryVariant, style = MaterialTheme.appTypography.titleSmall diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 1e6f81361..3595c9652 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -155,18 +156,21 @@ fun CourseImageHeader( verticalArrangement = Arrangement.Center ) { Icon( + modifier = Modifier.testTag("ic_congratulations"), painter = painterResource(id = R.drawable.ic_course_completed_mark), contentDescription = stringResource(id = R.string.course_congratulations), tint = Color.White ) Spacer(Modifier.height(6.dp)) Text( + modifier = Modifier.testTag("txt_congratulations"), text = stringResource(id = R.string.course_congratulations), style = MaterialTheme.appTypography.headlineMedium, color = Color.White ) Spacer(Modifier.height(4.dp)) Text( + modifier = Modifier.testTag("txt_course_passed"), text = stringResource(id = R.string.course_passed), style = MaterialTheme.appTypography.bodyMedium, color = Color.White diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt index 849423409..0582f663e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt @@ -7,29 +7,58 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.runtime.* +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -43,7 +72,11 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.AppUpdateState import org.openedx.core.UIMessage -import org.openedx.core.domain.model.* +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage @@ -63,7 +96,7 @@ import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R import org.openedx.dashboard.presentation.DashboardRouter -import java.util.* +import java.util.Date class DashboardFragment : Fragment() { @@ -127,7 +160,7 @@ class DashboardFragment : Fragment() { } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable internal fun MyCoursesScreen( windowSize: WindowSize, @@ -157,7 +190,11 @@ internal fun MyCoursesScreen( Scaffold( scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, backgroundColor = MaterialTheme.appColors.background ) { paddingValues -> @@ -238,17 +275,7 @@ internal fun MyCoursesScreen( content = { item() { Column { - Text( - text = stringResource(id = R.string.dashboard_courses), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.displaySmall - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = stringResource(id = R.string.dashboard_welcome_back), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleSmall - ) + Header() Spacer(modifier = Modifier.height(16.dp)) } } @@ -290,17 +317,7 @@ internal fun MyCoursesScreen( .then(contentWidth) .then(emptyStatePaddings) ) { - Text( - text = stringResource(id = R.string.dashboard_courses), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.displaySmall - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = stringResource(id = R.string.dashboard_welcome_back), - color = MaterialTheme.appColors.textPrimaryVariant, - style = MaterialTheme.appTypography.titleSmall - ) + Header() EmptyState() } } @@ -366,6 +383,7 @@ private fun CourseItem( val context = LocalContext.current Surface( modifier = Modifier + .testTag("btn_course_item") .height(142.dp) .fillMaxWidth() .clickable { onClick(enrolledCourse) } @@ -398,6 +416,7 @@ private fun CourseItem( .background(MaterialTheme.appColors.background) ) { Text( + modifier = Modifier.testTag("txt_course_org"), text = enrolledCourse.course.org, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.labelMedium @@ -410,6 +429,7 @@ private fun CourseItem( verticalArrangement = Arrangement.SpaceBetween ) { Text( + modifier = Modifier.testTag("txt_course_name"), text = enrolledCourse.course.name, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleSmall, @@ -424,6 +444,7 @@ private fun CourseItem( horizontalArrangement = Arrangement.SpaceBetween ) { Text( + modifier = Modifier.testTag("txt_course_date"), text = TimeUtils.getCourseFormattedDate( context, Date(), @@ -444,6 +465,7 @@ private fun CourseItem( ) { Icon( modifier = Modifier + .testTag("ic_course_item") .size(15.dp), imageVector = Icons.Filled.ArrowForward, contentDescription = null, @@ -457,6 +479,24 @@ private fun CourseItem( } } +@Composable +private fun Header() { + Text( + modifier = Modifier.testTag("txt_courses_title"), + text = stringResource(id = R.string.dashboard_courses), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.displaySmall + ) + Text( + modifier = Modifier + .testTag("txt_courses_description") + .padding(top = 4.dp), + text = stringResource(id = R.string.dashboard_welcome_back), + color = MaterialTheme.appColors.textPrimaryVariant, + style = MaterialTheme.appTypography.titleSmall + ) +} + @Composable private fun EmptyState() { Box( @@ -474,7 +514,9 @@ private fun EmptyState() { ) Spacer(Modifier.height(16.dp)) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_empty_state_title") + .fillMaxWidth(), text = stringResource(id = R.string.dashboard_its_empty), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium, @@ -482,7 +524,9 @@ private fun EmptyState() { ) Spacer(Modifier.height(8.dp)) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_empty_state_description") + .fillMaxWidth(), text = stringResource(id = R.string.dashboard_you_are_not_enrolled), color = MaterialTheme.appColors.textPrimaryVariant, style = MaterialTheme.appTypography.bodySmall, diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt index 00da95a6e..08dedc32e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt @@ -25,12 +25,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -207,6 +210,7 @@ class ProgramFragment(private val myPrograms: Boolean = false) : Fragment() { } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun ProgramInfoScreen( windowSize: WindowSize, @@ -235,7 +239,9 @@ private fun ProgramInfoScreen( Scaffold( scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { testTagsAsResourceId = true }, backgroundColor = MaterialTheme.appColors.background ) { val modifierScreenWidth by remember(key1 = windowSize) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt index b64e3b5b8..6db42a074 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt @@ -40,10 +40,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -186,7 +190,7 @@ class NativeDiscoveryFragment : Fragment() { } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable internal fun DiscoveryScreen( windowSize: WindowSize, @@ -222,7 +226,11 @@ internal fun DiscoveryScreen( Scaffold( scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, backgroundColor = MaterialTheme.appColors.background, bottomBar = { if (!isUserLoggedIn) { @@ -306,6 +314,7 @@ internal fun DiscoveryScreen( verticalArrangement = Arrangement.Center ) { Text( + modifier = Modifier.testTag("txt_discovery_title"), text = stringResource(id = R.string.discovery_Discovery), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium @@ -354,12 +363,15 @@ internal fun DiscoveryScreen( item { Column { Text( + modifier = Modifier.testTag("txt_discovery_new"), text = stringResource(id = R.string.discovery_discovery_new), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.displaySmall ) Text( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier + .testTag("txt_discovery_lets_find") + .padding(top = 4.dp), text = stringResource(id = R.string.discovery_lets_find), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleSmall diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt index 33a2bd24a..f03e34d58 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView @@ -35,6 +36,8 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -148,6 +151,7 @@ class WebViewDiscoveryFragment : Fragment() { } } +@OptIn(ExperimentalComposeUiApi::class) @Composable @SuppressLint("SetJavaScriptEnabled") private fun WebViewDiscoveryScreen( @@ -169,7 +173,11 @@ private fun WebViewDiscoveryScreen( Scaffold( scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, backgroundColor = MaterialTheme.appColors.background, bottomBar = { if (isPreLogin) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt index 26af85022..f72540b14 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt @@ -45,8 +45,11 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign @@ -205,7 +208,8 @@ private fun CourseSearchScreen( scaffoldState = scaffoldState, modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .semantics { testTagsAsResourceId = true }, backgroundColor = MaterialTheme.appColors.background, bottomBar = { if (!isUserLoggedIn) { @@ -282,6 +286,7 @@ private fun CourseSearchScreen( } Text( modifier = Modifier + .testTag("txt_search_title") .fillMaxWidth() .padding(horizontal = 56.dp), text = stringResource(id = org.openedx.core.R.string.core_search), @@ -340,12 +345,15 @@ private fun CourseSearchScreen( item { Column { Text( + modifier = Modifier.testTag("txt_search_results_title"), text = stringResource(id = discoveryR.string.discovery_search_results), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.displaySmall ) Text( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier + .testTag("txt_search_results_subtitle") + .padding(top = 4.dp), text = typingText, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleSmall diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt index 701fa4bb0..50771187a 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt @@ -31,11 +31,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction @@ -52,11 +56,11 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.R import org.openedx.core.UIMessage -import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.IconText import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.OpenEdXOutlinedTextField +import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.displayCutoutForLandscape @@ -128,6 +132,7 @@ class DeleteProfileFragment : Fragment() { } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun DeleteProfileScreen( windowSize: WindowSize, @@ -153,7 +158,8 @@ fun DeleteProfileScreen( Scaffold( modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .semantics { testTagsAsResourceId = true }, scaffoldState = scaffoldState ) { paddingValues -> @@ -191,26 +197,12 @@ fun DeleteProfileScreen( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally ) { - Box( + Toolbar( modifier = topBarWidth, - contentAlignment = Alignment.CenterStart - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = stringResource(id = profileR.string.profile_delete_account), - color = MaterialTheme.appColors.textPrimary, - textAlign = TextAlign.Center, - style = MaterialTheme.appTypography.titleMedium - ) - - BackBtn( - modifier = Modifier.padding(end = 8.dp) - ) { - onBackClick() - } - } - + label = stringResource(id = profileR.string.profile_delete_account), + canShowBackBtn = true, + onBackClick = onBackClick + ) Column( Modifier .fillMaxHeight() @@ -226,7 +218,9 @@ fun DeleteProfileScreen( ) Spacer(Modifier.height(32.dp)) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_delete_account_title") + .fillMaxWidth(), text = buildAnnotatedString { append(stringResource(id = profileR.string.profile_you_want_to)) append(" ") @@ -251,7 +245,9 @@ fun DeleteProfileScreen( ) Spacer(Modifier.height(16.dp)) Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_delete_account_description") + .fillMaxWidth(), text = stringResource(id = profileR.string.profile_confirm_action), style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textSecondary, @@ -323,4 +319,4 @@ fun DeleteProfileScreenPreview() { onDeleteClick = {} ) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt index e911d1b4b..1efdc094e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt @@ -16,7 +16,6 @@ import android.provider.MediaStore import android.view.Gravity import android.view.LayoutInflater import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -84,8 +83,11 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -107,13 +109,13 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.AppDataConstants.DEFAULT_MIME_TYPE -import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.domain.model.LanguageProficiency import org.openedx.core.domain.model.ProfileImage import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.getFileName import org.openedx.core.extension.parcelable +import org.openedx.core.extension.tagId import org.openedx.core.ui.AutoSizeText import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage @@ -135,12 +137,13 @@ import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.LocaleUtils +import org.openedx.profile.R import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.ProfileRouter import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream -import org.openedx.profile.R as profileR +import org.openedx.core.R as coreR private const val BIO_TEXT_FIELD_LIMIT = 300 @@ -159,21 +162,6 @@ class EditProfileFragment : Fragment() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val callback = requireActivity() - .onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (viewModel.profileDataChanged) { - viewModel.setShowLeaveDialog(true) - } else { - requireActivity().supportFragmentManager.popBackStack() - } - } - }) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -256,6 +244,7 @@ class EditProfileFragment : Fragment() { } } + @Suppress("DEPRECATION") private fun cropImage(uri: Uri): Uri { val matrix = Matrix() matrix.postRotate(getImageOrientation(uri).toFloat()) @@ -398,15 +387,21 @@ private fun EditProfileScreen( } val imageRes: Any = if (!isImageDeleted) { - if (selectedImageUri != null) { - selectedImageUri.toString() - } else if (uiState.account.profileImage.hasImage) { - uiState.account.profileImage.imageUrlFull - } else { - R.drawable.core_ic_default_profile_picture + when { + selectedImageUri != null -> { + selectedImageUri.toString() + } + + uiState.account.profileImage.hasImage -> { + uiState.account.profileImage.imageUrlFull + } + + else -> { + coreR.drawable.core_ic_default_profile_picture + } } } else { - R.drawable.core_ic_default_profile_picture + coreR.drawable.core_ic_default_profile_picture } val modalListState = rememberLazyListState() @@ -426,7 +421,10 @@ private fun EditProfileScreen( Scaffold( modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState ) { paddingValues -> @@ -467,6 +465,7 @@ private fun EditProfileScreen( ModalBottomSheetLayout( modifier = Modifier + .testTag("btn_bottom_sheet_edit_profile") .padding(bottom = if (isImeVisible && bottomSheetScaffoldState.isVisible) 120.dp else 0.dp) .noRippleClickable { if (bottomSheetScaffoldState.isVisible) { @@ -560,8 +559,9 @@ private fun EditProfileScreen( ) { Text( modifier = Modifier + .testTag("txt_edit_profile_title") .fillMaxWidth(), - text = stringResource(id = profileR.string.profile_edit_profile), + text = stringResource(id = R.string.profile_edit_profile), color = MaterialTheme.appColors.textPrimary, textAlign = TextAlign.Center, style = MaterialTheme.appTypography.titleMedium @@ -579,7 +579,7 @@ private fun EditProfileScreen( modifier = Modifier .height(48.dp) .padding(end = 24.dp), - text = stringResource(id = profileR.string.profile_done), + text = stringResource(id = R.string.profile_done), icon = Icons.Filled.Done, color = MaterialTheme.appColors.primary, textStyle = MaterialTheme.appTypography.labelLarge, @@ -609,7 +609,8 @@ private fun EditProfileScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = stringResource(if (uiState.isLimited) profileR.string.profile_limited_profile else profileR.string.profile_full_profile), + modifier = Modifier.testTag("txt_edit_profile_type_label"), + text = stringResource(if (uiState.isLimited) R.string.profile_limited_profile else R.string.profile_full_profile), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.titleSmall ) @@ -618,12 +619,16 @@ private fun EditProfileScreen( AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(imageRes) - .error(R.drawable.core_ic_default_profile_picture) - .placeholder(R.drawable.core_ic_default_profile_picture) + .error(coreR.drawable.core_ic_default_profile_picture) + .placeholder(coreR.drawable.core_ic_default_profile_picture) .build(), contentScale = ContentScale.Crop, - contentDescription = stringResource(id = R.string.core_accessibility_user_profile_image, uiState.account.username), + contentDescription = stringResource( + id = coreR.string.core_accessibility_user_profile_image, + uiState.account.username + ), modifier = Modifier + .testTag("img_edit_profile_user_image") .border( 2.dp, MaterialTheme.appColors.onSurface, @@ -646,33 +651,36 @@ private fun EditProfileScreen( .clip(CircleShape) .background(MaterialTheme.appColors.primary) .padding(5.dp), - painter = painterResource(id = profileR.drawable.profile_ic_edit_image), + painter = painterResource(id = R.drawable.profile_ic_edit_image), contentDescription = null, tint = Color.White ) } Spacer(modifier = Modifier.height(20.dp)) Text( + modifier = Modifier.testTag("txt_edit_profile_user_name"), text = uiState.account.name, style = MaterialTheme.appTypography.headlineSmall, color = MaterialTheme.appColors.textPrimary ) Spacer(modifier = Modifier.height(24.dp)) Text( - modifier = Modifier.clickable { - if (!LocaleUtils.isProfileLimited(mapFields[YEAR_OF_BIRTH].toString())) { - val privacy = if (uiState.isLimited) { - Account.Privacy.ALL_USERS + modifier = Modifier + .testTag("txt_edit_profile_limited_profile_label") + .clickable { + if (!LocaleUtils.isProfileLimited(mapFields[YEAR_OF_BIRTH].toString())) { + val privacy = if (uiState.isLimited) { + Account.Privacy.ALL_USERS + } else { + Account.Privacy.PRIVATE + } + mapFields[ACCOUNT_PRIVACY] = privacy + onLimitedProfileChange(!uiState.isLimited) } else { - Account.Privacy.PRIVATE + openWarningMessageDialog = true } - mapFields[ACCOUNT_PRIVACY] = privacy - onLimitedProfileChange(!uiState.isLimited) - } else { - openWarningMessageDialog = true - } - }, - text = stringResource(if (uiState.isLimited) profileR.string.profile_switch_to_full else profileR.string.profile_switch_to_limited), + }, + text = stringResource(if (uiState.isLimited) R.string.profile_switch_to_full else R.string.profile_switch_to_limited), color = MaterialTheme.appColors.textAccent, style = MaterialTheme.appTypography.labelLarge ) @@ -680,17 +688,23 @@ private fun EditProfileScreen( ProfileFields( disabled = uiState.isLimited, onFieldClick = { it, title -> - if (it == YEAR_OF_BIRTH) { - serverFieldName.value = YEAR_OF_BIRTH - expandedList = - LocaleUtils.getBirthYearsRange() - } else if (it == COUNTRY) { - serverFieldName.value = COUNTRY - expandedList = - LocaleUtils.getCountries() - } else if (it == LANGUAGE) { - serverFieldName.value = LANGUAGE - expandedList = LocaleUtils.getLanguages() + when (it) { + YEAR_OF_BIRTH -> { + serverFieldName.value = YEAR_OF_BIRTH + expandedList = + LocaleUtils.getBirthYearsRange() + } + + COUNTRY -> { + serverFieldName.value = COUNTRY + expandedList = + LocaleUtils.getCountries() + } + + LANGUAGE -> { + serverFieldName.value = LANGUAGE + expandedList = LocaleUtils.getLanguages() + } } bottomDialogTitle = title keyboardController?.hide() @@ -720,8 +734,8 @@ private fun EditProfileScreen( ) Spacer(Modifier.height(40.dp)) IconText( - text = stringResource(id = org.openedx.profile.R.string.profile_delete_profile), - painter = painterResource(id = profileR.drawable.profile_ic_trash), + text = stringResource(id = R.string.profile_delete_profile), + painter = painterResource(id = R.drawable.profile_ic_trash), textStyle = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.error, onClick = { @@ -773,13 +787,17 @@ private fun LimitedProfileDialog( ) Spacer(modifier = Modifier.width(8.dp)) Text( - modifier = Modifier.weight(1f), - text = stringResource(id = profileR.string.profile_oh_sorry), + modifier = Modifier + .testTag("txt_edit_profile_limited_profile_title") + .weight(1f), + text = stringResource(id = R.string.profile_oh_sorry), color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.titleMedium ) Icon( - modifier = Modifier.clickable { onCloseClick() }, + modifier = Modifier + .testTag("ic_edit_profile_limited_profile_close") + .clickable { onCloseClick() }, imageVector = Icons.Filled.Close, contentDescription = null, tint = MaterialTheme.appColors.textDark @@ -787,8 +805,10 @@ private fun LimitedProfileDialog( } Spacer(modifier = Modifier.height(8.dp)) Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = profileR.string.profile_must_be_over), + modifier = Modifier + .testTag("txt_edit_profile_limited_profile_message") + .fillMaxWidth(), + text = stringResource(id = R.string.profile_must_be_over), color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.bodyMedium ) @@ -808,7 +828,9 @@ private fun ChangeImageDialog( val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider dialogWindowProvider.window.setGravity(Gravity.BOTTOM) Box( - Modifier.padding(bottom = 24.dp) + Modifier + .padding(bottom = 24.dp) + .semantics { testTagsAsResourceId = true } ) { Column( Modifier @@ -833,18 +855,20 @@ private fun ChangeImageDialog( ) Spacer(Modifier.height(14.dp)) Text( - text = stringResource(id = profileR.string.profile_change_image), + modifier = Modifier.testTag("txt_edit_profile_change_image_title"), + text = stringResource(id = R.string.profile_change_image), style = MaterialTheme.appTypography.titleLarge, color = MaterialTheme.appColors.textPrimary ) Spacer(Modifier.height(20.dp)) OpenEdXButton( - text = stringResource(id = profileR.string.profile_select_from_gallery), + text = stringResource(id = R.string.profile_select_from_gallery), onClick = onSelectFromGalleryClick, content = { IconText( - text = stringResource(id = profileR.string.profile_select_from_gallery), - painter = painterResource(id = profileR.drawable.profile_ic_gallery), + modifier = Modifier.testTag("it_select_from_gallery"), + text = stringResource(id = R.string.profile_select_from_gallery), + painter = painterResource(id = R.drawable.profile_ic_gallery), color = Color.White, textStyle = MaterialTheme.appTypography.labelLarge ) @@ -854,12 +878,13 @@ private fun ChangeImageDialog( OpenEdXOutlinedButton( borderColor = MaterialTheme.appColors.error, textColor = MaterialTheme.appColors.textPrimary, - text = stringResource(id = profileR.string.profile_remove_photo), + text = stringResource(id = R.string.profile_remove_photo), onClick = onRemoveImageClick, content = { IconText( - text = stringResource(id = profileR.string.profile_remove_photo), - painter = painterResource(id = profileR.drawable.profile_ic_remove_image), + modifier = Modifier.testTag("it_remove_photo"), + text = stringResource(id = R.string.profile_remove_photo), + painter = painterResource(id = R.drawable.profile_ic_remove_image), color = MaterialTheme.appColors.error, textStyle = MaterialTheme.appTypography.labelLarge ) @@ -869,7 +894,7 @@ private fun ChangeImageDialog( OpenEdXOutlinedButton( borderColor = MaterialTheme.appColors.textPrimaryVariant, textColor = MaterialTheme.appColors.textPrimary, - text = stringResource(id = R.string.core_cancel), + text = stringResource(id = coreR.string.core_cancel), onClick = onCancelClick ) Spacer(Modifier.height(20.dp)) @@ -894,27 +919,27 @@ private fun ProfileFields( } else "" Column(verticalArrangement = Arrangement.spacedBy(20.dp)) { SelectableField( - name = stringResource(id = profileR.string.profile_year), + name = stringResource(id = R.string.profile_year), initialValue = mapFields[YEAR_OF_BIRTH].toString(), onClick = { - onFieldClick(YEAR_OF_BIRTH, context.getString(profileR.string.profile_year)) + onFieldClick(YEAR_OF_BIRTH, context.getString(R.string.profile_year)) } ) if (!disabled) { SelectableField( - name = stringResource(id = profileR.string.profile_location), + name = stringResource(id = R.string.profile_location), initialValue = LocaleUtils.getCountryByCountryCode(mapFields[COUNTRY].toString()), onClick = { - onFieldClick(COUNTRY, context.getString(profileR.string.profile_location)) + onFieldClick(COUNTRY, context.getString(R.string.profile_location)) } ) SelectableField( - name = stringResource(id = profileR.string.profile_spoken_language), + name = stringResource(id = R.string.profile_spoken_language), initialValue = lang, onClick = { onFieldClick( LANGUAGE, - context.getString(profileR.string.profile_spoken_language) + context.getString(R.string.profile_spoken_language) ) } ) @@ -922,7 +947,7 @@ private fun ProfileFields( modifier = Modifier .fillMaxWidth() .height(132.dp), - name = stringResource(id = profileR.string.profile_about_me), + name = stringResource(id = R.string.profile_about_me), initialValue = mapFields[BIO].toString(), onValueChanged = { onValueChanged(it.take(BIO_TEXT_FIELD_LIMIT)) @@ -954,9 +979,11 @@ private fun SelectableField( disabledPlaceholderColor = MaterialTheme.appColors.textFieldHint ) } - Column() { + Column { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_label_${name.tagId()}") + .fillMaxWidth(), text = name, style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textPrimary @@ -978,12 +1005,14 @@ private fun SelectableField( ) }, modifier = Modifier + .testTag("tf_select_${name.tagId()}") .fillMaxWidth() .noRippleClickable { onClick() }, placeholder = { Text( + modifier = Modifier.testTag("txt_placeholder_${name.tagId()}"), text = name, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium @@ -1008,7 +1037,9 @@ private fun InputEditField( Column { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_label_${name.tagId()}") + .fillMaxWidth(), text = name, style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textPrimary @@ -1027,6 +1058,7 @@ private fun InputEditField( shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( + modifier = Modifier.testTag("txt_placeholder_${name.tagId()}"), text = name, color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium @@ -1042,7 +1074,7 @@ private fun InputEditField( onDoneClick() }, textStyle = MaterialTheme.appTypography.bodyMedium, - modifier = modifier + modifier = modifier.testTag("tf_input_${name.tagId()}") ) } } @@ -1072,38 +1104,47 @@ private fun LeaveProfile( MaterialTheme.appShapes.cardShape ) .padding(horizontal = 40.dp) - .padding(top = 48.dp, bottom = 36.dp), + .padding(top = 48.dp, bottom = 36.dp) + .semantics { + testTagsAsResourceId = true + }, horizontalAlignment = Alignment.CenterHorizontally ) { Icon( modifier = Modifier .size(100.dp), - painter = painterResource(org.openedx.profile.R.drawable.profile_ic_save), + painter = painterResource(R.drawable.profile_ic_save), contentDescription = null ) Spacer(Modifier.size(48.dp)) Text( - text = stringResource(id = org.openedx.profile.R.string.profile_leave_profile), + modifier = Modifier + .testTag("txt_leave_profile_title"), + text = stringResource(id = R.string.profile_leave_profile), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleLarge, textAlign = TextAlign.Center ) Spacer(Modifier.size(12.dp)) Text( - text = stringResource(id = org.openedx.profile.R.string.profile_changes_you_made), + modifier = Modifier + .testTag("txt_leave_profile_description"), + text = stringResource(id = R.string.profile_changes_you_made), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.bodyMedium, textAlign = TextAlign.Center ) Spacer(Modifier.size(40.dp)) OpenEdXButton( - text = stringResource(id = org.openedx.profile.R.string.profile_leave), + text = stringResource(id = R.string.profile_leave), onClick = onLeaveClick, backgroundColor = MaterialTheme.appColors.warning, content = { Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_leave), + modifier = Modifier + .testTag("txt_leave") + .fillMaxWidth(), + text = stringResource(id = R.string.profile_leave), color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.labelLarge, textAlign = TextAlign.Center @@ -1114,7 +1155,7 @@ private fun LeaveProfile( OpenEdXOutlinedButton( borderColor = MaterialTheme.appColors.textPrimary, textColor = MaterialTheme.appColors.textPrimary, - text = stringResource(id = org.openedx.profile.R.string.profile_keep_editing), + text = stringResource(id = R.string.profile_keep_editing), onClick = onDismissRequest ) } @@ -1130,12 +1171,17 @@ private fun LeaveProfileLandscape( val screenWidth = configuration.screenWidthDp.dp Dialog( onDismissRequest = onDismissRequest, - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false, usePlatformDefaultWidth = false), + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ), content = { Card( modifier = Modifier .width(screenWidth * 0.7f) - .clip(MaterialTheme.appShapes.courseImageShape), + .clip(MaterialTheme.appShapes.courseImageShape) + .semantics { testTagsAsResourceId = true }, backgroundColor = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.courseImageShape ) { @@ -1152,22 +1198,26 @@ private fun LeaveProfileLandscape( ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource(id = org.openedx.profile.R.drawable.profile_ic_save), + painter = painterResource(id = R.drawable.profile_ic_save), contentDescription = null, tint = MaterialTheme.appColors.onBackground ) Spacer(Modifier.height(20.dp)) Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_leave_profile), + modifier = Modifier + .testTag("txt_leave_profile_dialog_title") + .fillMaxWidth(), + text = stringResource(id = R.string.profile_leave_profile), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleLarge, textAlign = TextAlign.Center ) Spacer(Modifier.height(8.dp)) Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_changes_you_made), + modifier = Modifier + .testTag("txt_leave_profile_dialog_description") + .fillMaxWidth(), + text = stringResource(id = R.string.profile_changes_you_made), color = MaterialTheme.appColors.textFieldText, style = MaterialTheme.appTypography.titleSmall, textAlign = TextAlign.Center @@ -1179,11 +1229,12 @@ private fun LeaveProfileLandscape( horizontalAlignment = Alignment.CenterHorizontally ) { OpenEdXButton( - text = stringResource(id = org.openedx.profile.R.string.profile_leave), + text = stringResource(id = R.string.profile_leave), backgroundColor = MaterialTheme.appColors.warning, content = { AutoSizeText( - text = stringResource(id = org.openedx.profile.R.string.profile_leave), + modifier = Modifier.testTag("txt_leave_profile_dialog_leave"), + text = stringResource(id = R.string.profile_leave), style = MaterialTheme.appTypography.bodyMedium, color = MaterialTheme.appColors.textDark ) @@ -1194,11 +1245,13 @@ private fun LeaveProfileLandscape( OpenEdXOutlinedButton( borderColor = MaterialTheme.appColors.textPrimary, textColor = MaterialTheme.appColors.textPrimary, - text = stringResource(id = org.openedx.profile.R.string.profile_keep_editing), + text = stringResource(id = R.string.profile_keep_editing), onClick = onDismissRequest, content = { AutoSizeText( - text = stringResource(id = org.openedx.profile.R.string.profile_keep_editing), + modifier = Modifier + .testTag("btn_leave_profile_dialog_keep_editing"), + text = stringResource(id = R.string.profile_keep_editing), style = MaterialTheme.appTypography.bodyMedium, color = MaterialTheme.appColors.textPrimary ) @@ -1228,6 +1281,25 @@ fun LeaveProfileLandscapePreview() { ) } +@Preview +@Composable +fun ChangeProfileImagePreview() { + ChangeImageDialog( + onSelectFromGalleryClick = {}, + onRemoveImageClick = {}, + onCancelClick = {} + ) +} + +@Preview +@Composable +fun LimitedProfilePreview() { + LimitedProfileDialog( + modifier = Modifier, + onCloseClick = {} + ) +} + @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_NO) diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt index cd544a7a7..9ae81cc05 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt @@ -46,12 +46,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -63,6 +67,7 @@ import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.domain.model.AgreementUrls import org.openedx.core.domain.model.ProfileImage +import org.openedx.core.extension.tagId import org.openedx.core.presentation.global.AppData import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage @@ -83,7 +88,7 @@ import org.openedx.profile.presentation.ui.ProfileInfoSection import org.openedx.profile.presentation.ui.ProfileTopic import org.openedx.profile.domain.model.Configuration as AppConfiguration -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable internal fun ProfileView( windowSize: WindowSize, @@ -101,7 +106,11 @@ internal fun ProfileView( onRefresh = { onAction(ProfileViewAction.SwipeRefresh) }) Scaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState ) { paddingValues -> @@ -153,6 +162,7 @@ internal fun ProfileView( ) { Text( modifier = Modifier + .testTag("txt_profile_title") .fillMaxWidth(), text = stringResource(id = R.string.core_profile), color = MaterialTheme.appColors.textPrimary, @@ -162,6 +172,7 @@ internal fun ProfileView( IconText( modifier = Modifier + .testTag("it_edit_account") .height(48.dp) .padding(end = 24.dp), text = stringResource(org.openedx.profile.R.string.profile_edit), @@ -251,6 +262,7 @@ internal fun ProfileView( private fun SettingsSection(onVideoSettingsClick: () -> Unit) { Column { Text( + modifier = Modifier.testTag("txt_settings"), text = stringResource(id = org.openedx.profile.R.string.profile_settings), style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textSecondary @@ -280,6 +292,7 @@ private fun SupportInfoSection( ) { Column { Text( + modifier = Modifier.testTag("txt_support_info"), text = stringResource(id = org.openedx.profile.R.string.profile_support_info), style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textSecondary @@ -366,6 +379,7 @@ private fun SupportInfoSection( private fun LogoutButton(onClick: () -> Unit) { Card( modifier = Modifier + .testTag("btn_logout") .fillMaxWidth() .clickable { onClick() @@ -379,6 +393,7 @@ private fun LogoutButton(onClick: () -> Unit) { horizontalArrangement = Arrangement.SpaceBetween ) { Text( + modifier = Modifier.testTag("txt_logout"), text = stringResource(id = org.openedx.profile.R.string.profile_logout), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.error @@ -392,6 +407,7 @@ private fun LogoutButton(onClick: () -> Unit) { } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun LogoutDialog( onDismissRequest: () -> Unit, @@ -414,7 +430,8 @@ private fun LogoutDialog( MaterialTheme.appColors.cardViewBorder, MaterialTheme.appShapes.cardShape ) - .padding(horizontal = 40.dp, vertical = 36.dp), + .padding(horizontal = 40.dp, vertical = 36.dp) + .semantics { testTagsAsResourceId = true }, horizontalAlignment = Alignment.CenterHorizontally ) { Box( @@ -422,7 +439,9 @@ private fun LogoutDialog( contentAlignment = Alignment.CenterEnd ) { IconButton( - modifier = Modifier.size(24.dp), + modifier = Modifier + .testTag("ib_close") + .size(24.dp), onClick = onDismissRequest ) { Icon( @@ -442,6 +461,7 @@ private fun LogoutDialog( ) Spacer(Modifier.size(36.dp)) Text( + modifier = Modifier.testTag("txt_logout_dialog_title"), text = stringResource(id = org.openedx.profile.R.string.profile_logout_dialog_body), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleLarge, @@ -455,17 +475,22 @@ private fun LogoutDialog( content = { Box( Modifier + .testTag("btn_logout") .fillMaxWidth(), contentAlignment = Alignment.CenterEnd ) { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag("txt_logout") + .fillMaxWidth(), text = stringResource(id = org.openedx.profile.R.string.profile_logout), color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.labelLarge, textAlign = TextAlign.Center ) Icon( + modifier = Modifier + .testTag("ic_logout"), painter = painterResource(id = org.openedx.profile.R.drawable.profile_ic_logout), contentDescription = null, tint = Color.Black @@ -491,6 +516,7 @@ private fun ProfileInfoItem( } Row( Modifier + .testTag("btn_${text.tagId()}") .fillMaxWidth() .clickable { onClick() } .padding(20.dp), @@ -498,7 +524,9 @@ private fun ProfileInfoItem( verticalAlignment = Alignment.CenterVertically ) { Text( - modifier = Modifier.weight(1f), + modifier = Modifier + .testTag("txt_${text.tagId()}") + .weight(1f), text = text, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -553,6 +581,7 @@ private fun AppVersionItemAppToDate(versionName: String) { verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( + modifier = Modifier.testTag("txt_app_version_code"), text = stringResource(id = R.string.core_version, versionName), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textPrimary @@ -570,6 +599,7 @@ private fun AppVersionItemAppToDate(versionName: String) { tint = MaterialTheme.appColors.accessGreen ) Text( + modifier = Modifier.testTag("txt_up_to_date"), text = stringResource(id = R.string.core_up_to_date), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.labelLarge @@ -586,6 +616,7 @@ private fun AppVersionItemUpgradeRecommended( ) { Row( modifier = Modifier + .testTag("btn_upgrade_recommended") .fillMaxWidth() .clickable { onClick() @@ -597,11 +628,13 @@ private fun AppVersionItemUpgradeRecommended( verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( + modifier = Modifier.testTag("txt_app_version_code"), text = stringResource(id = R.string.core_version, versionName), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textPrimary ) Text( + modifier = Modifier.testTag("txt_upgrade_recommended"), text = stringResource( id = R.string.core_tap_to_update_to_version, appUpgradeEvent.newVersionName @@ -626,6 +659,7 @@ fun AppVersionItemUpgradeRequired( ) { Row( modifier = Modifier + .testTag("btn_upgrade_required") .fillMaxWidth() .clickable { onClick() @@ -646,12 +680,14 @@ fun AppVersionItemUpgradeRequired( contentDescription = null ) Text( + modifier = Modifier.testTag("txt_app_version_code"), text = stringResource(id = R.string.core_version, versionName), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textPrimary ) } Text( + modifier = Modifier.testTag("txt_upgrade_required"), text = stringResource(id = R.string.core_tap_to_install_required_app_update), color = MaterialTheme.appColors.textAccent, style = MaterialTheme.appTypography.labelLarge @@ -747,7 +783,7 @@ private val mockAppData = AppData( versionName = "1.0.0", ) -private val mockAccount = Account( +val mockAccount = Account( username = "thom84", bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", requiresParentalConsent = true, diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt index 46c645a76..cc76fc859 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt @@ -6,34 +6,59 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment -import org.openedx.core.R +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.domain.model.VideoQuality -import org.openedx.core.ui.* +import org.openedx.core.extension.nonZero +import org.openedx.core.extension.tagId +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.ui.windowSizeValue import org.openedx.profile.R as profileR class VideoQualityFragment : Fragment() { @@ -54,7 +79,7 @@ class VideoQualityFragment : Fragment() { VideoQualityScreen( windowSize = windowSize, - videoQuality = videoQuality, + selectedVideoQuality = videoQuality, onQualityChanged = { viewModel.setVideoDownloadQuality(it) }, @@ -67,10 +92,11 @@ class VideoQualityFragment : Fragment() { } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun VideoQualityScreen( windowSize: WindowSize, - videoQuality: VideoQuality, + selectedVideoQuality: VideoQuality, onQualityChanged: (VideoQuality) -> Unit, onBackClick: () -> Unit ) { @@ -78,7 +104,10 @@ private fun VideoQualityScreen( Scaffold( modifier = Modifier .fillMaxSize() - .navigationBarsPadding(), + .navigationBarsPadding() + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState, ) { paddingValues -> @@ -114,22 +143,12 @@ private fun VideoQualityScreen( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally ) { - Box( + Toolbar( modifier = topBarWidth, - contentAlignment = Alignment.CenterStart - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = stringResource(id = profileR.string.profile_video_streaming_quality), - color = MaterialTheme.appColors.textPrimary, - textAlign = TextAlign.Center, - style = MaterialTheme.appTypography.titleMedium - ) - BackBtn(Modifier.padding(start = 8.dp)) { - onBackClick() - } - } + label = stringResource(id = profileR.string.profile_video_streaming_quality), + canShowBackBtn = true, + onBackClick = onBackClick + ) Column( modifier = Modifier @@ -137,50 +156,17 @@ private fun VideoQualityScreen( .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - val autoQuality = - stringResource(id = R.string.auto_recommended_text).split(Regex("\\s"), 2) - QualityOption( - title = autoQuality[0], - description = autoQuality[1], - selected = videoQuality == VideoQuality.AUTO, - onClick = { - onQualityChanged(VideoQuality.AUTO) - } - ) - Divider() - val option360p = - stringResource(id = R.string.video_quality_p360).split(Regex("\\s"), 2) - QualityOption( - title = option360p[0], - description = option360p[1], - selected = videoQuality == VideoQuality.OPTION_360P, - onClick = { - onQualityChanged(VideoQuality.OPTION_360P) - } - ) - Divider() - val option540p = - stringResource(id = R.string.video_quality_p540) - QualityOption( - title = option540p, - description = "", - selected = videoQuality == VideoQuality.OPTION_540P, - onClick = { - onQualityChanged(VideoQuality.OPTION_540P) - } - ) - Divider() - val option720p = - stringResource(id = R.string.video_quality_p720).split(Regex("\\s"), 2) - QualityOption( - title = option720p[0], - description = option720p[1], - selected = videoQuality == VideoQuality.OPTION_720P, - onClick = { - onQualityChanged(VideoQuality.OPTION_720P) - } - ) - Divider() + VideoQuality.values().forEach { videoQuality -> + QualityOption( + title = stringResource(id = videoQuality.titleResId), + description = videoQuality.desResId.nonZero() + ?.let { stringResource(id = videoQuality.desResId) } ?: "", + selected = selectedVideoQuality == videoQuality, + onClick = { + onQualityChanged(videoQuality) + } + ) + } } } } @@ -190,12 +176,13 @@ private fun VideoQualityScreen( @Composable private fun QualityOption( title: String, - description: String?, + description: String, selected: Boolean, onClick: () -> Unit ) { Row( Modifier + .testTag("btn_video_quality_${title.tagId()}") .fillMaxWidth() .height(90.dp) .clickable { @@ -209,14 +196,16 @@ private fun QualityOption( verticalArrangement = Arrangement.Center ) { Text( + modifier = Modifier.testTag("txt_video_quality_title_${title.tagId()}"), text = title, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) - if (!description.isNullOrEmpty()) { + if (description.isNotEmpty()) { Spacer(Modifier.height(4.dp)) Text( - text = description.replace(Regex("[(|)]"), ""), + modifier = Modifier.testTag("txt_video_quality_description_${title.tagId()}"), + text = description, color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.labelMedium ) @@ -224,13 +213,14 @@ private fun QualityOption( } if (selected) { Icon( + modifier = Modifier.testTag("ic_video_quality_selected_${title.tagId()}"), imageVector = Icons.Filled.Done, tint = MaterialTheme.appColors.primary, contentDescription = null ) } } - + Divider() } @Preview(uiMode = UI_MODE_NIGHT_NO) @@ -240,8 +230,8 @@ private fun VideoQualityScreenPreview() { OpenEdXTheme { VideoQualityScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - videoQuality = VideoQuality.OPTION_720P, + selectedVideoQuality = VideoQuality.OPTION_720P, onQualityChanged = {}, onBackClick = {}) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt index 747df792a..42ecf16f6 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt @@ -6,31 +6,61 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.runtime.* +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.domain.model.VideoSettings -import org.openedx.core.ui.* +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue import org.openedx.profile.presentation.ProfileRouter -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.profile.R as profileR class VideoSettingsFragment : Fragment() { @@ -76,6 +106,7 @@ class VideoSettingsFragment : Fragment() { } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun VideoSettingsScreen( windowSize: WindowSize, @@ -91,7 +122,11 @@ private fun VideoSettingsScreen( } Scaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, scaffoldState = scaffoldState ) { paddingValues -> @@ -127,23 +162,12 @@ private fun VideoSettingsScreen( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally ) { - Box( + Toolbar( modifier = topBarWidth, - contentAlignment = Alignment.CenterStart - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_video_settings), - color = MaterialTheme.appColors.textPrimary, - textAlign = TextAlign.Center, - style = MaterialTheme.appTypography.titleMedium - ) - - BackBtn(modifier = Modifier.padding(start = 8.dp)) { - onBackClick() - } - } + label = stringResource(id = org.openedx.profile.R.string.profile_video_settings), + canShowBackBtn = true, + onBackClick = onBackClick + ) Column( modifier = Modifier.then(contentWidth), @@ -151,6 +175,7 @@ private fun VideoSettingsScreen( ) { Row( Modifier + .testTag("btn_wifi_only") .fillMaxWidth() .height(92.dp) .noRippleClickable { @@ -162,18 +187,21 @@ private fun VideoSettingsScreen( ) { Column(Modifier.weight(1f)) { Text( + modifier = Modifier.testTag("txt_wifi_only_label"), text = stringResource(id = profileR.string.profile_wifi_only_download), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( + modifier = Modifier.testTag("txt_wifi_only_description"), text = stringResource(id = profileR.string.profile_only_download_when_wifi_turned_on), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.labelMedium ) } Switch( + modifier = Modifier.testTag("sw_wifi_only"), checked = wifiDownloadOnly, onCheckedChange = { wifiDownloadOnly = !wifiDownloadOnly @@ -188,6 +216,7 @@ private fun VideoSettingsScreen( Divider() Row( Modifier + .testTag("btn_video_quality") .fillMaxWidth() .height(92.dp) .clickable { @@ -198,12 +227,14 @@ private fun VideoSettingsScreen( ) { Column(Modifier.weight(1f)) { Text( + modifier = Modifier.testTag("txt_video_quality_label"), text = stringResource(id = profileR.string.profile_video_streaming_quality), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( + modifier = Modifier.testTag("txt_video_quality_description"), text = stringResource(id = videoSettings.videoQuality.titleResId), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.labelMedium @@ -235,4 +266,4 @@ private fun VideoSettingsScreenPreview() { videoSettings = VideoSettings.default ) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt b/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt index 9dceab592..c47820f23 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt @@ -1,5 +1,6 @@ package org.openedx.profile.presentation.ui +import android.content.res.Configuration import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,9 +19,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest @@ -29,6 +32,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.profile.domain.model.Account +import org.openedx.profile.presentation.profile.compose.mockAccount @Composable fun ProfileTopic(account: Account) { @@ -47,8 +51,12 @@ fun ProfileTopic(account: Account) { .error(R.drawable.core_ic_default_profile_picture) .placeholder(R.drawable.core_ic_default_profile_picture) .build(), - contentDescription = stringResource(id = R.string.core_accessibility_user_profile_image, account.username), + contentDescription = stringResource( + id = R.string.core_accessibility_user_profile_image, + account.username + ), modifier = Modifier + .testTag("img_profile") .border( 2.dp, MaterialTheme.appColors.onSurface, @@ -61,6 +69,7 @@ fun ProfileTopic(account: Account) { if (account.name.isNotEmpty()) { Spacer(modifier = Modifier.height(20.dp)) Text( + modifier = Modifier.testTag("txt_profile_name"), text = account.name, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.headlineSmall @@ -68,6 +77,7 @@ fun ProfileTopic(account: Account) { } Spacer(modifier = Modifier.height(4.dp)) Text( + modifier = Modifier.testTag("txt_profile_username"), text = "@${account.username}", color = MaterialTheme.appColors.textPrimaryVariant, style = MaterialTheme.appTypography.labelLarge @@ -81,6 +91,7 @@ fun ProfileInfoSection(account: Account) { if (account.yearOfBirth != null || account.bio.isNotEmpty()) { Column { Text( + modifier = Modifier.testTag("txt_profile_info_label"), text = stringResource(id = org.openedx.profile.R.string.profile_prof_info), style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textSecondary @@ -100,6 +111,7 @@ fun ProfileInfoSection(account: Account) { ) { if (account.yearOfBirth != null) { Text( + modifier = Modifier.testTag("txt_profile_year_of_birth"), text = buildAnnotatedString { val value = if (account.yearOfBirth != null) { account.yearOfBirth.toString() @@ -123,6 +135,7 @@ fun ProfileInfoSection(account: Account) { } if (account.bio.isNotEmpty()) { Text( + modifier = Modifier.testTag("txt_profile_bio"), text = buildAnnotatedString { val text = stringResource( id = org.openedx.profile.R.string.profile_bio, @@ -145,4 +158,22 @@ fun ProfileInfoSection(account: Account) { } } } -} \ No newline at end of file +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ProfileTopicPreview() { + ProfileTopic( + account = mockAccount + ) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun ProfileInfoSectionPreview() { + ProfileInfoSection( + account = mockAccount + ) +}