From 5cf61665d4a8a54b959da10dde738ad378c538bb Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 2 Jul 2026 23:18:03 -0400 Subject: [PATCH] Add unit test coverage for regV5 screens. --- .../accountlocked/AccountLockedScreen.kt | 19 ++- .../screens/aepentry/EnterAepScreen.kt | 15 +- .../screens/captcha/CaptchaScreen.kt | 4 + .../countrycode/CountryCodePickerScreen.kt | 29 +++- .../createprofile/CreateProfileScreen.kt | 18 ++- .../complete/DeviceTransferCompleteScreen.kt | 7 +- .../DeviceTransferInstructionsScreen.kt | 7 +- .../progress/DeviceTransferProgressScreen.kt | 14 +- .../setup/DeviceTransferSetupScreen.kt | 13 +- .../PhoneNumberDiscoverabilityScreen.kt | 23 ++- .../EnterLocalBackupV1PassphaseScreen.kt | 18 ++- .../screens/pincreation/PinCreationScreen.kt | 19 ++- .../screens/pinentry/PinEntryScreen.kt | 18 ++- .../quickrestore/QuickRestoreQrScreen.kt | 23 ++- .../RemoteBackupRestoreScreen.kt | 10 +- .../org/signal/registration/test/TestTags.kt | 83 ++++++++++ .../accountlocked/AccountLockedScreenTest.kt | 99 ++++++++++++ .../screens/aepentry/EnterAepScreenTest.kt | 121 +++++++++++++++ .../AllowNotificationsScreenTest.kt | 94 +++++++++++ .../screens/captcha/CaptchaScreenTest.kt | 75 +++++++++ .../CountryCodePickerScreenTest.kt | 127 +++++++++++++++ .../createprofile/CreateProfileScreenTest.kt | 142 +++++++++++++++++ .../DeviceTransferCompleteScreenTest.kt | 75 +++++++++ .../DeviceTransferInstructionsScreenTest.kt | 75 +++++++++ .../DeviceTransferProgressScreenTest.kt | 118 ++++++++++++++ .../setup/DeviceTransferSetupScreenTest.kt | 138 +++++++++++++++++ .../PhoneNumberDiscoverabilityScreenTest.kt | 146 ++++++++++++++++++ .../EnterLocalBackupV1PassphaseScreenTest.kt | 108 +++++++++++++ .../LocalBackupRestoreScreenTest.kt | 113 ++++++++++++++ .../messagesync/MessageSyncScreenTest.kt | 100 ++++++++++++ .../permissions/PermissionsScreenTest.kt | 109 +++++++++++++ .../pincreation/PinCreationScreenTest.kt | 104 +++++++++++++ .../screens/pinentry/PinEntryScreenTest.kt | 138 +++++++++++++++++ .../quickrestore/QuickRestoreQrScreenTest.kt | 83 ++++++++++ .../RemoteBackupRestoreScreenTest.kt | 91 +++++++++++ .../ArchiveRestoreSelectionScreenTest.kt | 132 ++++++++++++++++ 36 files changed, 2454 insertions(+), 54 deletions(-) create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/accountlocked/AccountLockedScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/aepentry/EnterAepScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/allownotifications/AllowNotificationsScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/captcha/CaptchaScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryCodePickerScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/createprofile/CreateProfileScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/messagesync/MessageSyncScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/permissions/PermissionsScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreenTest.kt create mode 100644 feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionScreenTest.kt diff --git a/feature/registration/src/main/java/org/signal/registration/screens/accountlocked/AccountLockedScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/accountlocked/AccountLockedScreen.kt index 2cca345ce6..bdfece1dac 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/accountlocked/AccountLockedScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/accountlocked/AccountLockedScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle @@ -41,6 +42,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags /** * Screen shown when the user's account is locked due to too many failed PIN attempts @@ -67,7 +69,9 @@ private fun OnePaneLayout( ) { val scrollState = rememberScrollState() OnePaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ACCOUNT_LOCKED_SCREEN), params = params, content = { paddingValues -> Column( @@ -89,7 +93,7 @@ private fun OnePaneLayout( isElevated = scrollState.canScrollForward ) { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .padding(vertical = 16.dp) .horizontalGutters(), @@ -121,7 +125,9 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ACCOUNT_LOCKED_SCREEN), params = params, firstPane = { paddingValues -> Column( @@ -151,7 +157,7 @@ private fun TwoPaneLayout( isElevated = firstPaneScrollState.canScrollForward || secondPaneScrollState.canScrollForward ) { Row( - modifier = modifier + modifier = Modifier .padding(vertical = 16.dp) .fillMaxWidth() .horizontalGutters(), @@ -195,7 +201,7 @@ private fun Description( private fun NextButton(onEvent: (AccountLockedScreenEvents) -> Unit, modifier: Modifier = Modifier) { Button( onClick = { onEvent(AccountLockedScreenEvents.Next) }, - modifier = modifier + modifier = modifier.testTag(TestTags.ACCOUNT_LOCKED_NEXT_BUTTON) ) { Text(stringResource(R.string.RegistrationActivity_next)) } @@ -222,7 +228,8 @@ private fun LearnMore(onEvent: (AccountLockedScreenEvents) -> Unit) { } } }, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.testTag(TestTags.ACCOUNT_LOCKED_LEARN_MORE_BUTTON) ) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt index 876d6e94b6..c39c025bb4 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.ImeAction @@ -60,6 +61,7 @@ import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper import org.signal.registration.screens.localbackuprestore.attachBackupKeyAutoFillHelper import org.signal.registration.screens.localbackuprestore.backupKeyAutoFillHelper +import org.signal.registration.test.TestTags import org.signal.registration.util.RegistrationCredentialManager @Composable @@ -84,7 +86,9 @@ private fun OnePaneLayout( val scrollState = rememberScrollState() OnePaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ENTER_AEP_SCREEN), params = params, content = { paddingValues -> Column( @@ -141,7 +145,9 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ENTER_AEP_SCREEN), params = params, firstPane = { paddingValues -> Column( @@ -258,6 +264,7 @@ private fun RecoveryKeyTextField(state: EnterAepState, onEvent: (EnterAepEvents) visualTransformation = visualTransform, modifier = Modifier .fillMaxWidth() + .testTag(TestTags.ENTER_AEP_INPUT) .focusRequester(focusRequester) .attachBackupKeyAutoFillHelper(autoFillHelper) .onGloballyPositioned { @@ -292,7 +299,7 @@ private fun FillFromPasswordManagerButton(onEvent: (EnterAepEvents) -> Unit, mod @Composable private fun NoRecoverKeyButton(onEvent: (EnterAepEvents) -> Unit, modifier: Modifier = Modifier) { TextButton( - modifier = modifier, + modifier = modifier.testTag(TestTags.ENTER_AEP_NO_KEY_BUTTON), shape = RoundedCornerShape(0.dp), onClick = { onEvent(EnterAepEvents.Cancel) } ) { @@ -303,7 +310,7 @@ private fun NoRecoverKeyButton(onEvent: (EnterAepEvents) -> Unit, modifier: Modi @Composable private fun NextButton(state: EnterAepState, onEvent: (EnterAepEvents) -> Unit, modifier: Modifier = Modifier) { Buttons.LargeTonal( - modifier = modifier, + modifier = modifier.testTag(TestTags.ENTER_AEP_NEXT_BUTTON), enabled = state.isBackupKeyValid && state.aepValidationError == null && !state.isRegistering, onClick = { onEvent(EnterAepEvents.Submit) } ) { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/captcha/CaptchaScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/captcha/CaptchaScreen.kt index d09bd21682..80ead112fd 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/captcha/CaptchaScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/captcha/CaptchaScreen.kt @@ -29,12 +29,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Previews import org.signal.registration.R +import org.signal.registration.test.TestTags /** * Screen to display a captcha verification using a WebView. @@ -52,6 +54,7 @@ fun CaptchaScreen( Column( modifier = modifier + .testTag(TestTags.CAPTCHA_SCREEN) .fillMaxSize() .windowInsetsPadding(WindowInsets.safeDrawing) ) { @@ -133,6 +136,7 @@ fun CaptchaScreen( onClick = { onEvent(CaptchaScreenEvents.Cancel) }, modifier = Modifier .align(Alignment.CenterHorizontally) + .testTag(TestTags.CAPTCHA_CANCEL_BUTTON) .padding(16.dp) ) { Text(stringResource(R.string.CaptchaScreen__cancel)) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerScreen.kt index cbf3efda6d..f2a144b0e6 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryCodePickerScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle @@ -66,6 +67,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags /** * Screen that allows someone to search and select a country code from a supported list of countries. @@ -92,7 +94,9 @@ private fun OnePaneLayout( val topBarScrollBehavior = RegistrationScaffold.rememberTopBarScrollBehavior() OnePaneRegistrationScaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.COUNTRY_CODE_PICKER_SCREEN), params = layoutParams, topBar = { TopAppBar( @@ -128,7 +132,9 @@ private fun TwoPaneLayout( val topBarScrollBehavior = RegistrationScaffold.rememberTopBarScrollBehavior() TwoPaneRegistrationScaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.COUNTRY_CODE_PICKER_SCREEN), params = params, topBar = { TopAppBar( @@ -176,9 +182,19 @@ fun TopAppBar( Scaffolds.DefaultTopAppBar( title = "", titleContent = { _, _ -> }, - onNavigationClick = onCloseClick, - navigationIcon = SignalIcons.X.imageVector, - navigationContentDescription = stringResource(R.string.CountryCodeSelectScreen__close), + navigationIconContent = { + IconButton( + onClick = onCloseClick, + modifier = Modifier + .padding(end = 16.dp) + .testTag(TestTags.COUNTRY_CODE_CLOSE_BUTTON) + ) { + Icon( + imageVector = SignalIcons.X.imageVector, + contentDescription = stringResource(R.string.CountryCodeSelectScreen__close) + ) + } + }, scrollBehavior = scrollBehavior ) } @@ -393,7 +409,8 @@ private fun SearchBar( .background(MaterialTheme.colorScheme.background) .fillMaxWidth() .defaultMinSize(minHeight = 54.dp) - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .testTag(TestTags.COUNTRY_CODE_SEARCH_FIELD), visualTransformation = VisualTransformation.None, colors = TextFieldDefaults.colors( // TODO move to SignalTheme diff --git a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt index 974c9713d5..9a36a791e7 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -52,6 +53,7 @@ import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.rememberWindowBreakpoint import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags /** * Profile creation screen for the registration flow. Captures the user's given name, family name, @@ -110,7 +112,9 @@ private fun CompactLayout( modifier: Modifier = Modifier ) { RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.CREATE_PROFILE_SCREEN), content = { Column( modifier = Modifier @@ -153,7 +157,9 @@ private fun CompactLayout( capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Next ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD) ) Spacer(modifier = Modifier.height(16.dp)) @@ -168,7 +174,9 @@ private fun CompactLayout( capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Done ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.CREATE_PROFILE_FAMILY_NAME_FIELD) ) Spacer(modifier = Modifier.height(16.dp)) @@ -193,6 +201,7 @@ private fun CompactLayout( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.CREATE_PROFILE_NEXT_BUTTON) ) { if (state.isSubmitting) { CircularProgressIndicator( @@ -241,7 +250,8 @@ private fun WhoCanFindMeRow( modifier = Modifier .fillMaxWidth() .clickable(enabled = enabled, onClick = onClick) - .padding(vertical = 12.dp), + .padding(vertical = 12.dp) + .testTag(TestTags.CREATE_PROFILE_WHO_CAN_FIND_ME_ROW), verticalAlignment = Alignment.CenterVertically ) { Icon( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreen.kt index a5a676b3fe..b0b3e596a8 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -30,6 +31,7 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags @Composable fun DeviceTransferCompleteScreen( @@ -40,7 +42,9 @@ fun DeviceTransferCompleteScreen( BackHandler(enabled = true) { /* no-op: the transfer is done, don't let the user back out */ } RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.DEVICE_TRANSFER_COMPLETE_SCREEN), content = { Column( modifier = Modifier @@ -87,6 +91,7 @@ fun DeviceTransferCompleteScreen( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_COMPLETE_CONTINUE_BUTTON) ) { Text(stringResource(R.string.DeviceTransferComplete__continue_registration)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreen.kt index 0e51b6196c..0d3fd9ea24 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -29,6 +30,7 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags @Composable fun DeviceTransferInstructionsScreen( @@ -37,7 +39,9 @@ fun DeviceTransferInstructionsScreen( modifier: Modifier = Modifier ) { RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.DEVICE_TRANSFER_INSTRUCTIONS_SCREEN), content = { Column( modifier = Modifier @@ -84,6 +88,7 @@ fun DeviceTransferInstructionsScreen( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_INSTRUCTIONS_CONTINUE_BUTTON) ) { Text(stringResource(R.string.DeviceTransferInstructions__continue)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreen.kt index 5bb69af315..b3563dc162 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -30,6 +31,7 @@ import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags @Composable fun DeviceTransferProgressScreen( @@ -55,7 +57,9 @@ fun DeviceTransferProgressScreen( } RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.DEVICE_TRANSFER_PROGRESS_SCREEN), content = { Column( modifier = Modifier @@ -127,7 +131,9 @@ fun DeviceTransferProgressScreen( if (state.status == DeviceTransferProgressState.Status.FAILED) { Buttons.LargeTonal( onClick = { onEvent(DeviceTransferProgressScreenEvents.TryAgainClicked) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.DEVICE_TRANSFER_PROGRESS_TRY_AGAIN_BUTTON) ) { Text(stringResource(R.string.DeviceTransferProgress__try_again)) } @@ -136,7 +142,9 @@ fun DeviceTransferProgressScreen( Buttons.LargeTonal( onClick = { onEvent(DeviceTransferProgressScreenEvents.CancelClicked) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.DEVICE_TRANSFER_PROGRESS_CANCEL_BUTTON) ) { Text(stringResource(R.string.DeviceTransferProgress__cancel)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreen.kt index 20734a087c..4c28781ea8 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -47,6 +48,7 @@ import org.signal.core.ui.compose.Previews import org.signal.devicetransfer.WifiDirect import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags import java.util.Locale @Composable @@ -129,7 +131,9 @@ internal fun DeviceTransferSetupScreen( } RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.DEVICE_TRANSFER_SETUP_SCREEN), content = { Column( modifier = Modifier @@ -245,6 +249,7 @@ private fun VerifyStep( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_MATCH_BUTTON) ) { Text(stringResource(R.string.DeviceTransferSetup__numbers_match)) } @@ -254,6 +259,7 @@ private fun VerifyStep( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_DO_NOT_MATCH_BUTTON) ) { Text(stringResource(R.string.DeviceTransferSetup__numbers_do_not_match)) } @@ -277,6 +283,7 @@ private fun ErrorStep( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_SETUP_ERROR_ACTION_BUTTON) ) { Text(buttonText) } @@ -304,7 +311,9 @@ private fun TroubleshootingStep(onTryAgain: () -> Unit) { ) { Buttons.LargeTonal( onClick = onTryAgain, - modifier = Modifier.widthIn(max = 320.dp) + modifier = Modifier + .widthIn(max = 320.dp) + .testTag(TestTags.DEVICE_TRANSFER_SETUP_TROUBLESHOOTING_RETRY_BUTTON) ) { Text(stringResource(R.string.DeviceTransferSetup__try_again)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreen.kt index ade79c394e..d83f694e87 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import org.signal.core.ui.compose.AllDevicePreviews @@ -34,6 +35,7 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.test.TestTags @Composable fun PhoneNumberDiscoverabilityScreen( @@ -42,13 +44,18 @@ fun PhoneNumberDiscoverabilityScreen( modifier: Modifier = Modifier ) { RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SCREEN), topBar = { Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.BackClicked) }) { + IconButton( + onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.BackClicked) }, + modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON) + ) { Icon( imageVector = SignalIcons.ArrowStart.imageVector, contentDescription = stringResource(R.string.PhoneNumberDiscoverabilityScreen__back) @@ -70,13 +77,15 @@ fun PhoneNumberDiscoverabilityScreen( DiscoverabilityOption( label = stringResource(R.string.PhoneNumberPrivacy_everyone), selected = state.discoverable, - onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected) } + onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected) }, + modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION) ) DiscoverabilityOption( label = stringResource(R.string.PhoneNumberPrivacy_nobody), selected = !state.discoverable, - onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodySelected) } + onClick = { onEvent(PhoneNumberDiscoverabilityScreenEvents.NobodySelected) }, + modifier = Modifier.testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION) ) Spacer(modifier = Modifier.height(8.dp)) @@ -107,6 +116,7 @@ fun PhoneNumberDiscoverabilityScreen( modifier = Modifier .fillMaxWidth() .widthIn(max = 320.dp) + .testTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON) ) { Text(stringResource(R.string.PhoneNumberDiscoverabilityScreen__save)) } @@ -137,10 +147,11 @@ fun PhoneNumberDiscoverabilityScreen( private fun DiscoverabilityOption( label: String, selected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, + modifier: Modifier = Modifier ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), diff --git a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt index a0b8efeab3..74621c9b62 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontFamily @@ -54,6 +55,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags private const val PASSPHRASE_LENGTH = 30 private const val CHUNK_SIZE = 5 @@ -106,7 +108,9 @@ private fun OnePaneLayout( ) { val scrollState = rememberScrollState() OnePaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_SCREEN), params = params, content = { paddingValues -> Column( @@ -154,7 +158,9 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_SCREEN), params = params, firstPane = { paddingValues -> Column( @@ -265,6 +271,7 @@ private fun PassphraseTextField( visualTransformation = visualTransform, modifier = Modifier .fillMaxWidth() + .testTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_INPUT) .focusRequester(focusRequester) .onGloballyPositioned { if (requestFocus) { @@ -293,7 +300,9 @@ private fun FooterButtons( .padding(horizontal = 24.dp, vertical = 16.dp) ) { TextButton( - modifier = Modifier.weight(weight = 1f, fill = false), + modifier = Modifier + .weight(weight = 1f, fill = false) + .testTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NO_PASSPHRASE_BUTTON), onClick = onCancel, shape = RoundedCornerShape(0.dp) ) { @@ -304,7 +313,8 @@ private fun FooterButtons( Buttons.LargeTonal( enabled = isValid, - onClick = { onSubmit(passphrase) } + onClick = { onSubmit(passphrase) }, + modifier = Modifier.testTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON) ) { Text(text = stringResource(R.string.LocalBackupRestoreScreen__next)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt index a1a0088cab..0d6d4725c5 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle @@ -74,6 +75,7 @@ import org.signal.registration.screens.PinVisualTransformation import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags import org.signal.core.ui.R as CoreR private const val STEP_TRANSITION_DURATION = 250 @@ -130,7 +132,9 @@ private fun OnePaneLayout( val topBarScrollBehavior = RegistrationScaffold.rememberTopBarScrollBehavior() RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PIN_CREATION_SCREEN), topBar = { PinCreationTopBar( scrollBehavior = topBarScrollBehavior, @@ -191,7 +195,9 @@ private fun TwoPaneLayout( val topBarScrollBehavior = RegistrationScaffold.rememberTopBarScrollBehavior() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PIN_CREATION_SCREEN), params = params, topBar = { PinCreationTopBar( @@ -391,7 +397,9 @@ private fun PinInputField( TextField( value = pin, onValueChange = onPinChanged, - modifier = modifier.focusRequester(focusRequester), + modifier = modifier + .testTag(TestTags.PIN_CREATION_INPUT) + .focusRequester(focusRequester), textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center), singleLine = true, keyboardOptions = KeyboardOptions( @@ -432,7 +440,9 @@ private fun KeyboardToggleButton( ) { TextButton( onClick = onToggleKeyboard, - modifier = modifier.fillMaxWidth() + modifier = modifier + .fillMaxWidth() + .testTag(TestTags.PIN_CREATION_TOGGLE_KEYBOARD_BUTTON) ) { Icon( painter = SignalIcons.Keyboard.painter, @@ -537,6 +547,7 @@ private fun NextButton( modifier = Modifier .widthIn(max = params.maxButtonWidth) .padding(params.footerPadding) + .testTag(TestTags.PIN_CREATION_NEXT_BUTTON) ) { if (loading) { CircularProgressIndicator( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt index f49ea99663..beca8810a0 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -58,6 +59,7 @@ import org.signal.registration.screens.PinVisualTransformation import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags /** * PIN entry screen for the registration flow. @@ -160,7 +162,9 @@ private fun OnePaneLayout( val scrollState = rememberScrollState() RegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PIN_ENTRY_SCREEN), content = { Box( modifier = modifier.fillMaxSize() @@ -233,7 +237,9 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PIN_ENTRY_SCREEN), params = params, firstPane = { paddingValues -> Column( @@ -346,6 +352,7 @@ private fun PinInputField( onValueChange = onPinChanged, modifier = Modifier .fillMaxWidth() + .testTag(TestTags.PIN_ENTRY_INPUT) .focusRequester(focusRequester), textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center), singleLine = true, @@ -403,7 +410,9 @@ private fun KeyboardToggleButton( ) { TextButton( onClick = onToggleKeyboard, - modifier = modifier.fillMaxWidth() + modifier = modifier + .fillMaxWidth() + .testTag(TestTags.PIN_ENTRY_TOGGLE_KEYBOARD_BUTTON) ) { Icon( painter = SignalIcons.Keyboard.painter, @@ -437,6 +446,7 @@ private fun ContinueButton( modifier = Modifier .widthIn(max = params.maxButtonWidth) .padding(params.footerPadding) + .testTag(TestTags.PIN_ENTRY_CONTINUE_BUTTON) ) { if (loading) { CircularProgressIndicator( @@ -459,7 +469,7 @@ private fun SkipButton( ) { TextButton( onClick = onSkip, - modifier = modifier + modifier = modifier.testTag(TestTags.PIN_ENTRY_SKIP_BUTTON) ) { Text( text = stringResource(R.string.PinEntryScreen__skip), diff --git a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt index 9f499cbd57..fa1f07bca7 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign @@ -63,6 +64,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags import org.signal.core.ui.R as CoreR /** @@ -92,7 +94,9 @@ private fun OnePaneLayout( ) { val scrollState = rememberScrollState() OnePaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.QUICK_RESTORE_QR_SCREEN), params = params, topBar = { TopAppBar() }, content = { paddingValues -> @@ -131,7 +135,9 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag(TestTags.QUICK_RESTORE_QR_SCREEN), params = params, topBar = { TopAppBar() }, firstPane = { paddingValues -> @@ -276,7 +282,10 @@ private fun QrCodePane( Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { + Button( + onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }, + modifier = Modifier.testTag(TestTags.QUICK_RESTORE_QR_RETRY_BUTTON) + ) { Text(stringResource(R.string.QuickRestoreQRScreen__retry)) } } @@ -296,7 +305,10 @@ private fun QrCodePane( Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { + Button( + onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }, + modifier = Modifier.testTag(TestTags.QUICK_RESTORE_QR_RETRY_BUTTON) + ) { Text(stringResource(R.string.QuickRestoreQRScreen__retry)) } } @@ -338,7 +350,8 @@ private fun CancelFooter(onEvent: (QuickRestoreQrEvents) -> Unit) { horizontalArrangement = Arrangement.Center ) { TextButton( - onClick = { onEvent(QuickRestoreQrEvents.Cancel) } + onClick = { onEvent(QuickRestoreQrEvents.Cancel) }, + modifier = Modifier.testTag(TestTags.QUICK_RESTORE_QR_CANCEL_BUTTON) ) { Text(stringResource(android.R.string.cancel)) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt index 4e6f75b1cd..e2086ef58f 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -51,6 +52,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold import org.signal.registration.screens.TwoPaneRegistrationScaffold import org.signal.registration.screens.attachDebugLogHelper +import org.signal.registration.test.TestTags import java.util.Date @Composable @@ -107,7 +109,7 @@ private fun OnePaneLayout( val scrollState = rememberScrollState() OnePaneRegistrationScaffold( - modifier = modifier, + modifier = modifier.testTag(TestTags.REMOTE_BACKUP_RESTORE_SCREEN), params = params, content = { paddingValues -> Column( @@ -150,7 +152,7 @@ private fun TwoPaneLayout( val secondPaneScrollState = rememberScrollState() TwoPaneRegistrationScaffold( - modifier = modifier, + modifier = modifier.testTag(TestTags.REMOTE_BACKUP_RESTORE_SCREEN), params = params, firstPane = { paddingValues -> Column( @@ -273,7 +275,7 @@ private fun RestoreButton( ) { Buttons.LargeTonal( onClick = { onEvent(RemoteBackupRestoreScreenEvents.BackupRestoreBackup) }, - modifier = modifier + modifier = modifier.testTag(TestTags.REMOTE_BACKUP_RESTORE_RESTORE_BUTTON) ) { Text(text = stringResource(R.string.RemoteRestoreScreen__restore_backup)) } @@ -286,7 +288,7 @@ private fun CancelButton( ) { TextButton( onClick = { onEvent(RemoteBackupRestoreScreenEvents.Cancel) }, - modifier = modifier + modifier = modifier.testTag(TestTags.REMOTE_BACKUP_RESTORE_CANCEL_BUTTON) ) { Text(text = stringResource(android.R.string.cancel)) } diff --git a/feature/registration/src/main/java/org/signal/registration/test/TestTags.kt b/feature/registration/src/main/java/org/signal/registration/test/TestTags.kt index 72e59ab58e..e8ed30b747 100644 --- a/feature/registration/src/main/java/org/signal/registration/test/TestTags.kt +++ b/feature/registration/src/main/java/org/signal/registration/test/TestTags.kt @@ -76,4 +76,87 @@ object TestTags { const val LOCAL_BACKUP_RESTORE_RESTORE_BUTTON = "local_backup_restore_restore_button" const val LOCAL_BACKUP_RESTORE_PROGRESS_BAR = "local_backup_restore_progress_bar" const val LOCAL_BACKUP_RESTORE_CONTINUE_BUTTON = "local_backup_restore_continue_button" + + // Account Locked Screen + const val ACCOUNT_LOCKED_SCREEN = "account_locked_screen" + const val ACCOUNT_LOCKED_NEXT_BUTTON = "account_locked_next_button" + const val ACCOUNT_LOCKED_LEARN_MORE_BUTTON = "account_locked_learn_more_button" + + // Enter AEP Screen + const val ENTER_AEP_SCREEN = "enter_aep_screen" + const val ENTER_AEP_INPUT = "enter_aep_input" + const val ENTER_AEP_NEXT_BUTTON = "enter_aep_next_button" + const val ENTER_AEP_NO_KEY_BUTTON = "enter_aep_no_key_button" + + // Captcha Screen + const val CAPTCHA_SCREEN = "captcha_screen" + const val CAPTCHA_CANCEL_BUTTON = "captcha_cancel_button" + + // Country Code Picker Screen + const val COUNTRY_CODE_PICKER_SCREEN = "country_code_picker_screen" + const val COUNTRY_CODE_SEARCH_FIELD = "country_code_search_field" + const val COUNTRY_CODE_CLOSE_BUTTON = "country_code_close_button" + + // Create Profile Screen + const val CREATE_PROFILE_SCREEN = "create_profile_screen" + const val CREATE_PROFILE_GIVEN_NAME_FIELD = "create_profile_given_name_field" + const val CREATE_PROFILE_FAMILY_NAME_FIELD = "create_profile_family_name_field" + const val CREATE_PROFILE_WHO_CAN_FIND_ME_ROW = "create_profile_who_can_find_me_row" + const val CREATE_PROFILE_NEXT_BUTTON = "create_profile_next_button" + + // Phone Number Discoverability Screen + const val PHONE_NUMBER_DISCOVERABILITY_SCREEN = "phone_number_discoverability_screen" + const val PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION = "phone_number_discoverability_everyone_option" + const val PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION = "phone_number_discoverability_nobody_option" + const val PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON = "phone_number_discoverability_save_button" + const val PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON = "phone_number_discoverability_back_button" + + // Device Transfer Complete Screen + const val DEVICE_TRANSFER_COMPLETE_SCREEN = "device_transfer_complete_screen" + const val DEVICE_TRANSFER_COMPLETE_CONTINUE_BUTTON = "device_transfer_complete_continue_button" + + // Device Transfer Instructions Screen + const val DEVICE_TRANSFER_INSTRUCTIONS_SCREEN = "device_transfer_instructions_screen" + const val DEVICE_TRANSFER_INSTRUCTIONS_CONTINUE_BUTTON = "device_transfer_instructions_continue_button" + + // Device Transfer Progress Screen + const val DEVICE_TRANSFER_PROGRESS_SCREEN = "device_transfer_progress_screen" + const val DEVICE_TRANSFER_PROGRESS_CANCEL_BUTTON = "device_transfer_progress_cancel_button" + const val DEVICE_TRANSFER_PROGRESS_TRY_AGAIN_BUTTON = "device_transfer_progress_try_again_button" + + // Device Transfer Setup Screen + const val DEVICE_TRANSFER_SETUP_SCREEN = "device_transfer_setup_screen" + const val DEVICE_TRANSFER_SETUP_NUMBERS_MATCH_BUTTON = "device_transfer_setup_numbers_match_button" + const val DEVICE_TRANSFER_SETUP_NUMBERS_DO_NOT_MATCH_BUTTON = "device_transfer_setup_numbers_do_not_match_button" + const val DEVICE_TRANSFER_SETUP_ERROR_ACTION_BUTTON = "device_transfer_setup_error_action_button" + const val DEVICE_TRANSFER_SETUP_TROUBLESHOOTING_RETRY_BUTTON = "device_transfer_setup_troubleshooting_retry_button" + + // Enter Local Backup Passphrase Screen + const val ENTER_LOCAL_BACKUP_PASSPHRASE_SCREEN = "enter_local_backup_passphrase_screen" + const val ENTER_LOCAL_BACKUP_PASSPHRASE_INPUT = "enter_local_backup_passphrase_input" + const val ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON = "enter_local_backup_passphrase_next_button" + const val ENTER_LOCAL_BACKUP_PASSPHRASE_NO_PASSPHRASE_BUTTON = "enter_local_backup_passphrase_no_passphrase_button" + + // Pin Creation Screen + const val PIN_CREATION_SCREEN = "pin_creation_screen" + const val PIN_CREATION_INPUT = "pin_creation_input" + const val PIN_CREATION_NEXT_BUTTON = "pin_creation_next_button" + const val PIN_CREATION_TOGGLE_KEYBOARD_BUTTON = "pin_creation_toggle_keyboard_button" + + // Pin Entry Screen + const val PIN_ENTRY_SCREEN = "pin_entry_screen" + const val PIN_ENTRY_INPUT = "pin_entry_input" + const val PIN_ENTRY_CONTINUE_BUTTON = "pin_entry_continue_button" + const val PIN_ENTRY_TOGGLE_KEYBOARD_BUTTON = "pin_entry_toggle_keyboard_button" + const val PIN_ENTRY_SKIP_BUTTON = "pin_entry_skip_button" + + // Quick Restore QR Screen + const val QUICK_RESTORE_QR_SCREEN = "quick_restore_qr_screen" + const val QUICK_RESTORE_QR_RETRY_BUTTON = "quick_restore_qr_retry_button" + const val QUICK_RESTORE_QR_CANCEL_BUTTON = "quick_restore_qr_cancel_button" + + // Remote Backup Restore Screen + const val REMOTE_BACKUP_RESTORE_SCREEN = "remote_backup_restore_screen" + const val REMOTE_BACKUP_RESTORE_RESTORE_BUTTON = "remote_backup_restore_restore_button" + const val REMOTE_BACKUP_RESTORE_CANCEL_BUTTON = "remote_backup_restore_cancel_button" } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/accountlocked/AccountLockedScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/accountlocked/AccountLockedScreenTest.kt new file mode 100644 index 0000000000..2f8296ad6d --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/accountlocked/AccountLockedScreenTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.accountlocked + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for AccountLockedScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class AccountLockedScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays screen and next button`() { + // Given + composeTestRule.setContent { + SignalTheme { + AccountLockedScreen( + state = AccountLockedState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.ACCOUNT_LOCKED_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ACCOUNT_LOCKED_NEXT_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Next is clicked, Next event is emitted`() { + // Given + var emittedEvent: AccountLockedScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + AccountLockedScreen( + state = AccountLockedState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.ACCOUNT_LOCKED_NEXT_BUTTON).performClick() + + // Then + assert(emittedEvent == AccountLockedScreenEvents.Next) + } + + @Test + fun `when Learn More is clicked, LearnMore event is emitted`() { + // Given + var emittedEvent: AccountLockedScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + AccountLockedScreen( + state = AccountLockedState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.ACCOUNT_LOCKED_LEARN_MORE_BUTTON).performClick() + + // Then + assert(emittedEvent == AccountLockedScreenEvents.LearnMore) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/aepentry/EnterAepScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/aepentry/EnterAepScreenTest.kt new file mode 100644 index 0000000000..6b60435282 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/aepentry/EnterAepScreenTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.aepentry + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for EnterAepScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class EnterAepScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays screen, input, and next button`() { + // Given + composeTestRule.setContent { + SignalTheme { + EnterAepScreen( + state = EnterAepState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_INPUT).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_NEXT_BUTTON).assertIsDisplayed() + } + + @Test + fun `when No Recovery Key is clicked, Cancel event is emitted`() { + // Given + var emittedEvent: EnterAepEvents? = null + + composeTestRule.setContent { + SignalTheme { + EnterAepScreen( + state = EnterAepState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_NO_KEY_BUTTON).performClick() + + // Then + assert(emittedEvent == EnterAepEvents.Cancel) + } + + @Test + fun `Next button is disabled when backup key is not valid`() { + // Given + composeTestRule.setContent { + SignalTheme { + EnterAepScreen( + state = EnterAepState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_NEXT_BUTTON).assertIsNotEnabled() + } + + @Test + fun `when Next is clicked and backup key is valid, Submit event is emitted`() { + // Given + var emittedEvent: EnterAepEvents? = null + + composeTestRule.setContent { + SignalTheme { + EnterAepScreen( + state = EnterAepState( + isBackupKeyValid = true, + aepValidationError = null, + isRegistering = false + ), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.ENTER_AEP_NEXT_BUTTON).performClick() + + // Then + assert(emittedEvent == EnterAepEvents.Submit) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/allownotifications/AllowNotificationsScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/allownotifications/AllowNotificationsScreenTest.kt new file mode 100644 index 0000000000..62c9e263df --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/allownotifications/AllowNotificationsScreenTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.allownotifications + +import android.Manifest +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.screens.util.MockPermissionsState +import org.signal.registration.test.TestTags + +/** + * Tests for AllowNotificationsScreen that validate its callbacks. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class AllowNotificationsScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @OptIn(ExperimentalPermissionsApi::class) + @Test + fun `screen displays buttons`() { + composeTestRule.setContent { + SignalTheme { + AllowNotificationsScreen( + permissionState = MockPermissionsState(permission = Manifest.permission.POST_NOTIFICATIONS), + onProceed = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ALLOW_NOTIFICATIONS_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ALLOW_NOTIFICATIONS_NEXT_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ALLOW_NOTIFICATIONS_NOT_NOW_BUTTON).assertIsDisplayed() + } + + @OptIn(ExperimentalPermissionsApi::class) + @Test + fun `when Not Now is clicked, onProceed is invoked`() { + var proceeded = false + + composeTestRule.setContent { + SignalTheme { + AllowNotificationsScreen( + permissionState = MockPermissionsState(permission = Manifest.permission.POST_NOTIFICATIONS), + onProceed = { proceeded = true } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ALLOW_NOTIFICATIONS_NOT_NOW_BUTTON).performClick() + + assert(proceeded) + } + + @OptIn(ExperimentalPermissionsApi::class) + @Test + fun `when Next is clicked with granted permission, onProceed is invoked`() { + var proceeded = false + + composeTestRule.setContent { + SignalTheme { + AllowNotificationsScreen( + permissionState = MockPermissionsState(permission = Manifest.permission.POST_NOTIFICATIONS), + onProceed = { proceeded = true } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ALLOW_NOTIFICATIONS_NEXT_BUTTON).performClick() + + assert(proceeded) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/captcha/CaptchaScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/captcha/CaptchaScreenTest.kt new file mode 100644 index 0000000000..8b1dd113c4 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/captcha/CaptchaScreenTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.captcha + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for CaptchaScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class CaptchaScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays cancel button`() { + // Given + composeTestRule.setContent { + SignalTheme { + CaptchaScreen( + state = CaptchaState(captchaUrl = "https://example.com"), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.CAPTCHA_CANCEL_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Cancel is clicked, Cancel event is emitted`() { + // Given + var emittedEvent: CaptchaScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CaptchaScreen( + state = CaptchaState(captchaUrl = "https://example.com"), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.CAPTCHA_CANCEL_BUTTON).performClick() + + // Then + assert(emittedEvent == CaptchaScreenEvents.Cancel) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryCodePickerScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryCodePickerScreenTest.kt new file mode 100644 index 0000000000..ad98b7a604 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryCodePickerScreenTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.countrycode + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for CountryCodePickerScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class CountryCodePickerScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays search field`() { + // Given + composeTestRule.setContent { + SignalTheme { + CountryCodePickerScreen( + state = CountryCodeState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.COUNTRY_CODE_SEARCH_FIELD).assertIsDisplayed() + } + + @Test + fun `when close button is clicked, Dismissed event is emitted`() { + // Given + var emittedEvent: CountryCodePickerScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CountryCodePickerScreen( + state = CountryCodeState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.COUNTRY_CODE_CLOSE_BUTTON).performClick() + + // Then + assert(emittedEvent == CountryCodePickerScreenEvents.Dismissed) + } + + @Test + fun `when typing in search field, Search event is emitted`() { + // Given + var emittedEvent: CountryCodePickerScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CountryCodePickerScreen( + state = CountryCodeState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.COUNTRY_CODE_SEARCH_FIELD).performTextInput("US") + + // Then + assert(emittedEvent is CountryCodePickerScreenEvents.Search) + } + + @Test + fun `when a country is selected, CountrySelected event is emitted`() { + // Given + var emittedEvent: CountryCodePickerScreenEvents? = null + val country = Country("🇺🇸", "United States", 1, "US") + + composeTestRule.setContent { + SignalTheme { + CountryCodePickerScreen( + state = CountryCodeState( + countryList = listOf(country), + filteredList = listOf(country) + ), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithText("United States").performClick() + + // Then + assert(emittedEvent is CountryCodePickerScreenEvents.CountrySelected) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/createprofile/CreateProfileScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/createprofile/CreateProfileScreenTest.kt new file mode 100644 index 0000000000..2e72ceabd1 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/createprofile/CreateProfileScreenTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.createprofile + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for CreateProfileScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class CreateProfileScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays given name and family name fields`() { + // Given + composeTestRule.setContent { + SignalTheme { + CreateProfileScreen( + state = CreateProfileState(isLoading = false), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_FAMILY_NAME_FIELD).assertIsDisplayed() + } + + @Test + fun `when typing in given name field, GivenNameChanged event is emitted`() { + // Given + var emittedEvent: CreateProfileScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CreateProfileScreen( + state = CreateProfileState(isLoading = false), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_GIVEN_NAME_FIELD).performTextInput("Alice") + + // Then + assert(emittedEvent is CreateProfileScreenEvents.GivenNameChanged) + } + + @Config(qualifiers = "w360dp-h1200dp") + @Test + fun `when who can find me row is clicked, WhoCanFindMeClicked event is emitted`() { + // Given + var emittedEvent: CreateProfileScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CreateProfileScreen( + state = CreateProfileState(isLoading = false), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_WHO_CAN_FIND_ME_ROW).performClick() + + // Then + assert(emittedEvent == CreateProfileScreenEvents.WhoCanFindMeClicked) + } + + @Test + fun `when given name is blank, Next button is disabled`() { + // Given + composeTestRule.setContent { + SignalTheme { + CreateProfileScreen( + state = CreateProfileState(isLoading = false), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_NEXT_BUTTON).assertIsNotEnabled() + } + + @Test + fun `when given name is present, Next button is enabled and emits NextClicked`() { + // Given + var emittedEvent: CreateProfileScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + CreateProfileScreen( + state = CreateProfileState(givenName = "Alice", isLoading = false), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.CREATE_PROFILE_NEXT_BUTTON).assertIsEnabled().performClick() + + // Then + assert(emittedEvent == CreateProfileScreenEvents.NextClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreenTest.kt new file mode 100644 index 0000000000..685a6144c9 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteScreenTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.devicetransfer.complete + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for DeviceTransferCompleteScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class DeviceTransferCompleteScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays continue button`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferCompleteScreen( + state = DeviceTransferCompleteState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_COMPLETE_CONTINUE_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Continue is clicked, ContinueClicked event is emitted`() { + // Given + var emittedEvent: DeviceTransferCompleteScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferCompleteScreen( + state = DeviceTransferCompleteState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_COMPLETE_CONTINUE_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferCompleteScreenEvents.ContinueClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreenTest.kt new file mode 100644 index 0000000000..366f861b3c --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsScreenTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.devicetransfer.instructions + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for DeviceTransferInstructionsScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class DeviceTransferInstructionsScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays continue button`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferInstructionsScreen( + state = DeviceTransferInstructionsState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_INSTRUCTIONS_CONTINUE_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Continue is clicked, ContinueClicked event is emitted`() { + // Given + var emittedEvent: DeviceTransferInstructionsScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferInstructionsScreen( + state = DeviceTransferInstructionsState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_INSTRUCTIONS_CONTINUE_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferInstructionsScreenEvents.ContinueClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreenTest.kt new file mode 100644 index 0000000000..e52490973d --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressScreenTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.devicetransfer.progress + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for DeviceTransferProgressScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class DeviceTransferProgressScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `default state displays cancel button`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferProgressScreen( + state = DeviceTransferProgressState(), + showCancelDialog = false, + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_PROGRESS_CANCEL_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Cancel is clicked, CancelClicked event is emitted`() { + // Given + var emittedEvent: DeviceTransferProgressScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferProgressScreen( + state = DeviceTransferProgressState(), + showCancelDialog = false, + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_PROGRESS_CANCEL_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferProgressScreenEvents.CancelClicked) + } + + @Test + fun `failed state displays try again button`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferProgressScreen( + state = DeviceTransferProgressState(status = DeviceTransferProgressState.Status.FAILED), + showCancelDialog = false, + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_PROGRESS_TRY_AGAIN_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Try Again is clicked, TryAgainClicked event is emitted`() { + // Given + var emittedEvent: DeviceTransferProgressScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferProgressScreen( + state = DeviceTransferProgressState(status = DeviceTransferProgressState.Status.FAILED), + showCancelDialog = false, + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_PROGRESS_TRY_AGAIN_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferProgressScreenEvents.TryAgainClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreenTest.kt new file mode 100644 index 0000000000..75128bf8c6 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupScreenTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.devicetransfer.setup + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for DeviceTransferSetupScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class DeviceTransferSetupScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `verify step displays match and do not match buttons`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferSetupScreen( + state = DeviceTransferSetupState(step = SetupStep.VERIFY, authenticationCode = 1234567), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_MATCH_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_DO_NOT_MATCH_BUTTON).assertIsDisplayed() + } + + @Test + fun `when numbers match is clicked, UserVerifiedCode event is emitted`() { + // Given + var emittedEvent: DeviceTransferSetupScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferSetupScreen( + state = DeviceTransferSetupState(step = SetupStep.VERIFY, authenticationCode = 1234567), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_MATCH_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferSetupScreenEvents.UserVerifiedCode) + } + + @Test + fun `when numbers do not match is clicked, UserRejectedCode event is emitted`() { + // Given + var emittedEvent: DeviceTransferSetupScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferSetupScreen( + state = DeviceTransferSetupState(step = SetupStep.VERIFY, authenticationCode = 1234567), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_NUMBERS_DO_NOT_MATCH_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferSetupScreenEvents.UserRejectedCode) + } + + @Test + fun `troubleshooting step displays retry button`() { + // Given + composeTestRule.setContent { + SignalTheme { + DeviceTransferSetupScreen( + state = DeviceTransferSetupState(step = SetupStep.TROUBLESHOOTING), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_TROUBLESHOOTING_RETRY_BUTTON).assertIsDisplayed() + } + + @Test + fun `when troubleshooting retry is clicked, RetryClicked event is emitted`() { + // Given + var emittedEvent: DeviceTransferSetupScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + DeviceTransferSetupScreen( + state = DeviceTransferSetupState(step = SetupStep.TROUBLESHOOTING), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.DEVICE_TRANSFER_SETUP_TROUBLESHOOTING_RETRY_BUTTON).performClick() + + // Then + assert(emittedEvent == DeviceTransferSetupScreenEvents.RetryClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreenTest.kt new file mode 100644 index 0000000000..7d7261dfad --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/discoverability/PhoneNumberDiscoverabilityScreenTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.discoverability + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for PhoneNumberDiscoverabilityScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class PhoneNumberDiscoverabilityScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays everyone and nobody options and save button`() { + // Given + composeTestRule.setContent { + SignalTheme { + PhoneNumberDiscoverabilityScreen( + state = PhoneNumberDiscoverabilityState(), + onEvent = {} + ) + } + } + + // Then + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON).assertIsDisplayed() + } + + @Test + fun `when nobody option is clicked, NobodySelected event is emitted`() { + // Given + var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PhoneNumberDiscoverabilityScreen( + state = PhoneNumberDiscoverabilityState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_NOBODY_OPTION).performClick() + + // Then + assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.NobodySelected) + } + + @Test + fun `when everyone option is clicked, EveryoneSelected event is emitted`() { + // Given + var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PhoneNumberDiscoverabilityScreen( + state = PhoneNumberDiscoverabilityState(discoverable = false), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_EVERYONE_OPTION).performClick() + + // Then + assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.EveryoneSelected) + } + + @Test + fun `when save button is clicked, SaveClicked event is emitted`() { + // Given + var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PhoneNumberDiscoverabilityScreen( + state = PhoneNumberDiscoverabilityState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_SAVE_BUTTON).performClick() + + // Then + assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.SaveClicked) + } + + @Test + fun `when back button is clicked, BackClicked event is emitted`() { + // Given + var emittedEvent: PhoneNumberDiscoverabilityScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PhoneNumberDiscoverabilityScreen( + state = PhoneNumberDiscoverabilityState(), + onEvent = { event -> + emittedEvent = event + } + ) + } + } + + // When + composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_DISCOVERABILITY_BACK_BUTTON).performClick() + + // Then + assert(emittedEvent == PhoneNumberDiscoverabilityScreenEvents.BackClicked) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreenTest.kt new file mode 100644 index 0000000000..1cc57cc3f2 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreenTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.localbackuprestore + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for EnterLocalBackupV1PassphaseScreen that validate callback invocations. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class EnterLocalBackupV1PassphaseScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays input and next button`() { + composeTestRule.setContent { + SignalTheme { + EnterLocalBackupV1PassphaseScreen( + onSubmit = {}, + onCancel = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_INPUT).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON).assertIsDisplayed() + } + + @Test + fun `when No passphrase is clicked, onCancel is invoked`() { + var cancelled = false + + composeTestRule.setContent { + SignalTheme { + EnterLocalBackupV1PassphaseScreen( + onSubmit = {}, + onCancel = { cancelled = true } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NO_PASSPHRASE_BUTTON).performClick() + + assert(cancelled) + } + + @Test + fun `next is disabled initially`() { + composeTestRule.setContent { + SignalTheme { + EnterLocalBackupV1PassphaseScreen( + onSubmit = {}, + onCancel = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON).assertIsNotEnabled() + } + + @Test + fun `when a 30 digit passphrase is entered, next is enabled and clicking it submits`() { + val passphrase = "012345678901234567890123456789" + var submitted: String? = null + + composeTestRule.setContent { + SignalTheme { + EnterLocalBackupV1PassphaseScreen( + onSubmit = { submitted = it }, + onCancel = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_INPUT).performTextInput(passphrase) + + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON).assertIsEnabled() + composeTestRule.onNodeWithTag(TestTags.ENTER_LOCAL_BACKUP_PASSPHRASE_NEXT_BUTTON).performClick() + + assert(submitted == passphrase) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreScreenTest.kt new file mode 100644 index 0000000000..0f8e58d5bf --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/localbackuprestore/LocalBackupRestoreScreenTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.localbackuprestore + +import android.app.Application +import android.net.Uri +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags +import java.time.LocalDateTime + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class LocalBackupRestoreScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `SelectFolder phase displays screen and select folder button`() { + composeTestRule.setContent { + SignalTheme { + LocalBackupRestoreScreen( + state = LocalBackupRestoreState(restorePhase = LocalBackupRestoreState.RestorePhase.SelectFolder), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_SELECT_FOLDER_BUTTON).performScrollTo().assertIsDisplayed() + } + + @Test + fun `when select folder button is clicked, PickBackupFolder is emitted`() { + var emittedEvent: LocalBackupRestoreEvents? = null + + composeTestRule.setContent { + SignalTheme { + LocalBackupRestoreScreen( + state = LocalBackupRestoreState(restorePhase = LocalBackupRestoreState.RestorePhase.SelectFolder), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_SELECT_FOLDER_BUTTON).performScrollTo().performClick() + + assert(emittedEvent == LocalBackupRestoreEvents.PickBackupFolder) + } + + @Test + fun `BackupFound phase displays restore button and clicking it emits RestoreBackup`() { + var emittedEvent: LocalBackupRestoreEvents? = null + + composeTestRule.setContent { + SignalTheme { + LocalBackupRestoreScreen( + state = LocalBackupRestoreState( + restorePhase = LocalBackupRestoreState.RestorePhase.BackupFound, + backupInfo = LocalBackupInfo( + type = LocalBackupInfo.BackupType.V2, + date = LocalDateTime.of(2026, 3, 15, 14, 30, 0), + name = "signal-backup-2026-03-15-14-30-00", + uri = Uri.EMPTY, + sizeBytes = 1_482_184_499 + ) + ), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_RESTORE_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_RESTORE_BUTTON).performClick() + + assert(emittedEvent == LocalBackupRestoreEvents.RestoreBackup) + } + + @Test + fun `InProgress phase displays progress bar`() { + composeTestRule.setContent { + SignalTheme { + LocalBackupRestoreScreen( + state = LocalBackupRestoreState( + restorePhase = LocalBackupRestoreState.RestorePhase.InProgress, + progressFraction = 0.5f + ), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_PROGRESS_BAR).performScrollTo().assertIsDisplayed() + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/messagesync/MessageSyncScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/messagesync/MessageSyncScreenTest.kt new file mode 100644 index 0000000000..5369348fa1 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/messagesync/MessageSyncScreenTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.messagesync + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.core.util.kibiBytes +import org.signal.core.util.mebiBytes +import org.signal.registration.test.TestTags + +/** + * Tests for MessageSyncScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class MessageSyncScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays cancel button`() { + composeTestRule.setContent { + SignalTheme { + MessageSyncScreen( + state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.MESSAGE_SYNC_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.MESSAGE_SYNC_CANCEL_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Cancel is clicked, CancelClick event is emitted`() { + var emittedEvent: MessageSyncScreenEvent? = null + + composeTestRule.setContent { + SignalTheme { + MessageSyncScreen( + state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes), + onEvent = { event -> emittedEvent = event } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.MESSAGE_SYNC_CANCEL_BUTTON).performClick() + + assert(emittedEvent == MessageSyncScreenEvent.CancelClick) + } + + @Test + fun `learn more link is displayed`() { + composeTestRule.setContent { + SignalTheme { + MessageSyncScreen( + state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.MESSAGE_SYNC_LEARN_MORE_LINK).assertIsDisplayed() + } + + @Test + fun `when finishing, Cancel button is disabled`() { + composeTestRule.setContent { + SignalTheme { + MessageSyncScreen( + state = MessageSyncScreenState(isFinishing = true), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.MESSAGE_SYNC_CANCEL_BUTTON).assertIsNotEnabled() + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/permissions/PermissionsScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/permissions/PermissionsScreenTest.kt new file mode 100644 index 0000000000..59e53c2c21 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/permissions/PermissionsScreenTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@file:OptIn(ExperimentalPermissionsApi::class) + +package org.signal.registration.screens.permissions + +import android.Manifest +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.screens.util.MockMultiplePermissionsState +import org.signal.registration.screens.util.MockPermissionsState +import org.signal.registration.test.TestTags + +/** + * Tests for PermissionsScreen that validate its callbacks. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class PermissionsScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays buttons`() { + composeTestRule.setContent { + SignalTheme { + PermissionsScreen( + permissionsState = MockMultiplePermissionsState( + permissions = listOf( + MockPermissionsState(Manifest.permission.POST_NOTIFICATIONS), + MockPermissionsState(Manifest.permission.READ_CONTACTS) + ) + ) + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PERMISSIONS_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PERMISSIONS_NEXT_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PERMISSIONS_NOT_NOW_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Not Now is clicked, onProceed is invoked`() { + var proceeded = false + + composeTestRule.setContent { + SignalTheme { + PermissionsScreen( + permissionsState = MockMultiplePermissionsState( + permissions = listOf( + MockPermissionsState(Manifest.permission.POST_NOTIFICATIONS), + MockPermissionsState(Manifest.permission.READ_CONTACTS) + ) + ), + onProceed = { proceeded = true } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PERMISSIONS_NOT_NOW_BUTTON).performClick() + + assert(proceeded) + } + + @Test + fun `when Next is clicked with all permissions granted, onProceed is invoked`() { + var proceeded = false + + composeTestRule.setContent { + SignalTheme { + PermissionsScreen( + permissionsState = MockMultiplePermissionsState( + allPermissionsGranted = true, + permissions = listOf( + MockPermissionsState(Manifest.permission.POST_NOTIFICATIONS), + MockPermissionsState(Manifest.permission.READ_CONTACTS) + ) + ), + onProceed = { proceeded = true } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PERMISSIONS_NEXT_BUTTON).performClick() + + assert(proceeded) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationScreenTest.kt new file mode 100644 index 0000000000..9a629c0530 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationScreenTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.pincreation + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for PinCreationScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class PinCreationScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays input and next button`() { + composeTestRule.setContent { + SignalTheme { + PinCreationScreen( + state = PinCreationState(), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_INPUT).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_NEXT_BUTTON).assertIsDisplayed() + } + + @Test + fun `when toggle keyboard is clicked, ToggleKeyboard event is emitted`() { + var emittedEvent: PinCreationScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PinCreationScreen( + state = PinCreationState(), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_TOGGLE_KEYBOARD_BUTTON).performClick() + + assert(emittedEvent == PinCreationScreenEvents.ToggleKeyboard) + } + + @Test + fun `next is disabled when pin is empty`() { + composeTestRule.setContent { + SignalTheme { + PinCreationScreen( + state = PinCreationState(), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_NEXT_BUTTON).assertIsNotEnabled() + } + + @Test + fun `when a pin is entered and next is clicked, PinSubmitted event is emitted`() { + var emittedEvent: PinCreationScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PinCreationScreen( + state = PinCreationState(), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_INPUT).performTextInput("1234") + composeTestRule.onNodeWithTag(TestTags.PIN_CREATION_NEXT_BUTTON).performClick() + + assert(emittedEvent is PinCreationScreenEvents.PinSubmitted) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryScreenTest.kt new file mode 100644 index 0000000000..04a3621cda --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryScreenTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.pinentry + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +/** + * Tests for PinEntryScreen that validate event emissions. + * Uses Robolectric to run fast JUnit tests without an emulator. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class PinEntryScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays input and continue button`() { + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_INPUT).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_CONTINUE_BUTTON).assertIsDisplayed() + } + + @Test + fun `when toggle keyboard is clicked, ToggleKeyboard event is emitted`() { + var emittedEvent: PinEntryScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_TOGGLE_KEYBOARD_BUTTON).performClick() + + assert(emittedEvent == PinEntryScreenEvents.ToggleKeyboard) + } + + @Test + fun `continue is disabled when pin is empty`() { + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_CONTINUE_BUTTON).assertIsNotEnabled() + } + + @Test + fun `when a pin is entered and continue is clicked, PinEntered event is emitted`() { + var emittedEvent: PinEntryScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_INPUT).performTextInput("1234") + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_CONTINUE_BUTTON).performClick() + + assert(emittedEvent is PinEntryScreenEvents.PinEntered) + } + + @Test + fun `skip button is displayed in SvrRestore mode`() { + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_SKIP_BUTTON).assertIsDisplayed() + } + + @Test + fun `when skip is confirmed in the dialog, Skip event is emitted`() { + var emittedEvent: PinEntryScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + PinEntryScreen( + state = PinEntryState(), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.PIN_ENTRY_SKIP_BUTTON).performClick() + composeTestRule.onNodeWithText("Create New PIN").performClick() + + assert(emittedEvent == PinEntryScreenEvents.Skip) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreenTest.kt new file mode 100644 index 0000000000..0783c6e2fd --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreenTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.quickrestore + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class QuickRestoreQrScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + @Test + fun `screen displays cancel button`() { + composeTestRule.setContent { + SignalTheme { + QuickRestoreQrScreen( + state = QuickRestoreQrState(qrState = QrState.Loading), + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.QUICK_RESTORE_QR_CANCEL_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Cancel is clicked, Cancel event is emitted`() { + var emittedEvent: QuickRestoreQrEvents? = null + + composeTestRule.setContent { + SignalTheme { + QuickRestoreQrScreen( + state = QuickRestoreQrState(qrState = QrState.Loading), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.QUICK_RESTORE_QR_CANCEL_BUTTON).performClick() + + assert(emittedEvent == QuickRestoreQrEvents.Cancel) + } + + @Test + fun `when qr state is Failed, Retry button is displayed and clicking it emits RetryQrCode`() { + var emittedEvent: QuickRestoreQrEvents? = null + + composeTestRule.setContent { + SignalTheme { + QuickRestoreQrScreen( + state = QuickRestoreQrState(qrState = QrState.Failed), + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.QUICK_RESTORE_QR_RETRY_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.QUICK_RESTORE_QR_RETRY_BUTTON).performClick() + + assert(emittedEvent == QuickRestoreQrEvents.RetryQrCode) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreenTest.kt new file mode 100644 index 0000000000..232a99edcb --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/remotebackuprestore/RemoteBackupRestoreScreenTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.remotebackuprestore + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.models.AccountEntropyPool +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class RemoteBackupRestoreScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + private val loadedState = RemoteBackupRestoreState( + aep = AccountEntropyPool("0000000000000000000000000000000000000000000000000000000000000000"), + loadState = RemoteBackupRestoreState.LoadState.Loaded, + backupTime = System.currentTimeMillis(), + backupSize = 1234567 + ) + + @Test + fun `screen displays restore and cancel buttons`() { + composeTestRule.setContent { + SignalTheme { + RemoteRestoreScreen( + state = loadedState, + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.REMOTE_BACKUP_RESTORE_RESTORE_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.REMOTE_BACKUP_RESTORE_CANCEL_BUTTON).assertIsDisplayed() + } + + @Test + fun `when Restore is clicked, BackupRestoreBackup event is emitted`() { + var emittedEvent: RemoteBackupRestoreScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + RemoteRestoreScreen( + state = loadedState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.REMOTE_BACKUP_RESTORE_RESTORE_BUTTON).performClick() + + assert(emittedEvent == RemoteBackupRestoreScreenEvents.BackupRestoreBackup) + } + + @Test + fun `when Cancel is clicked, Cancel event is emitted`() { + var emittedEvent: RemoteBackupRestoreScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + RemoteRestoreScreen( + state = loadedState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.REMOTE_BACKUP_RESTORE_CANCEL_BUTTON).performClick() + + assert(emittedEvent == RemoteBackupRestoreScreenEvents.Cancel) + } +} diff --git a/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionScreenTest.kt new file mode 100644 index 0000000000..8627214203 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/restoreselection/ArchiveRestoreSelectionScreenTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.registration.screens.restoreselection + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.signal.core.ui.CoreUiDependenciesRule +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.registration.test.TestTags + +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class ArchiveRestoreSelectionScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext()) + + private val allOptionsState = ArchiveRestoreSelectionState( + restoreOptions = listOf( + ArchiveRestoreOption.SignalSecureBackup, + ArchiveRestoreOption.LocalBackup, + ArchiveRestoreOption.DeviceTransfer, + ArchiveRestoreOption.None + ) + ) + + @Test + fun `screen displays all option cards`() { + composeTestRule.setContent { + SignalTheme { + ArchiveRestoreSelectionScreen( + state = allOptionsState, + onEvent = {} + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_FROM_SIGNAL_BACKUPS).performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_FROM_BACKUP_FOLDER).performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_DEVICE_TRANSFER).performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_NONE).performScrollTo().assertIsDisplayed() + } + + @Test + fun `when Signal Backups card is clicked, RestoreOptionSelected SignalSecureBackup is emitted`() { + var emittedEvent: ArchiveRestoreSelectionScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + ArchiveRestoreSelectionScreen( + state = allOptionsState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_FROM_SIGNAL_BACKUPS).performScrollTo().performClick() + + assert(emittedEvent == ArchiveRestoreSelectionScreenEvents.RestoreOptionSelected(ArchiveRestoreOption.SignalSecureBackup)) + } + + @Test + fun `when Device Transfer card is clicked, RestoreOptionSelected DeviceTransfer is emitted`() { + var emittedEvent: ArchiveRestoreSelectionScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + ArchiveRestoreSelectionScreen( + state = allOptionsState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_DEVICE_TRANSFER).performScrollTo().performClick() + + assert(emittedEvent == ArchiveRestoreSelectionScreenEvents.RestoreOptionSelected(ArchiveRestoreOption.DeviceTransfer)) + } + + @Test + fun `when Backup Folder card is clicked, RestoreOptionSelected LocalBackup is emitted`() { + var emittedEvent: ArchiveRestoreSelectionScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + ArchiveRestoreSelectionScreen( + state = allOptionsState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_FROM_BACKUP_FOLDER).performScrollTo().performClick() + + assert(emittedEvent == ArchiveRestoreSelectionScreenEvents.RestoreOptionSelected(ArchiveRestoreOption.LocalBackup)) + } + + @Test + fun `when None card is clicked, RestoreOptionSelected None is emitted`() { + var emittedEvent: ArchiveRestoreSelectionScreenEvents? = null + + composeTestRule.setContent { + SignalTheme { + ArchiveRestoreSelectionScreen( + state = allOptionsState, + onEvent = { emittedEvent = it } + ) + } + } + + composeTestRule.onNodeWithTag(TestTags.ARCHIVE_RESTORE_SELECTION_NONE).performScrollTo().performClick() + + assert(emittedEvent == ArchiveRestoreSelectionScreenEvents.RestoreOptionSelected(ArchiveRestoreOption.None)) + } +}