Skip to content

Commit 0997b06

Browse files
committed
refactor(test): Improve UI test stability and maintainability
This commit introduces significant refactoring to the UI testing framework to enhance stability and maintainability. - A new file `SecurityDialogCommons.kt` is created to centralize test tags for all security-related dialogs (`EnterPasswordDialog`, `ConfirmPasswordDialog`, `ChangePasswordDialog`) and the sign-in screen. - Dynamic test tag generation based on string resources (`descTagTriple`) has been removed in favor of these new statically defined constants. - The confirm button in password dialogs, previously identified by the text "yes", is now a reusable `PasswordSaveButton` with the text "Save" and a unique test tag, making tests less reliant on hardcoded strings and more robust. - Test case functions in `BaseTestCase.kt` are now `suspend` functions to better handle asynchronous operations. - Test timeouts have been increased in `ext.kt` and `PrepopulateDbTestCase.kt` to reduce flakiness in CI environments. - `AndroidUiTests` are now marked as `@FlakyTest`. - Unused or redundant retry logic (`retryUntil`, `retryUntilNotDisplayed`) has been removed from `ext.kt`. - `waitUntilNotExist` is introduced and used to ensure dialogs are properly dismissed before proceeding, improving test reliability.
1 parent c6ad0f9 commit 0997b06

File tree

26 files changed

+259
-202
lines changed

26 files changed

+259
-202
lines changed

app/android/src/androidTest/java/com/softartdev/notedelight/ui/AndroidUiTests.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.softartdev.notedelight.ui
33
import androidx.compose.ui.test.junit4.createAndroidComposeRule
44
import androidx.test.espresso.Espresso
55
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import androidx.test.filters.FlakyTest
67
import androidx.test.filters.LargeTest
78
import com.softartdev.notedelight.MainActivity
89
import leakcanary.DetectLeaksAfterTestSuccess
@@ -13,6 +14,7 @@ import org.junit.rules.RuleChain
1314
import org.junit.runner.RunWith
1415

