Add unit test coverage for regV5 screens.

This commit is contained in:
Greyson Parrelli
2026-07-02 23:18:03 -04:00
committed by Alex Hart
parent 9814c520e2
commit 5cf61665d4
36 changed files with 2454 additions and 54 deletions
@@ -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)
)
}
@@ -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) }
) {
@@ -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))
@@ -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
@@ -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(
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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))
}
@@ -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),
@@ -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))
}
@@ -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(
@@ -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),
@@ -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))
}
@@ -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))
}
@@ -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"
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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()
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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))
}
}