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
+ )
+}