[#1064] Restore Seed-Birthday Screen

* [#1064] Restore Seed-Birthday Screen

- Reworked Restore Seed-Birthday height screen
- Contains UI, screen logic and tests
- Closes #1064

* Update changelog

* [#859] Fix failing Restore screenshot test

Closes #859
This commit is contained in:
Honza Rychnovský 2023-11-28 13:05:31 +01:00 committed by GitHub
parent 0054588ec0
commit 3d33bb8ca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 171 additions and 252 deletions

View File

@ -11,6 +11,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
### Changed
- Updated user interface of these screens:
- New Wallet Recovery Seed screen accessible from onboarding
- Seed Recovery screen accessible from Settings
- Restore existing wallet accessible from onboarding
- New Wallet Recovery Seed screen (accessible from onboarding)
- Seed Recovery screen (accessible from Settings)
- Restore Seed screen for an existing wallet (accessible from onboarding)
- Restore Seed Birthday Height screen for an existing wallet (accessible from onboarding)

View File

@ -11,8 +11,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Suppress("LongParameterList")
@Composable
@ -20,6 +22,7 @@ fun FormTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
textStyle: TextStyle = ZcashTheme.extendedTypography.textFieldValue,
label: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
@ -38,13 +41,16 @@ fun FormTextField(
value = value,
onValueChange = onValueChange,
label = label,
textStyle = textStyle,
keyboardOptions = keyboardOptions,
colors = colors,
modifier = if (withBorder) {
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
} else {
modifier
},
modifier = modifier.then(
if (withBorder) {
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
} else {
Modifier
}
),
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
keyboardActions = keyboardActions,

View File

@ -124,6 +124,7 @@ data class ExtendedTypography(
val securityWarningText: TextStyle,
val textFieldHint: TextStyle,
val textFieldValue: TextStyle,
val textFieldBirthday: TextStyle,
)
@Suppress("CompositionLocalAllowlist")
@ -171,5 +172,6 @@ val LocalExtendedTypography = staticCompositionLocalOf {
textFieldValue = PrimaryTypography.bodyLarge.copy(
fontSize = 17.sp,
),
textFieldBirthday = SecondaryTypography.headlineMedium.copy(),
)
}

View File

@ -185,12 +185,16 @@ class RestoreViewTest : UiTestPrerequisites() {
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
composeTestRule.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsEnabled()
it.performClick()
}
assertEquals(testSetup.getRestoreHeight(), null)
assertEquals(testSetup.getStage(), RestoreStage.Complete)
assertEquals(1, testSetup.getOnFinishedCount())
}
@Test
@ -201,14 +205,7 @@ class RestoreViewTest : UiTestPrerequisites() {
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
}
@ -221,34 +218,7 @@ class RestoreViewTest : UiTestPrerequisites() {
}
assertEquals(testSetup.getRestoreHeight(), ZcashNetwork.Mainnet.saplingActivationHeight)
assertEquals(testSetup.getStage(), RestoreStage.Complete)
}
@Test
@MediumTest
fun height_set_valid_but_skip() {
val testSetup = newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
it.performClick()
}
assertNull(testSetup.getRestoreHeight())
assertEquals(testSetup.getStage(), RestoreStage.Complete)
assertEquals(1, testSetup.getOnFinishedCount())
}
@Test
@ -263,10 +233,10 @@ class RestoreViewTest : UiTestPrerequisites() {
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
it.assertIsEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
}
@ -275,14 +245,11 @@ class RestoreViewTest : UiTestPrerequisites() {
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
it.performClick()
}
assertNull(testSetup.getRestoreHeight())
assertEquals(testSetup.getStage(), RestoreStage.Complete)
assertEquals(0, testSetup.getOnFinishedCount())
}
@Test
@ -293,14 +260,7 @@ class RestoreViewTest : UiTestPrerequisites() {
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_hint)).also {
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput("1.2")
}
@ -309,27 +269,27 @@ class RestoreViewTest : UiTestPrerequisites() {
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_birthday_button_skip)).also {
it.performClick()
}
assertNull(testSetup.getRestoreHeight())
assertEquals(testSetup.getStage(), RestoreStage.Complete)
assertEquals(0, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun complete_click_take_to_wallet() {
val testSetup = newTestSetup(
initialStage = RestoreStage.Complete,
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
assertEquals(0, testSetup.getOnFinishedCount())
composeTestRule.onNodeWithText(getStringResource(R.string.restore_button_see_wallet), ignoreCase = true).also {
composeTestRule.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.performClick()
}
@ -375,26 +335,6 @@ class RestoreViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_from_complete() {
val testSetup = newTestSetup(
initialStage = RestoreStage.Complete,
initialWordsList = SeedPhraseFixture.new().split
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(
getStringResource(R.string.restore_back_content_description)
).also {
it.performClick()
}
assertEquals(testSetup.getStage(), RestoreStage.Birthday)
assertEquals(0, testSetup.getOnBackCount())
}
private fun newTestSetup(
initialStage: RestoreStage = RestoreStage.Seed,
initialWordsList: List<String> = emptyList()

View File

@ -4,8 +4,7 @@ enum class RestoreStage {
// Note: the ordinal order is used to manage progression through each stage
// so be careful if reordering these
Seed,
Birthday,
Complete;
Birthday;
/**
* @see getPrevious
@ -15,15 +14,15 @@ enum class RestoreStage {
/**
* @see getNext
*/
fun hasNext() = ordinal < values().size - 1
fun hasNext() = ordinal < entries.size - 1
/**
* @return Previous item in ordinal order. Returns the first item when it cannot go further back.
*/
fun getPrevious() = values()[maxOf(0, ordinal - 1)]
fun getPrevious() = entries[maxOf(0, ordinal - 1)]
/**
* @return Last item in ordinal order. Returns the last item when it cannot go further forward.
*/
fun getNext() = values()[minOf(values().size - 1, ordinal + 1)]
fun getNext() = entries[minOf(entries.size - 1, ordinal + 1)]
}

View File

@ -31,6 +31,7 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@ -71,11 +72,9 @@ import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.ChipOnSurface
import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
@ -88,9 +87,9 @@ import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentHashSetOf
import kotlinx.coroutines.launch
@Preview("Restore Wallet")
@Preview("Restore Seed")
@Composable
private fun PreviewRestore() {
private fun PreviewRestoreSeed() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
RestoreWallet(
@ -119,12 +118,31 @@ private fun PreviewRestore() {
}
}
@Preview("Restore Complete")
@Preview("Restore Seed Birthday")
@Composable
private fun PreviewRestoreComplete() {
private fun PreviewRestoreBirthday() {
ZcashTheme(forceDarkMode = false) {
RestoreComplete(
onComplete = {}
RestoreWallet(
ZcashNetwork.Mainnet,
restoreState = RestoreState(RestoreStage.Birthday),
completeWordList = persistentHashSetOf(
"abandon",
"ability",
"able",
"about",
"above",
"absent",
"absorb",
"abstract",
"rib",
"ribbon"
),
userWordList = WordList(listOf("abandon", "absorb")),
restoreHeight = null,
setRestoreHeight = {},
onBack = {},
paste = { "" },
onFinished = {}
)
}
}
@ -135,7 +153,6 @@ private fun PreviewRestoreComplete() {
* @param restoreHeight A null height indicates no user input.
*/
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RestoreWallet(
zcashNetwork: ZcashNetwork,
@ -167,20 +184,28 @@ fun RestoreWallet(
Scaffold(
modifier = Modifier.navigationBarsPadding(),
topBar = {
RestoreTopAppBar(
onBack = {
if (currentStage.hasPrevious()) {
restoreState.goPrevious()
} else {
onBack()
}
},
isShowClear = currentStage == RestoreStage.Seed,
onClear = {
userWordList.set(emptyList())
text = ""
when (currentStage) {
RestoreStage.Seed -> {
RestoreSeedTopAppBar(
onBack = onBack,
onClear = {
userWordList.set(emptyList())
text = ""
}
)
}
)
RestoreStage.Birthday -> {
RestoreSeedBirthdayTopAppBar(
onBack = {
if (currentStage.hasPrevious()) {
restoreState.goPrevious()
} else {
onBack()
}
}
)
}
}
},
bottomBar = {
when (currentStage) {
@ -198,10 +223,7 @@ fun RestoreWallet(
)
}
RestoreStage.Birthday -> {
// No content
}
RestoreStage.Complete -> {
// No content
// No content. The action button is part of scrollable content.
}
}
},
@ -231,25 +253,14 @@ fun RestoreWallet(
)
}
RestoreStage.Birthday -> {
RestoreBirthday(
RestoreBirthdayMainContent(
zcashNetwork = zcashNetwork,
initialRestoreHeight = restoreHeight,
setRestoreHeight = setRestoreHeight,
onNext = { restoreState.goNext() },
onDone = onFinished,
modifier = commonModifier
.imePadding()
.navigationBarsPadding()
.animateContentSize()
)
}
RestoreStage.Complete -> {
// In some cases we need to hide the software keyboard manually, as it stays shown after
// input on prior screens
LocalSoftwareKeyboardController.current?.hide()
RestoreComplete(
onComplete = onFinished,
modifier = commonModifier
)
}
}
@ -257,29 +268,6 @@ fun RestoreWallet(
)
}
@Composable
private fun RestoreTopAppBar(
onBack: () -> Unit,
onClear: () -> Unit,
isShowClear: Boolean,
modifier: Modifier = Modifier,
) {
SmallTopAppBar(
backText = stringResource(id = R.string.restore_back).uppercase(),
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
onBack = onBack,
regularActions = if (isShowClear) { {
ClearSeedMenuItem(
onSeedClear = onClear
)
}
} else {
null
},
modifier = modifier,
)
}
@Composable
private fun ClearSeedMenuItem(
modifier: Modifier = Modifier,
@ -295,6 +283,38 @@ private fun ClearSeedMenuItem(
)
}
@Composable
private fun RestoreSeedTopAppBar(
onBack: () -> Unit,
onClear: () -> Unit,
modifier: Modifier = Modifier,
) {
SmallTopAppBar(
backText = stringResource(id = R.string.restore_back).uppercase(),
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
onBack = onBack,
regularActions = {
ClearSeedMenuItem(
onSeedClear = onClear
)
},
modifier = modifier,
)
}
@Composable
private fun RestoreSeedBirthdayTopAppBar(
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
SmallTopAppBar(
backText = stringResource(id = R.string.restore_back).uppercase(),
backContentDescriptionText = stringResource(R.string.restore_back_content_description),
onBack = onBack,
modifier = modifier
)
}
// TODO [#672]: Implement custom seed phrase pasting for wallet import
// TODO [#672]: https://github.com/Electric-Coin-Company/zashi-android/issues/672
// TODO [#1060]: https://github.com/Electric-Coin-Company/zashi-android/issues/1060
@ -317,8 +337,6 @@ private fun RestoreSeedMainContent(
val focusRequester = remember { FocusRequester() }
val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) }
Twig.error { "TEST: $parseResult, $text" }
if (parseResult is ParseResult.Add) {
setText("")
userWordList.append(parseResult.words)
@ -388,7 +406,6 @@ private fun RestoreSeedMainContent(
DisposableEffect(parseResult) {
// Causes the TextFiled to refocus
if (!isSeedValid) {
Twig.error { "NUT" }
focusRequester.requestFocus()
}
// Causes scroll to the TextField after the first type action
@ -638,13 +655,16 @@ private fun Warn(
@Composable
@Suppress("LongMethod")
private fun RestoreBirthday(
private fun RestoreBirthdayMainContent(
zcashNetwork: ZcashNetwork,
initialRestoreHeight: BlockHeight?,
setRestoreHeight: (BlockHeight?) -> Unit,
onNext: () -> Unit,
onDone: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }
val (height, setHeight) = rememberSaveable {
mutableStateOf(initialRestoreHeight?.value?.toString() ?: "")
}
@ -652,15 +672,16 @@ private fun RestoreBirthday(
Column(
Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.verticalScroll(scrollState)
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
Header(stringResource(R.string.restore_birthday_header))
TopScreenLogoTitle(
title = stringResource(R.string.restore_birthday_header),
logoContentDescription = stringResource(R.string.zcash_logo_content_description),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Body(stringResource(R.string.restore_birthday_body))
Body(stringResource(R.string.restore_birthday_sub_header))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
@ -670,11 +691,12 @@ private fun RestoreBirthday(
val filteredHeightString = heightString.filter { it.isDigit() }
setHeight(filteredHeightString)
},
Modifier
modifier = Modifier
.fillMaxWidth()
.padding(ZcashTheme.dimens.spacingTiny)
.focusRequester(focusRequester)
.testTag(RestoreTag.BIRTHDAY_TEXT_FIELD),
label = { Text(stringResource(id = R.string.restore_birthday_hint)) },
textStyle = ZcashTheme.extendedTypography.textFieldBirthday,
keyboardOptions = KeyboardOptions(
KeyboardCapitalization.None,
autoCorrect = false,
@ -682,7 +704,7 @@ private fun RestoreBirthday(
keyboardType = KeyboardType.Number
),
keyboardActions = KeyboardActions(onAny = {}),
shape = RectangleShape,
withBorder = false,
)
Spacer(
@ -693,65 +715,33 @@ private fun RestoreBirthday(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
val isBirthdayValid = height.toLongOrNull()?.let {
// Empty birthday value is a valid birthday height too, thus run validation only in case of non-empty heights.
val isBirthdayValid = height.isEmpty() || height.toLongOrNull()?.let {
it >= zcashNetwork.saplingActivationHeight.value
} ?: false
val isEmptyBirthday = height.isEmpty()
PrimaryButton(
onClick = {
setRestoreHeight(BlockHeight.new(zcashNetwork, height.toLong()))
onNext()
if (isEmptyBirthday) {
setRestoreHeight(null)
} else if (isBirthdayValid) {
setRestoreHeight(BlockHeight.new(zcashNetwork, height.toLong()))
} else {
error("The restore button should not expect click events")
}
onDone()
},
text = stringResource(R.string.restore_birthday_button_restore),
enabled = isBirthdayValid,
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
enabled = isBirthdayValid
)
TertiaryButton(
onClick = {
setRestoreHeight(null)
onNext()
},
text = stringResource(R.string.restore_birthday_button_skip),
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingDefault)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}
@Composable
private fun RestoreComplete(
onComplete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
Header(stringResource(R.string.restore_complete_header))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Body(stringResource(R.string.restore_complete_info))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
PrimaryButton(
onClick = onComplete,
text = stringResource(R.string.restore_button_see_wallet),
outerPaddingValues = PaddingValues(top = ZcashTheme.dimens.spacingSmall)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
LaunchedEffect(Unit) {
// Causes the TextFiled to focus on the first screen visit
focusRequester.requestFocus()
}
}

View File

@ -11,14 +11,8 @@
<string name="restore_seed_warning_suggestions">This word is not in the seed phrase dictionary. Please select the correct one from the suggestions.</string>
<string name="restore_seed_warning_no_suggestions">This word is not in the seed phrase dictionary.</string>
<string name="restore_birthday_header">Do you know the wallets birthday?</string>
<string name="restore_birthday_body">This will allow a faster sync. If you dont know the wallets birthday, dont worry.</string>
<string name="restore_birthday_hint">Birthday height</string>
<string name="restore_birthday_button_restore">Restore with this birthday</string>
<string name="restore_birthday_button_skip">I dont know the birthday</string>
<string name="restore_complete_header">Seed phrase imported!</string>
<string name="restore_complete_info">We will now scan the blockchain to find your transactions and balance.</string>
<string name="restore_button_see_wallet">Take me to my wallet</string>
<string name="restore_birthday_header">Wallet birthday height</string>
<string name="restore_birthday_sub_header">(optional)</string>
<string name="restore_birthday_button_restore">Restore</string>
</resources>

View File

@ -141,11 +141,6 @@ class ScreenshotTest : UiTestPrerequisites() {
}
}
// TODO [#859]: Screenshot tests fail on Firebase Test Lab
// TODO [#859]: https://github.com/Electric-Coin-Company/zashi-android/issues/859
// Some of the restore screenshots broke with the Compose 1.4 update and we don't yet know why.
private val isRestoreScreenshotsEnabled = false
@Suppress("LongMethod", "FunctionNaming", "CyclomaticComplexMethod")
private fun take_screenshots_for_restore_wallet(resContext: Context, tag: String) {
// TODO [#286]: Screenshot tests fail on Firebase Test Lab
@ -207,39 +202,31 @@ class ScreenshotTest : UiTestPrerequisites() {
SeedPhrase.SEED_PHRASE_SIZE
}
if (isRestoreScreenshotsEnabled.not()) {
return
}
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_seed_button_next)).also {
it.performScrollTo()
composeTestRule.onNodeWithText(
text = resContext.getString(R.string.restore_seed_button_next),
ignoreCase = true
).also {
// Even with waiting for the word list in the view model, there's some latency before the button is enabled
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
runCatching { it.assertIsEnabled() }.isSuccess
}
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_button_restore)).also {
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
kotlin.runCatching { it.assertExists() }.isSuccess
}
}
takeScreenshot(tag, "Import 3")
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_button_skip)).also {
it.performScrollTo()
it.performClick()
}
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_complete_header)).also {
composeTestRule.onNodeWithText(resContext.getString(R.string.restore_birthday_header)).also {
it.assertExists()
}
takeScreenshot(tag, "Import 4")
takeScreenshot(tag, "Import 3")
composeTestRule.onNodeWithText(
text = resContext.getString(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.performScrollTo()
it.performClick()
}
}
@Test