diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreen.kt
index 634fe82fab..4ffccb4cfc 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreen.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreen.kt
@@ -2,7 +2,6 @@ package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
import android.Manifest
import android.content.Intent
-import android.net.Uri
import android.provider.Settings
import android.widget.Toast
import androidx.compose.foundation.layout.Column
@@ -10,8 +9,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.text.ClickableText
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -25,12 +26,15 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.R
@@ -48,7 +52,9 @@ import com.bitwarden.authenticator.ui.platform.composition.LocalPermissionsManag
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
import com.bitwarden.ui.platform.base.util.EventsEffect
-import com.bitwarden.ui.platform.base.util.toAnnotatedString
+import com.bitwarden.ui.platform.base.util.annotatedStringResource
+import com.bitwarden.ui.platform.base.util.spanStyleOf
+import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
@@ -81,7 +87,7 @@ fun ManualCodeEntryScreen(
when (event) {
is ManualCodeEntryEvent.NavigateToAppSettings -> {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- intent.data = Uri.parse("package:" + context.packageName)
+ intent.data = "package:${context.packageName}".toUri()
intentManager.startActivity(intent = intent)
}
@@ -116,32 +122,12 @@ fun ManualCodeEntryScreen(
)
}
- when (val dialog = state.dialog) {
-
- is ManualCodeEntryState.DialogState.Error -> {
- BitwardenBasicDialog(
- visibilityState = BasicDialogState.Shown(
- title = dialog.title,
- message = dialog.message,
- ),
- onDismissRequest = remember(state) {
- { viewModel.trySendAction(ManualCodeEntryAction.DismissDialog) }
- },
- )
- }
-
- is ManualCodeEntryState.DialogState.Loading -> {
- BitwardenLoadingDialog(
- visibilityState = LoadingDialogState.Shown(
- dialog.message,
- ),
- )
- }
-
- null -> {
- Unit
- }
- }
+ ManualCodeEntryDialogs(
+ dialog = state.dialog,
+ onDismissRequest = remember(state) {
+ { viewModel.trySendAction(ManualCodeEntryAction.DismissDialog) }
+ },
+ )
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
@@ -157,92 +143,156 @@ fun ManualCodeEntryScreen(
)
},
) { paddingValues ->
- Column(modifier = Modifier.padding(paddingValues)) {
-
- Text(
- text = stringResource(id = R.string.enter_key_manually),
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(horizontal = 16.dp),
- )
-
- Spacer(modifier = Modifier.height(8.dp))
- BitwardenTextField(
- label = stringResource(id = R.string.name),
- value = state.issuer,
- onValueChange = remember(viewModel) {
- {
- viewModel.trySendAction(
- ManualCodeEntryAction.IssuerTextChange(it),
- )
+ ManualCodeEntryContent(
+ state = state,
+ onNameChange = remember(viewModel) {
+ { viewModel.trySendAction(ManualCodeEntryAction.IssuerTextChange(it)) }
+ },
+ onKeyChange = remember(viewModel) {
+ { viewModel.trySendAction(ManualCodeEntryAction.CodeTextChange(it)) }
+ },
+ onSaveLocallyClick = remember(viewModel) {
+ { viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick) }
+ },
+ onSaveToBitwardenClick = remember(viewModel) {
+ { viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick) }
+ },
+ onScanQrCodeClick = remember(viewModel) {
+ {
+ if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
+ viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
+ } else {
+ launcher.launch(Manifest.permission.CAMERA)
}
- },
- modifier = Modifier
- .semantics { testTag = "NameTextField" }
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- )
- Spacer(modifier = Modifier.height(8.dp))
- BitwardenPasswordField(
- singleLine = false,
- label = stringResource(id = R.string.key),
- value = state.code,
- onValueChange = remember(viewModel) {
- {
- viewModel.trySendAction(
- ManualCodeEntryAction.CodeTextChange(it),
- )
- }
- },
- capitalization = KeyboardCapitalization.Characters,
- modifier = Modifier
- .semantics { testTag = "KeyTextField" }
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- )
-
- Spacer(modifier = Modifier.height(16.dp))
- SaveManualCodeButtons(
- state = state.buttonState,
- onSaveLocallyClick = remember(viewModel) {
- {
- viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
- }
- },
- onSaveToBitwardenClick = remember(viewModel) {
- {
- viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick)
- }
- },
- )
-
- Text(
- text = stringResource(id = R.string.cannot_add_authenticator_key),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- vertical = 8.dp,
- horizontal = 16.dp,
- ),
- )
-
- ClickableText(
- text = stringResource(id = R.string.scan_qr_code).toAnnotatedString(),
- style = MaterialTheme.typography.bodyMedium.copy(
- color = MaterialTheme.colorScheme.primary,
- ),
- modifier = Modifier
- .padding(horizontal = 16.dp),
- onClick = remember(viewModel) {
- {
- if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) {
- viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
- } else {
- launcher.launch(Manifest.permission.CAMERA)
- }
- }
- },
- )
- }
+ }
+ },
+ modifier = Modifier.padding(paddingValues = paddingValues),
+ )
+ }
+}
+
+@Composable
+private fun ManualCodeEntryContent(
+ state: ManualCodeEntryState,
+ onNameChange: (name: String) -> Unit,
+ onKeyChange: (key: String) -> Unit,
+ onSaveLocallyClick: () -> Unit,
+ onSaveToBitwardenClick: () -> Unit,
+ onScanQrCodeClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier.verticalScroll(state = rememberScrollState())) {
+ Text(
+ text = stringResource(id = R.string.enter_key_manually),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+
+ Spacer(modifier = Modifier.height(height = 8.dp))
+ BitwardenTextField(
+ label = stringResource(id = R.string.name),
+ value = state.issuer,
+ onValueChange = onNameChange,
+ modifier = Modifier
+ .testTag(tag = "NameTextField")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 8.dp))
+ BitwardenPasswordField(
+ singleLine = false,
+ label = stringResource(id = R.string.key),
+ value = state.code,
+ onValueChange = onKeyChange,
+ capitalization = KeyboardCapitalization.Characters,
+ modifier = Modifier
+ .testTag(tag = "KeyTextField")
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+ SaveManualCodeButtons(
+ state = state.buttonState,
+ onSaveLocallyClick = onSaveLocallyClick,
+ onSaveToBitwardenClick = onSaveToBitwardenClick,
+ modifier = Modifier
+ .standardHorizontalMargin()
+ .fillMaxWidth(),
+ )
+ Spacer(modifier = Modifier.height(height = 8.dp))
+ Text(
+ text = stringResource(id = R.string.cannot_add_authenticator_key),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 8.dp))
+ ScanQrCodeText(
+ onClick = onScanQrCodeClick,
+ modifier = Modifier.standardHorizontalMargin(),
+ )
+ Spacer(modifier = Modifier.height(height = 16.dp))
+ Spacer(modifier = Modifier.navigationBarsPadding())
+ }
+}
+
+@Composable
+private fun ScanQrCodeText(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val accessibilityString = stringResource(id = R.string.scan_qr_code)
+ Text(
+ text = annotatedStringResource(
+ id = R.string.scan_qr_code,
+ emphasisHighlightStyle = spanStyleOf(
+ color = MaterialTheme.colorScheme.primary,
+ textStyle = MaterialTheme.typography.bodyMedium,
+ ),
+ onAnnotationClick = {
+ when (it) {
+ "scanQrCode" -> onClick()
+ }
+ },
+ ),
+ modifier = modifier.semantics {
+ customActions = listOf(
+ CustomAccessibilityAction(
+ label = accessibilityString,
+ action = {
+ onClick()
+ true
+ },
+ ),
+ )
+ },
+ )
+}
+
+@Composable
+private fun ManualCodeEntryDialogs(
+ dialog: ManualCodeEntryState.DialogState?,
+ onDismissRequest: () -> Unit,
+) {
+ when (val dialogString = dialog) {
+ is ManualCodeEntryState.DialogState.Error -> {
+ BitwardenBasicDialog(
+ visibilityState = BasicDialogState.Shown(
+ title = dialogString.title,
+ message = dialogString.message,
+ ),
+ onDismissRequest = onDismissRequest,
+ )
+ }
+
+ is ManualCodeEntryState.DialogState.Loading -> {
+ BitwardenLoadingDialog(visibilityState = LoadingDialogState.Shown(dialog.message))
+ }
+
+ null -> Unit
}
}
diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/SaveManualCodeButtons.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/SaveManualCodeButtons.kt
index abdfac93eb..7516b63de3 100644
--- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/SaveManualCodeButtons.kt
+++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/SaveManualCodeButtons.kt
@@ -2,13 +2,10 @@ package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.testTag
-import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
@@ -20,60 +17,50 @@ import com.bitwarden.authenticator.ui.platform.components.button.BitwardenOutlin
* @param state State of the buttons to show.
* @param onSaveLocallyClick Callback invoked when the user clicks save locally.
* @param onSaveToBitwardenClick Callback invoked when the user clicks save to Bitwarden.
+ * @param modifier The modifier to be applied to the composable.
*/
@Composable
fun SaveManualCodeButtons(
state: ManualCodeEntryState.ButtonState,
onSaveLocallyClick: () -> Unit,
onSaveToBitwardenClick: () -> Unit,
+ modifier: Modifier = Modifier,
) {
-
when (state) {
ManualCodeEntryState.ButtonState.LocalOnly -> {
BitwardenFilledTonalButton(
label = stringResource(id = R.string.add_code),
onClick = onSaveLocallyClick,
- modifier = Modifier
- .semantics { testTag = "AddCodeButton" }
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = modifier.testTag(tag = "AddCodeButton"),
)
}
ManualCodeEntryState.ButtonState.SaveLocallyPrimary -> {
- Column {
+ Column(modifier = modifier) {
BitwardenFilledButton(
label = stringResource(id = R.string.save_here),
onClick = onSaveLocallyClick,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = Modifier.fillMaxWidth(),
)
BitwardenOutlinedButton(
label = stringResource(R.string.save_to_bitwarden),
onClick = onSaveToBitwardenClick,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = Modifier.fillMaxWidth(),
)
}
}
ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary -> {
- Column {
+ Column(modifier = modifier) {
BitwardenFilledButton(
label = stringResource(id = R.string.save_to_bitwarden),
onClick = onSaveToBitwardenClick,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = Modifier.fillMaxWidth(),
)
BitwardenOutlinedButton(
label = stringResource(R.string.save_here),
onClick = onSaveLocallyClick,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
+ modifier = Modifier.fillMaxWidth(),
)
}
}
diff --git a/authenticator/src/main/res/values/strings.xml b/authenticator/src/main/res/values/strings.xml
index 6bf18030ea..f51a2cea2a 100644
--- a/authenticator/src/main/res/values/strings.xml
+++ b/authenticator/src/main/res/values/strings.xml
@@ -17,7 +17,7 @@
Add Item Rotation
Scan a QR code
Enter a setup key
- Scan QR code
+ Scan QR code
Point your camera at the QR code.
Cannot scan QR code.
Enter key manually
diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreenTest.kt
index e22cb75b4a..b8997ec485 100644
--- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreenTest.kt
+++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/manualcodeentry/ManualCodeEntryScreenTest.kt
@@ -1,12 +1,20 @@
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.isDialog
+import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
import com.bitwarden.authenticator.ui.platform.base.AuthenticatorComposeTest
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.FakePermissionManager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
+import com.bitwarden.ui.util.asText
+import com.bitwarden.ui.util.assertNoDialogExists
+import com.bitwarden.ui.util.performCustomAccessibilityAction
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@@ -14,11 +22,15 @@ import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
+ private var onNavigateBackCalled = false
+ private var onNavigateToQrCodeScreenCalled = false
+
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val mutableEventFlow = bufferedMutableSharedFlow()
@@ -28,7 +40,9 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
every { trySendAction(any()) } just runs
}
- private val intentManager: IntentManager = mockk()
+ private val intentManager: IntentManager = mockk {
+ every { startActivity(intent = any()) } just runs
+ }
private val permissionsManager = FakePermissionManager()
@Before
@@ -38,34 +52,70 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
permissionsManager = permissionsManager,
) {
ManualCodeEntryScreen(
- onNavigateBack = {},
- onNavigateToQrCodeScreen = {},
+ onNavigateBack = { onNavigateBackCalled = true },
+ onNavigateToQrCodeScreen = { onNavigateToQrCodeScreenCalled = true },
viewModel = viewModel,
)
}
}
+ @Test
+ fun `on NavigateBack should call onNavigateBack`() {
+ mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateBack)
+ assertTrue(onNavigateBackCalled)
+ }
+
+ @Test
+ fun `on NavigateToQrCodeScreen should call onNavigateToQrCodeScreen`() {
+ mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateToQrCodeScreen)
+ assertTrue(onNavigateToQrCodeScreenCalled)
+ }
+
+ @Test
+ fun `on NavigateToAppSettings should call intentManager`() {
+ mutableEventFlow.tryEmit(ManualCodeEntryEvent.NavigateToAppSettings)
+ verify(exactly = 1) {
+ intentManager.startActivity(intent = any())
+ }
+ }
+
+ @Test
+ fun `on Close click should emit `() {
+ composeTestRule
+ .onNodeWithContentDescription(label = "Close")
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(ManualCodeEntryAction.CloseClick)
+ }
+ }
+
@Test
fun `on Add code click should emit SaveLocallyClick`() {
composeTestRule
.onNodeWithText("Add code")
+ .performScrollTo()
.performClick()
- // Make sure save to bitwaren isn't showing:
+ // Make sure save to bitwarden isn't showing:
composeTestRule
.onNodeWithText("Add code to Bitwarden")
.assertDoesNotExist()
+ composeTestRule
+ .onNodeWithText(text = "Save here")
+ .assertDoesNotExist()
verify { viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick) }
}
@Test
- fun `on Add code to Bitwarden click should emit SaveToBitwardenClick`() {
+ fun `on Save here click should emit SaveToBitwardenClick`() {
mutableStateFlow.update {
it.copy(buttonState = ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary)
}
composeTestRule
.onNodeWithText("Save to Bitwarden")
+ .performScrollTo()
.performClick()
// Make sure locally only save isn't showing:
@@ -82,7 +132,7 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
}
@Test
- fun `on Add code locally click should emit SaveLocallyClick`() {
+ fun `on Save here click should emit SaveLocallyClick`() {
mutableStateFlow.update {
it.copy(buttonState = ManualCodeEntryState.ButtonState.SaveLocallyPrimary)
}
@@ -102,6 +152,120 @@ class ManualCodeEntryScreenTest : AuthenticatorComposeTest() {
verify { viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick) }
}
+
+ @Test
+ fun `on Scan QR code click with permission should emit ScanQrCodeTextClick`() {
+ permissionsManager.checkPermissionResult = true
+ composeTestRule
+ .onNodeWithText(text = "Scan QR code")
+ .performScrollTo()
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on Scan QR code click without permission and permission is not granted should display dialog`() {
+ permissionsManager.checkPermissionResult = false
+ permissionsManager.getPermissionsResult = false
+ composeTestRule
+ .onNodeWithText(text = "Scan QR code")
+ .performScrollTo()
+ .performClick()
+
+ composeTestRule
+ .onNodeWithText(text = "Enable camera permission to use the scanner")
+ .assert(hasAnyAncestor(isDialog()))
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNodeWithText(text = "No thanks")
+ .assert(hasAnyAncestor(isDialog()))
+ .performClick()
+
+ composeTestRule.assertNoDialogExists()
+
+ verify(exactly = 0) {
+ viewModel.trySendAction(any())
+ }
+ }
+
+ @Test
+ fun `on permission dialog Settings clock should emit SettingsClick`() {
+ permissionsManager.checkPermissionResult = false
+ permissionsManager.getPermissionsResult = false
+ composeTestRule
+ .onNodeWithText(text = "Scan QR code")
+ .performScrollTo()
+ .performClick()
+
+ composeTestRule
+ .onNodeWithText(text = "Settings")
+ .assert(hasAnyAncestor(isDialog()))
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(ManualCodeEntryAction.SettingsClick)
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on Scan QR code click without permission and permission is granted should emit ScanQrCodeTextClick`() {
+ permissionsManager.checkPermissionResult = false
+ permissionsManager.getPermissionsResult = true
+ composeTestRule
+ .onNodeWithText(text = "Scan QR code")
+ .performScrollTo()
+ .performClick()
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
+ }
+ }
+
+ @Suppress("MaxLineLength")
+ @Test
+ fun `on Scan QR code accessibility action without permission and permission is granted should emit ScanQrCodeTextClick`() {
+ permissionsManager.checkPermissionResult = false
+ permissionsManager.getPermissionsResult = true
+ composeTestRule
+ .onNodeWithText(text = "Scan QR code")
+ .performScrollTo()
+ .performCustomAccessibilityAction(label = "Scan QR code")
+
+ verify(exactly = 1) {
+ viewModel.trySendAction(ManualCodeEntryAction.ScanQrCodeTextClick)
+ }
+ }
+
+ @Test
+ fun `on dialog should updates according to state`() {
+ composeTestRule.assertNoDialogExists()
+ val loadingMessage = "Loading!"
+ mutableStateFlow.update {
+ it.copy(
+ dialog = ManualCodeEntryState.DialogState.Loading(
+ message = loadingMessage.asText(),
+ ),
+ )
+ }
+ composeTestRule.onNodeWithText(text = loadingMessage).assert(hasAnyAncestor(isDialog()))
+
+ val errorMessage = "Error!"
+ mutableStateFlow.update {
+ it.copy(
+ dialog = ManualCodeEntryState.DialogState.Error(message = errorMessage.asText()),
+ )
+ }
+ composeTestRule.onNodeWithText(text = errorMessage).assert(hasAnyAncestor(isDialog()))
+
+ mutableStateFlow.update { it.copy(dialog = null) }
+ composeTestRule.assertNoDialogExists()
+ }
}
private val DEFAULT_STATE = ManualCodeEntryState(