1516
@LargeTest
17+
@FlakyTest
1618
@RunWith(AndroidJUnit4::class)
1719
class AndroidUiTests : AbstractJvmUiTests() {
1820

ui/shared/src/commonMain/composeResources/values-ru/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@
6666
<string name="copy">Копировать</string>
6767
<string name="detail_pane_placeholder">Выберите или создайте заметку</string>
6868
<string name="info">Информация</string>
69+
<string name="save">Сохранить</string>
6970
</resources>

ui/shared/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@
6666
<string name="copy">Copy</string>
6767
<string name="detail_pane_placeholder">Select or create a note</string>
6868
<string name="info">Info</string>
69+
<string name="save">Save</string>
6970
</resources>

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/PasswordField.kt

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,18 @@ import androidx.compose.runtime.setValue
1919
import androidx.compose.ui.Modifier
2020
import androidx.compose.ui.autofill.ContentType
2121
import androidx.compose.ui.platform.testTag
22+
import androidx.compose.ui.semantics.contentDescription
2223
import androidx.compose.ui.semantics.contentType
2324
import androidx.compose.ui.semantics.semantics
2425
import androidx.compose.ui.text.input.ImeAction
2526
import androidx.compose.ui.text.input.KeyboardType
2627
import androidx.compose.ui.text.input.PasswordVisualTransformation
2728
import androidx.compose.ui.text.input.VisualTransformation
28-
import com.softartdev.notedelight.util.runBlockingAll
2929
import notedelight.ui.shared.generated.resources.Res
3030
import notedelight.ui.shared.generated.resources.enter_password
31-
import org.jetbrains.compose.resources.StringResource
32-
import org.jetbrains.compose.resources.getString
3331
import org.jetbrains.compose.resources.stringResource
3432
import org.jetbrains.compose.ui.tooling.preview.Preview
3533

36-
private const val PASSWORD_LABEL_TAG = "PASSWORD_LABEL_TAG"
37-
private const val PASSWORD_VISIBILITY_TAG = "PASSWORD_VISIBILITY_TAG"
38-
private const val PASSWORD_FIELD_TAG = "PASSWORD_FIELD_TAG"
39-
40-
fun StringResource.descTagTriple(): Triple<String, String, String> = this
41-
.let { runBlockingAll { getString(it) } }
42-
.let(String::descTagTriple)
43-
44-
fun String.descTagTriple(): Triple<String, String, String> = Triple(
45-
first = "${this}_$PASSWORD_LABEL_TAG",
46-
second = "${this}_$PASSWORD_VISIBILITY_TAG",
47-
third = "${this}_$PASSWORD_FIELD_TAG"
48-
)
49-
50-
@Composable
51-
fun rememberTagTriple(desc: String): Triple<String, String, String> =
52-
remember(desc, desc::descTagTriple)
53-
5434
@Composable
5535
fun PasswordField(
5636
modifier: Modifier = Modifier,
@@ -61,6 +41,9 @@ fun PasswordField(
6141
keyboardActions: KeyboardActions = KeyboardActions.Default,
6242
contentDescription: String = stringResource(Res.string.enter_password),
6343
passwordContentType: ContentType = ContentType.Password,
44+
labelTag: String,
45+
visibilityTag: String,
46+
fieldTag: String,
6447
) = PasswordField(
6548
modifier = modifier,
6649
password = passwordState.value,
@@ -71,6 +54,9 @@ fun PasswordField(
7154
keyboardActions = keyboardActions,
7255
contentDescription = contentDescription,
7356
passwordContentType = passwordContentType,
57+
labelTag = labelTag,
58+
visibilityTag = visibilityTag,
59+
fieldTag = fieldTag,
7460
)
7561

7662
@Composable
@@ -84,12 +70,17 @@ fun PasswordField(
8470
keyboardActions: KeyboardActions = KeyboardActions.Default,
8571
contentDescription: String = stringResource(Res.string.enter_password),
8672
passwordContentType: ContentType = ContentType.Password,
73+
labelTag: String,
74+
visibilityTag: String,
75+
fieldTag: String,
8776
) {
8877
val labelState by remember(label, isError) { mutableStateOf(label) } // workaround for ui-tests
89-
val (labelTag, visibilityTag, fieldTag) = rememberTagTriple(contentDescription)
9078
var passwordVisibility: Boolean by remember { mutableStateOf(false) }
9179
TextField(
92-
modifier = modifier.testTag(fieldTag).semantics { contentType = passwordContentType },
80+
modifier = modifier.testTag(fieldTag).semantics {
81+
contentType = passwordContentType
82+
this@semantics.contentDescription = contentDescription
83+
},
9384
label = { Text(labelState, modifier = Modifier.testTag(labelTag)) },
9485
leadingIcon = {
9586
IconButton(onClick = { passwordVisibility = !passwordVisibility },
@@ -113,4 +104,8 @@ fun PasswordField(
113104

114105
@Preview
115106
@Composable
116-
fun PreviewPasswordField() = PasswordField()
107+
fun PreviewPasswordField() = PasswordField(
108+
labelTag = "PREVIEW_PASSWORD_FIELD_LABEL_TAG",
109+
visibilityTag = "PREVIEW_PASSWORD_FIELD_VISIBILITY_TAG",
110+
fieldTag = "PREVIEW_PASSWORD_FIELD_FIELD_TAG",
111+
)

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/dialog/security/ChangePasswordDialog.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,9 @@ import notedelight.ui.shared.generated.resources.changing_password_dialog_title
3434
import notedelight.ui.shared.generated.resources.enter_new_password
3535
import notedelight.ui.shared.generated.resources.enter_old_password
3636
import notedelight.ui.shared.generated.resources.repeat_new_password
37-
import notedelight.ui.shared.generated.resources.yes
3837
import org.jetbrains.compose.resources.stringResource
3938
import org.jetbrains.compose.ui.tooling.preview.Preview
4039

41-
const val CHANGE_PASSWORD_DIALOG_TAG = "CHANGE_PASSWORD_DIALOG_TAG"
42-
4340
@Composable
4441
fun ChangePasswordDialog(changeViewModel: ChangeViewModel) {
4542
val result: ChangeResult by changeViewModel.stateFlow.collectAsState()
@@ -71,6 +68,9 @@ fun ShowChangePasswordDialog(
7168
imeAction = ImeAction.Next,
7269
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(Down) }),
7370
contentDescription = stringResource(Res.string.enter_old_password),
71+
labelTag = CHANGE_PASSWORD_DIALOG_OLD_LABEL_TAG,
72+
visibilityTag = CHANGE_PASSWORD_DIALOG_OLD_VISIBILITY_TAG,
73+
fieldTag = CHANGE_PASSWORD_DIALOG_OLD_FIELD_TAG,
7474
)
7575
Spacer(modifier = Modifier.height(8.dp))
7676
PasswordField(
@@ -82,6 +82,9 @@ fun ShowChangePasswordDialog(
8282
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(Down) }),
8383
contentDescription = stringResource(Res.string.enter_new_password),
8484
passwordContentType = ContentType.NewPassword,
85+
labelTag = CHANGE_PASSWORD_DIALOG_NEW_LABEL_TAG,
86+
visibilityTag = CHANGE_PASSWORD_DIALOG_NEW_VISIBILITY_TAG,
87+
fieldTag = CHANGE_PASSWORD_DIALOG_NEW_FIELD_TAG,
8588
)
8689
Spacer(modifier = Modifier.height(8.dp))
8790
PasswordField(
@@ -92,11 +95,14 @@ fun ShowChangePasswordDialog(
9295
imeAction = ImeAction.Done,
9396
keyboardActions = KeyboardActions(onDone = { onAction(ChangeAction.OnChangeClick) }),
9497
contentDescription = stringResource(Res.string.repeat_new_password),
95-
passwordContentType = ContentType.NewPassword
98+
passwordContentType = ContentType.NewPassword,
99+
labelTag = CHANGE_PASSWORD_DIALOG_REPEAT_LABEL_TAG,
100+
visibilityTag = CHANGE_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG,
101+
fieldTag = CHANGE_PASSWORD_DIALOG_REPEAT_FIELD_TAG,
96102
)
97103
}
98104
},
99-
confirmButton = { Button(onClick = { onAction(ChangeAction.OnChangeClick) }) { Text(stringResource(Res.string.yes)) } },
105+
confirmButton = { PasswordSaveButton(tag = CHANGE_PASSWORD_DIALOG_SAVE_BUTTON_TAG, onClick = { onAction(ChangeAction.OnChangeClick) }) },
100106
dismissButton = { Button(onClick = { onAction(ChangeAction.Cancel) }) { Text(stringResource(Res.string.cancel)) } },
101107
onDismissRequest = { onAction(ChangeAction.Cancel) }
102108
)

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/dialog/security/ConfirmPasswordDialog.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.compose.ui.autofill.ContentType
1919
import androidx.compose.ui.focus.FocusDirection.Companion.Down
2020
import androidx.compose.ui.platform.LocalAutofillManager
2121
import androidx.compose.ui.platform.LocalFocusManager
22+
import androidx.compose.ui.platform.testTag
2223
import androidx.compose.ui.text.input.ImeAction
2324
import androidx.compose.ui.unit.dp
2425
import com.softartdev.notedelight.presentation.settings.security.confirm.ConfirmAction
@@ -32,7 +33,6 @@ import notedelight.ui.shared.generated.resources.confirm_password
3233
import notedelight.ui.shared.generated.resources.confirm_password_dialog_subtitle
3334
import notedelight.ui.shared.generated.resources.confirm_password_dialog_title
3435
import notedelight.ui.shared.generated.resources.enter_password
35-
import notedelight.ui.shared.generated.resources.yes
3636
import org.jetbrains.compose.resources.stringResource
3737
import org.jetbrains.compose.ui.tooling.preview.Preview
3838

@@ -51,6 +51,7 @@ fun ShowConfirmPasswordDialog(
5151
result: ConfirmResult,
5252
onAction: (action: ConfirmAction) -> Unit = {}
5353
) = AlertDialog(
54+
modifier = Modifier.testTag(CONFIRM_PASSWORD_DIALOG_TAG),
5455
title = { Text(text = stringResource(Res.string.confirm_password_dialog_title)) },
5556
text = {
5657
val focusManager = LocalFocusManager.current
@@ -66,7 +67,10 @@ fun ShowConfirmPasswordDialog(
6667
contentDescription = stringResource(Res.string.enter_password),
6768
passwordContentType = ContentType.NewPassword,
6869
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(Down) }),
69-
imeAction = ImeAction.Next
70+
imeAction = ImeAction.Next,
71+
labelTag = CONFIRM_PASSWORD_DIALOG_LABEL_TAG,
72+
visibilityTag = CONFIRM_PASSWORD_DIALOG_VISIBILITY_TAG,
73+
fieldTag = CONFIRM_PASSWORD_DIALOG_FIELD_TAG
7074
)
7175
Spacer(modifier = Modifier.height(8.dp))
7276
PasswordField(
@@ -77,11 +81,14 @@ fun ShowConfirmPasswordDialog(
7781
contentDescription = stringResource(Res.string.confirm_password),
7882
passwordContentType = ContentType.NewPassword,
7983
keyboardActions = KeyboardActions { onAction(ConfirmAction.OnConfirmClick) },
80-
imeAction = ImeAction.Done
84+
imeAction = ImeAction.Done,
85+
labelTag = CONFIRM_PASSWORD_DIALOG_REPEAT_LABEL_TAG,
86+
visibilityTag = CONFIRM_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG,
87+
fieldTag = CONFIRM_PASSWORD_DIALOG_REPEAT_FIELD_TAG
8188
)
8289
}
8390
},
84-
confirmButton = { Button(onClick = { onAction(ConfirmAction.OnConfirmClick) }) { Text(stringResource(Res.string.yes)) } },
91+
confirmButton = { PasswordSaveButton(tag = CONFIRM_PASSWORD_DIALOG_SAVE_BUTTON_TAG, onClick = { onAction(ConfirmAction.OnConfirmClick) }) },
8592
dismissButton = { Button(onClick = { onAction(ConfirmAction.Cancel) }) { Text(stringResource(Res.string.cancel)) } },
8693
onDismissRequest = { onAction(ConfirmAction.Cancel) }
8794
)

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/dialog/security/EnterPasswordDialog.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,9 @@ import notedelight.ui.shared.generated.resources.cancel
2929
import notedelight.ui.shared.generated.resources.enter_password
3030
import notedelight.ui.shared.generated.resources.enter_password_dialog_subtitle
3131
import notedelight.ui.shared.generated.resources.enter_password_dialog_title
32-
import notedelight.ui.shared.generated.resources.yes
3332
import org.jetbrains.compose.resources.stringResource
3433
import org.jetbrains.compose.ui.tooling.preview.Preview
3534

36-
const val ENTER_PASSWORD_DIALOG_TAG = "ENTER_PASSWORD_DIALOG_TAG"
37-
3835
@Composable
3936
fun EnterPasswordDialog(enterViewModel: EnterViewModel) {
4037
val result: EnterResult by enterViewModel.stateFlow.collectAsState()
@@ -64,11 +61,14 @@ fun ShowEnterPasswordDialog(
6461
isError = result.isError,
6562
contentDescription = stringResource(Res.string.enter_password),
6663
imeAction = ImeAction.Done,
67-
keyboardActions = KeyboardActions { onAction(EnterAction.OnEnterClick) }
64+
keyboardActions = KeyboardActions { onAction(EnterAction.OnEnterClick) },
65+
labelTag = ENTER_PASSWORD_DIALOG_LABEL_TAG,
66+
visibilityTag = ENTER_PASSWORD_DIALOG_VISIBILITY_TAG,
67+
fieldTag = ENTER_PASSWORD_DIALOG_FIELD_TAG
6868
)
6969
}
7070
},
71-
confirmButton = { Button(onClick = { onAction(EnterAction.OnEnterClick) }) { Text(stringResource(Res.string.yes)) } },
71+
confirmButton = { PasswordSaveButton(tag = ENTER_PASSWORD_DIALOG_SAVE_BUTTON_TAG, onClick = { onAction(EnterAction.OnEnterClick) }) },
7272
dismissButton = { Button(onClick = { onAction(EnterAction.Cancel) }) { Text(stringResource(Res.string.cancel)) } },
7373
onDismissRequest = { onAction(EnterAction.Cancel) }
7474
)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.softartdev.notedelight.ui.dialog.security
2+
3+
import androidx.compose.material3.Button
4+
import androidx.compose.material3.Text
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Modifier
7+
import androidx.compose.ui.platform.testTag
8+
import notedelight.ui.shared.generated.resources.Res
9+
import notedelight.ui.shared.generated.resources.save
10+
import org.jetbrains.compose.resources.stringResource
11+
12+
/**
13+
* Test tags for [EnterPasswordDialog]
14+
*/
15+
const val ENTER_PASSWORD_DIALOG_TAG = "ENTER_PASSWORD_DIALOG_TAG"
16+
17+
const val ENTER_PASSWORD_DIALOG_LABEL_TAG = "ENTER_PASSWORD_DIALOG_LABEL_TAG"
18+
const val ENTER_PASSWORD_DIALOG_VISIBILITY_TAG = "ENTER_PASSWORD_DIALOG_VISIBILITY_TAG"
19+
const val ENTER_PASSWORD_DIALOG_FIELD_TAG = "ENTER_PASSWORD_DIALOG_FIELD_TAG"
20+
21+
const val ENTER_PASSWORD_DIALOG_SAVE_BUTTON_TAG = "ENTER_PASSWORD_DIALOG_SAVE_BUTTON_TAG"
22+
23+
/**
24+
* Test tags for [ConfirmPasswordDialog]
25+
*/
26+
const val CONFIRM_PASSWORD_DIALOG_TAG = "CONFIRM_PASSWORD_DIALOG_TAG"
27+
28+
const val CONFIRM_PASSWORD_DIALOG_LABEL_TAG = "CONFIRM_PASSWORD_DIALOG_LABEL_TAG"
29+
const val CONFIRM_PASSWORD_DIALOG_VISIBILITY_TAG = "CONFIRM_PASSWORD_DIALOG_VISIBILITY_TAG"
30+
const val CONFIRM_PASSWORD_DIALOG_FIELD_TAG = "CONFIRM_PASSWORD_DIALOG_FIELD_TAG"
31+
32+
const val CONFIRM_PASSWORD_DIALOG_REPEAT_LABEL_TAG = "CONFIRM_PASSWORD_DIALOG_REPEAT_LABEL_TAG"
33+
const val CONFIRM_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG = "CONFIRM_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG"
34+
const val CONFIRM_PASSWORD_DIALOG_REPEAT_FIELD_TAG = "CONFIRM_PASSWORD_DIALOG_REPEAT_FIELD_TAG"
35+
36+
const val CONFIRM_PASSWORD_DIALOG_SAVE_BUTTON_TAG = "CONFIRM_PASSWORD_DIALOG_SAVE_BUTTON_TAG"
37+
38+
/**
39+
* Test tags for [ChangePasswordDialog]
40+
*/
41+
const val CHANGE_PASSWORD_DIALOG_TAG = "CHANGE_PASSWORD_DIALOG_TAG"
42+
43+
const val CHANGE_PASSWORD_DIALOG_OLD_LABEL_TAG = "CHANGE_PASSWORD_DIALOG_OLD_LABEL_TAG"
44+
const val CHANGE_PASSWORD_DIALOG_OLD_VISIBILITY_TAG = "CHANGE_PASSWORD_DIALOG_OLD_VISIBILITY_TAG"
45+
const val CHANGE_PASSWORD_DIALOG_OLD_FIELD_TAG = "CHANGE_PASSWORD_DIALOG_OLD_FIELD_TAG"
46+
47+
const val CHANGE_PASSWORD_DIALOG_NEW_LABEL_TAG = "CHANGE_PASSWORD_DIALOG_NEW_LABEL_TAG"
48+
const val CHANGE_PASSWORD_DIALOG_NEW_VISIBILITY_TAG = "CHANGE_PASSWORD_DIALOG_NEW_VISIBILITY_TAG"
49+
const val CHANGE_PASSWORD_DIALOG_NEW_FIELD_TAG = "CHANGE_PASSWORD_DIALOG_NEW_FIELD_TAG"
50+
51+
const val CHANGE_PASSWORD_DIALOG_REPEAT_LABEL_TAG = "CHANGE_PASSWORD_DIALOG_REPEAT_LABEL_TAG"
52+
const val CHANGE_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG = "CHANGE_PASSWORD_DIALOG_REPEAT_VISIBILITY_TAG"
53+
const val CHANGE_PASSWORD_DIALOG_REPEAT_FIELD_TAG = "CHANGE_PASSWORD_DIALOG_REPEAT_FIELD_TAG"
54+
55+
const val CHANGE_PASSWORD_DIALOG_SAVE_BUTTON_TAG = "CHANGE_PASSWORD_DIALOG_SAVE_BUTTON_TAG"
56+
57+
/**
58+
* A common save button for password dialogs.
59+
*/
60+
@Composable
61+
fun PasswordSaveButton(
62+
modifier: Modifier = Modifier,
63+
tag: String,
64+
onClick: () -> Unit,
65+
) = Button(
66+
modifier = modifier.testTag(tag),
67+
onClick = onClick
68+
) { Text(stringResource(Res.string.save)) }

