mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-21 17:24:38 +01:00
Add basic TOTP support with mocked creation.
This commit is contained in:
committed by
Alex Hart
parent
6a032e4f27
commit
62897a309c
+27
-7
@@ -13,6 +13,7 @@ import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
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.performScrollToNode
|
||||
import assertk.assertThat
|
||||
@@ -21,7 +22,9 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.appsettings.R
|
||||
import org.signal.appsettings.account.AccountSettingsState.Dialog
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
|
||||
@@ -29,6 +32,8 @@ import org.signal.core.ui.compose.Dialogs
|
||||
@Config(application = Application::class)
|
||||
class AccountSettingsScreenTest {
|
||||
|
||||
private val context: Application = RuntimeEnvironment.getApplication()
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@@ -294,12 +299,12 @@ class AccountSettingsScreenTest {
|
||||
setContent(createState())
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_AUTHENTICATOR_APP).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TOTP_APP).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASignalLogin_whenIClickTheSignalLoginCard_thenIExpectAccountAndRecoveryEvent() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0)))
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0)))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).performClick()
|
||||
|
||||
@@ -307,19 +312,34 @@ class AccountSettingsScreenTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASignalLogin_whenIClickAuthenticatorApp_thenIExpectAuthenticatorAppEvent() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0)))
|
||||
fun givenASignalLogin_whenIClickTotpApp_thenIExpectTotpAppEvent() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0)))
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.CARD_SIGNAL_LOGIN).assertIsDisplayed()
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_AUTHENTICATOR_APP).performClick()
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_TOTP_APP).performClick()
|
||||
|
||||
assertThat(events).contains(AccountSettingsEvent.AuthenticatorAppClicked)
|
||||
assertThat(events).contains(AccountSettingsEvent.TotpAppClicked)
|
||||
}
|
||||
|
||||
/** A count we couldn't fetch has to read the same as no count at all, rather than as "0 configured". */
|
||||
@Test
|
||||
fun givenAnUnknownTotpAppCount_whenScreenDisplayed_thenTheRowDoesNotClaimACount() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = null, passkeyCount = 0)))
|
||||
|
||||
composeTestRule.onNodeWithText(context.getString(R.string.AccountSettingsFragment__one_time_verification_codes)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenConfiguredTotpApps_whenScreenDisplayed_thenTheRowShowsTheCount() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 2, passkeyCount = 0)))
|
||||
|
||||
composeTestRule.onNodeWithText(context.resources.getQuantityString(R.plurals.AccountSettingsFragment__d_configured, 2, 2)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASignalLogin_whenIClickPasskeys_thenIExpectPasskeysEvent() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(authenticatorAppCount = 0, passkeyCount = 0)))
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(totpAppCount = 0, passkeyCount = 0)))
|
||||
|
||||
scrollTo(AccountSettingsTestTags.ROW_PASSKEYS)
|
||||
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.authenticatorapps
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
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.appsettings.authenticatorapps.AuthenticatorAppsState.Dialog
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class AuthenticatorAppsScreenTest {
|
||||
|
||||
companion object {
|
||||
private val APPS = listOf(
|
||||
AuthenticatorApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()),
|
||||
AuthenticatorApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis())
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<AuthenticatorAppsEvent>()
|
||||
|
||||
@Test
|
||||
fun givenNoApps_whenIDisplayScreen_thenIExpectTheEmptyMessage() {
|
||||
setContent(AuthenticatorAppsState())
|
||||
|
||||
scrollTo(AuthenticatorAppsTestTags.EMPTY_MESSAGE)
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.EMPTY_MESSAGE).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIDisplayScreen_thenIExpectARowPerApp() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS))
|
||||
|
||||
for (app in APPS) {
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.SCROLLER).performScrollToNode(hasText(app.name))
|
||||
composeTestRule.onNodeWithText(app.name).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickAddAuthenticatorApp_thenIExpectAddAuthenticatorAppClickedEvent() {
|
||||
setContent(AuthenticatorAppsState())
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.BUTTON_ADD)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.AddAuthenticatorAppClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIClickRenameInTheMenu_thenIExpectRenameAppClickedEvent() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS))
|
||||
|
||||
scrollTo(AuthenticatorAppsTestTags.ROW_APP)
|
||||
composeTestRule.onAllNodesWithTag(AuthenticatorAppsTestTags.BUTTON_APP_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.MENU_ITEM_RENAME).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.RenameAppClicked(appId = APPS[0].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIClickRemoveInTheMenu_thenIExpectRemoveAppClickedEvent() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS))
|
||||
|
||||
scrollTo(AuthenticatorAppsTestTags.ROW_APP)
|
||||
composeTestRule.onAllNodesWithTag(AuthenticatorAppsTestTags.BUTTON_APP_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.MENU_ITEM_REMOVE).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.RemoveAppClicked(appId = APPS[0].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheConfirmRemoveDialog_whenIDisplayScreen_thenIExpectTheDialog() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.ConfirmRemove(APPS[0].id)))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.DIALOG_CONFIRM_REMOVE).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheConfirmRemoveDialog_whenICancel_thenIExpectDialogDismissedEvent() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.ConfirmRemove(APPS[0].id)))
|
||||
|
||||
composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.DialogDismissed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheMaxAppsDialog_whenIDisplayScreen_thenIExpectTheDialog() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.MaxAppsReached))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.DIALOG_MAX_APPS_REACHED).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheMaxAppsDialog_whenIClickLearnMore_thenIExpectLearnMoreAndDismissEvents() {
|
||||
setContent(AuthenticatorAppsState(apps = APPS, dialog = Dialog.MaxAppsReached))
|
||||
|
||||
composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.LearnMoreClicked)
|
||||
assertThat(events).contains(AuthenticatorAppsEvent.DialogDismissed)
|
||||
}
|
||||
|
||||
private fun setContent(state: AuthenticatorAppsState) {
|
||||
composeTestRule.setContent {
|
||||
AuthenticatorAppsScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollTo(testTag: String) {
|
||||
composeTestRule.onNodeWithTag(AuthenticatorAppsTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(testTag))
|
||||
}
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.authenticatorsetup
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class AuthenticatorSetupScreenTest {
|
||||
|
||||
companion object {
|
||||
private const val SETUP_KEY = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<AuthenticatorSetupEvent>()
|
||||
|
||||
@Test
|
||||
fun whenIClickOpen_thenIExpectOpenAuthenticatorAppEvent() {
|
||||
setContent()
|
||||
|
||||
scrollTo(AuthenticatorSetupTestTags.BUTTON_OPEN)
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_OPEN).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorSetupEvent.OpenAuthenticatorAppClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickCopy_thenIExpectCopyKeyEvent() {
|
||||
setContent()
|
||||
|
||||
scrollTo(AuthenticatorSetupTestTags.BUTTON_COPY)
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_COPY).performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorSetupEvent.CopyKeyClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickContinue_thenIExpectContinueEvent() {
|
||||
setContent()
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.BUTTON_CONTINUE)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorSetupEvent.ContinueClicked)
|
||||
}
|
||||
|
||||
private fun setContent() {
|
||||
composeTestRule.setContent {
|
||||
AuthenticatorSetupScreen(
|
||||
state = AuthenticatorSetupState(setupKey = SETUP_KEY),
|
||||
onEvent = { events += it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollTo(testTag: String) {
|
||||
composeTestRule.onNodeWithTag(AuthenticatorSetupTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(testTag))
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.totpapplist
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
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.appsettings.totpapplist.TotpAppListState.Dialog
|
||||
import org.signal.appsettings.totpapplist.TotpAppListState.LoadState
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class TotpAppListScreenTest {
|
||||
|
||||
companion object {
|
||||
private val APPS = listOf(
|
||||
TotpApp(id = 1, name = "Bitwarden Authenticator", createdAt = System.currentTimeMillis()),
|
||||
TotpApp(id = 2, name = "Twilio Authy", createdAt = System.currentTimeMillis())
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<TotpAppListEvent>()
|
||||
|
||||
@Test
|
||||
fun givenNoApps_whenIDisplayScreen_thenIExpectTheEmptyMessage() {
|
||||
setContent(TotpAppListState(loadState = LoadState.LOADED))
|
||||
|
||||
scrollTo(TotpAppListTestTags.EMPTY_MESSAGE)
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIDisplayScreen_thenIExpectARowPerApp() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED))
|
||||
|
||||
for (app in APPS) {
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.SCROLLER).performScrollToNode(hasText(app.name))
|
||||
composeTestRule.onNodeWithText(app.name).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickAddTotpApp_thenIExpectAddTotpAppClickedEvent() {
|
||||
setContent(TotpAppListState(loadState = LoadState.LOADED))
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.BUTTON_ADD)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.AddTotpAppClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIClickRenameInTheMenu_thenIExpectRenameAppClickedEvent() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED))
|
||||
|
||||
scrollTo(TotpAppListTestTags.ROW_APP)
|
||||
composeTestRule.onAllNodesWithTag(TotpAppListTestTags.BUTTON_APP_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.MENU_ITEM_RENAME).performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.RenameAppClicked(appId = APPS[0].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenApps_whenIClickRemoveInTheMenu_thenIExpectRemoveAppClickedEvent() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED))
|
||||
|
||||
scrollTo(TotpAppListTestTags.ROW_APP)
|
||||
composeTestRule.onAllNodesWithTag(TotpAppListTestTags.BUTTON_APP_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.MENU_ITEM_REMOVE).performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.RemoveAppClicked(appId = APPS[0].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheConfirmRemoveDialog_whenIDisplayScreen_thenIExpectTheDialog() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[0].id)))
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.DIALOG_CONFIRM_REMOVE).assertIsDisplayed()
|
||||
}
|
||||
|
||||
/** The event has to carry the id, since the dialog holding it is dismissed before the confirmation is reported. */
|
||||
@Test
|
||||
fun givenTheConfirmRemoveDialog_whenIConfirm_thenIExpectRemoveAppConfirmedForThatApp() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[1].id)))
|
||||
|
||||
composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.RemoveAppConfirmed(APPS[1].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheConfirmRemoveDialog_whenICancel_thenIExpectDialogDismissedEvent() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.ConfirmRemove(APPS[0].id)))
|
||||
|
||||
composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.DialogDismissed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheMaxAppsDialog_whenIDisplayScreen_thenIExpectTheDialog() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.MaxAppsReached))
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.DIALOG_MAX_APPS_REACHED).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTheMaxAppsDialog_whenIClickLearnMore_thenIExpectLearnMoreAndDismissEvents() {
|
||||
setContent(TotpAppListState(apps = APPS, loadState = LoadState.LOADED, dialog = Dialog.MaxAppsReached))
|
||||
|
||||
composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_DISMISS_BUTTON).performClick()
|
||||
|
||||
assertThat(events).contains(TotpAppListEvent.LearnMoreClicked)
|
||||
assertThat(events).contains(TotpAppListEvent.DialogDismissed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTheListHasntArrived_thenIExpectASpinnerRatherThanNoTotpApps() {
|
||||
setContent(TotpAppListState(loadState = LoadState.LOADING))
|
||||
|
||||
scrollTo(TotpAppListTestTags.LOADING)
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.LOADING).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertDoesNotExist()
|
||||
}
|
||||
|
||||
/** An account we couldn't ask about is not an account with no authenticator apps. */
|
||||
@Test
|
||||
fun givenTheListCouldntBeLoaded_whenIDisplayScreen_thenIExpectTheFailureMessageRatherThanTheEmptyOne() {
|
||||
setContent(TotpAppListState(loadState = LoadState.NETWORK_FAILURE))
|
||||
|
||||
scrollTo(TotpAppListTestTags.LOAD_FAILED_MESSAGE)
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.LOAD_FAILED_MESSAGE).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.EMPTY_MESSAGE).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.LOADING).assertDoesNotExist()
|
||||
}
|
||||
|
||||
private fun setContent(state: TotpAppListState) {
|
||||
composeTestRule.setContent {
|
||||
TotpAppListScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollTo(testTag: String) {
|
||||
composeTestRule.onNodeWithTag(TotpAppListTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(testTag))
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.authenticatorcodeentry
|
||||
package org.signal.appsettings.totpcodeentry
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
@@ -22,43 +22,43 @@ import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class AuthenticatorCodeEntryScreenTest {
|
||||
class TotpCodeEntryScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<AuthenticatorCodeEntryEvent>()
|
||||
private val events = mutableListOf<TotpCodeEntryEvent>()
|
||||
|
||||
@Test
|
||||
fun givenAPartialCode_whenScreenDisplayed_thenDoneIsDisabled() {
|
||||
setContent(AuthenticatorCodeEntryState(code = "123"))
|
||||
setContent(TotpCodeEntryState(code = "123"))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.BUTTON_DONE).assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.BUTTON_DONE).assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAFullCode_whenIClickDone_thenIExpectDoneEvent() {
|
||||
setContent(AuthenticatorCodeEntryState(code = "123456"))
|
||||
setContent(TotpCodeEntryState(code = "123456"))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.BUTTON_DONE)
|
||||
composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.BUTTON_DONE)
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorCodeEntryEvent.DoneClicked)
|
||||
assertThat(events).contains(TotpCodeEntryEvent.DoneClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenITypeInTheCodeField_thenIExpectCodeChangedEvent() {
|
||||
setContent(AuthenticatorCodeEntryState())
|
||||
setContent(TotpCodeEntryState())
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorCodeEntryTestTags.CODE_INPUT).performTextInput("123456")
|
||||
composeTestRule.onNodeWithTag(TotpCodeEntryTestTags.CODE_INPUT).performTextInput("123456")
|
||||
|
||||
assertThat(events).contains(AuthenticatorCodeEntryEvent.CodeChanged("123456"))
|
||||
assertThat(events).contains(TotpCodeEntryEvent.CodeChanged("123456"))
|
||||
}
|
||||
|
||||
private fun setContent(state: AuthenticatorCodeEntryState) {
|
||||
private fun setContent(state: TotpCodeEntryState) {
|
||||
composeTestRule.setContent {
|
||||
AuthenticatorCodeEntryScreen(
|
||||
TotpCodeEntryScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
+15
-15
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.authenticatorname
|
||||
package org.signal.appsettings.totpnameentry
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@@ -23,52 +23,52 @@ import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class AuthenticatorNameScreenTest {
|
||||
class TotpNameEntryScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<AuthenticatorNameEvent>()
|
||||
private val events = mutableListOf<TotpNameEntryEvent>()
|
||||
|
||||
@Test
|
||||
fun whenITypeAName_thenIExpectNameChangedEvent() {
|
||||
setContent(AuthenticatorNameState())
|
||||
setContent(TotpNameEntryState())
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.NAME_INPUT)
|
||||
composeTestRule.onNodeWithTag(TotpNameEntryTestTags.NAME_INPUT)
|
||||
.assertIsDisplayed()
|
||||
.performTextReplacement("Bitwarden Authenticator")
|
||||
|
||||
assertThat(events).contains(AuthenticatorNameEvent.NameChanged("Bitwarden Authenticator"))
|
||||
assertThat(events).contains(TotpNameEntryEvent.NameChanged("Bitwarden Authenticator"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABlankName_whenIDisplayScreen_thenIExpectNextDisabled() {
|
||||
setContent(AuthenticatorNameState())
|
||||
setContent(TotpNameEntryState())
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT).assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT).assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASubmittingState_whenIDisplayScreen_thenIExpectNextDisabled() {
|
||||
setContent(AuthenticatorNameState(name = "Twilio Authy", submitting = true))
|
||||
setContent(TotpNameEntryState(name = "Twilio Authy", submitting = true))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT).assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT).assertIsNotEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAName_whenIClickNext_thenIExpectNextClickedEvent() {
|
||||
setContent(AuthenticatorNameState(name = "Twilio Authy"))
|
||||
setContent(TotpNameEntryState(name = "Twilio Authy"))
|
||||
|
||||
composeTestRule.onNodeWithTag(AuthenticatorNameTestTags.BUTTON_NEXT)
|
||||
composeTestRule.onNodeWithTag(TotpNameEntryTestTags.BUTTON_NEXT)
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(AuthenticatorNameEvent.NextClicked)
|
||||
assertThat(events).contains(TotpNameEntryEvent.NextClicked)
|
||||
}
|
||||
|
||||
private fun setContent(state: AuthenticatorNameState) {
|
||||
private fun setContent(state: TotpNameEntryState) {
|
||||
composeTestRule.setContent {
|
||||
AuthenticatorNameScreen(
|
||||
TotpNameEntryScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.totpsetup
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
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.performScrollToNode
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.isEmpty
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class TotpSetupScreenTest {
|
||||
|
||||
companion object {
|
||||
private const val SETUP_KEY = "KVZ7WL3FDDWJZMTOB7PLZPKVRFD4LYSX"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<TotpSetupEvent>()
|
||||
|
||||
@Test
|
||||
fun whenIClickOpen_thenIExpectOpenTotpAppEvent() {
|
||||
setContent()
|
||||
|
||||
scrollTo(TotpSetupTestTags.BUTTON_OPEN)
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_OPEN).performClick()
|
||||
|
||||
assertThat(events).contains(TotpSetupEvent.OpenTotpAppClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickCopy_thenIExpectCopyKeyEvent() {
|
||||
setContent()
|
||||
|
||||
scrollTo(TotpSetupTestTags.BUTTON_COPY)
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_COPY).performClick()
|
||||
|
||||
assertThat(events).contains(TotpSetupEvent.CopyKeyClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIClickContinue_thenIExpectContinueEvent() {
|
||||
setContent()
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(TotpSetupEvent.ContinueClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTheKeyHasntArrived_thenIExpectASpinnerAndNothingToClick() {
|
||||
setContent(TotpSetupState(loading = true))
|
||||
|
||||
scrollTo(TotpSetupTestTags.SETUP_KEY_SPINNER)
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.SETUP_KEY_SPINNER).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_COPY).assertIsNotEnabled()
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE).assertIsNotEnabled()
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.BUTTON_CONTINUE).performClick()
|
||||
|
||||
assertThat(events).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenTheKeyHasArrived_thenIExpectItInPlaceOfTheSpinner() {
|
||||
setContent()
|
||||
|
||||
scrollTo(TotpSetupTestTags.SETUP_KEY)
|
||||
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.SETUP_KEY).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenIDismissAFailureDialog_thenIExpectDialogDismissedEvent() {
|
||||
setContent(TotpSetupState(loading = false, dialog = TotpSetupState.Dialog.NetworkFailure))
|
||||
|
||||
composeTestRule.onNodeWithText("OK").performClick()
|
||||
|
||||
assertThat(events).contains(TotpSetupEvent.DialogDismissed)
|
||||
}
|
||||
|
||||
private fun setContent(state: TotpSetupState = TotpSetupState(setupKey = SETUP_KEY, loading = false)) {
|
||||
composeTestRule.setContent {
|
||||
TotpSetupScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollTo(testTag: String) {
|
||||
composeTestRule.onNodeWithTag(TotpSetupTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(testTag))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user