ui/shared/src/commonMain/kotlin/com/softartdev/notedelight/ui/signin/SignInScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ import org.jetbrains.compose.resources.StringResource
4444
import org.jetbrains.compose.resources.stringResource
4545
import org.jetbrains.compose.ui.tooling.preview.Preview
4646

47+
const val SIGN_IN_PASSWORD_LABEL_TAG = "SIGN_IN_PASSWORD_FIELD_LABEL_TAG"
48+
const val SIGN_IN_PASSWORD_VISIBILITY_TAG = "SIGN_IN_PASSWORD_FIELD_VISIBILITY_TAG"
49+
const val SIGN_IN_PASSWORD_FIELD_TAG = "SIGN_IN_PASSWORD_FIELD_TAG"
50+
4751
@Composable
4852
fun SignInScreen(signInViewModel: SignInViewModel) {
4953
val signInResultState: State<SignInResult> = signInViewModel.stateFlow.collectAsState()
@@ -101,6 +105,9 @@ fun SignInScreenBody(
101105
contentDescription = stringResource(Res.string.enter_password),
102106
imeAction = ImeAction.Go,
103107
keyboardActions = KeyboardActions { onAction(SignInAction.OnSignInClick(passwordState.value)) },
108+
labelTag = SIGN_IN_PASSWORD_LABEL_TAG,
109+
visibilityTag = SIGN_IN_PASSWORD_VISIBILITY_TAG,
110+
fieldTag = SIGN_IN_PASSWORD_FIELD_TAG,
104111
)
105112
Button(
106113
modifier = Modifier

0 commit comments

Comments
 (0)