mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-23 02:02:40 +01:00
Add scaffolding for passkey creation screen.
This commit is contained in:
committed by
Cody Henthorne
parent
91441f6fca
commit
3e35f70bea
+11
@@ -308,6 +308,17 @@ class AccountSettingsScreenTest {
|
||||
assertThat(events).contains(AccountSettingsEvent.AuthenticatorAppClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASignalLogin_whenIClickPasskeys_thenIExpectPasskeysEvent() {
|
||||
setContent(createState(signalLogin = AccountSettingsState.SignalLogin(keyCount = 2, hasAuthenticatorApp = false)))
|
||||
|
||||
scrollTo(AccountSettingsTestTags.ROW_PASSKEYS)
|
||||
|
||||
composeTestRule.onNodeWithTag(AccountSettingsTestTags.ROW_PASSKEYS).performClick()
|
||||
|
||||
assertThat(events).contains(AccountSettingsEvent.PasskeysClicked)
|
||||
}
|
||||
|
||||
private fun setContent(state: AccountSettingsState) {
|
||||
composeTestRule.setContent {
|
||||
AccountSettingsScreen(
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.passkeys
|
||||
|
||||
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
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class PasskeysScreenTest {
|
||||
|
||||
companion object {
|
||||
private val PASSKEYS = listOf(
|
||||
Passkey(id = 1, name = "My Security Key", createdAt = System.currentTimeMillis()),
|
||||
Passkey(id = 2, name = "My Pixel Phone", createdAt = System.currentTimeMillis())
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val events = mutableListOf<PasskeysEvent>()
|
||||
|
||||
@Test
|
||||
fun givenNoPasskeys_whenIClickSetUpAPasskey_thenIExpectSetUpPasskeyEvent() {
|
||||
setContent(PasskeysState())
|
||||
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.BUTTON_SET_UP)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(PasskeysEvent.SetUpPasskeyClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPasskeys_whenIDisplayScreen_thenIExpectARowPerPasskey() {
|
||||
setContent(PasskeysState(passkeys = PASSKEYS))
|
||||
|
||||
for (passkey in PASSKEYS) {
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.SCROLLER).performScrollToNode(hasText(passkey.name))
|
||||
composeTestRule.onNodeWithText(passkey.name).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPasskeys_whenIClickAddANewPasskey_thenIExpectSetUpPasskeyEvent() {
|
||||
setContent(PasskeysState(passkeys = PASSKEYS))
|
||||
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.BUTTON_SET_UP)
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
assertThat(events).contains(PasskeysEvent.SetUpPasskeyClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPasskeys_whenIClickRenameInAPasskeysMenu_thenIExpectRenameEvent() {
|
||||
setContent(PasskeysState(passkeys = PASSKEYS))
|
||||
|
||||
scrollTo(PasskeysTestTags.BUTTON_PASSKEY_MENU)
|
||||
|
||||
composeTestRule.onAllNodesWithTag(PasskeysTestTags.BUTTON_PASSKEY_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.MENU_ITEM_RENAME).performClick()
|
||||
|
||||
assertThat(events).contains(PasskeysEvent.RenamePasskeyClicked(passkeyId = PASSKEYS[0].id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPasskeys_whenIClickRemoveInAPasskeysMenu_thenIExpectRemoveEvent() {
|
||||
setContent(PasskeysState(passkeys = PASSKEYS))
|
||||
|
||||
scrollTo(PasskeysTestTags.BUTTON_PASSKEY_MENU)
|
||||
|
||||
composeTestRule.onAllNodesWithTag(PasskeysTestTags.BUTTON_PASSKEY_MENU)[0].performClick()
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.MENU_ITEM_REMOVE).performClick()
|
||||
|
||||
assertThat(events).contains(PasskeysEvent.RemovePasskeyClicked(passkeyId = PASSKEYS[0].id))
|
||||
}
|
||||
|
||||
private fun setContent(state: PasskeysState) {
|
||||
composeTestRule.setContent {
|
||||
PasskeysScreen(
|
||||
state = state,
|
||||
onEvent = { events += it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollTo(testTag: String) {
|
||||
composeTestRule.onNodeWithTag(PasskeysTestTags.SCROLLER)
|
||||
.performScrollToNode(hasTestTag(testTag))
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.appsettings.passkeys
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PasskeysViewModelTest {
|
||||
|
||||
companion object {
|
||||
private val PASSKEYS = listOf(
|
||||
Passkey(id = 1, name = "My Security Key", createdAt = System.currentTimeMillis()),
|
||||
Passkey(id = 2, name = "My Pixel Phone", createdAt = System.currentTimeMillis())
|
||||
)
|
||||
}
|
||||
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
private val repository = object : PasskeysRepository {
|
||||
override fun getPasskeys(): List<Passkey> = PASSKEYS
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the passkeys are available as soon as the screen opens`() = runTest(testDispatcher) {
|
||||
val viewModel = PasskeysViewModel(repository)
|
||||
|
||||
assertThat(viewModel.state.value.passkeys).isEqualTo(PASSKEYS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SetUpPasskeyClicked launches passkey creation`() = runTest(testDispatcher) {
|
||||
val viewModel = PasskeysViewModel(repository)
|
||||
val actions = collectActions(viewModel.actions)
|
||||
|
||||
viewModel.onEvent(PasskeysEvent.SetUpPasskeyClicked)
|
||||
|
||||
assertThat(actions.last()).isEqualTo(PasskeysAction.LaunchPasskeyCreation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LearnMoreClicked opens the learn more article`() = runTest(testDispatcher) {
|
||||
val viewModel = PasskeysViewModel(repository)
|
||||
val actions = collectActions(viewModel.actions)
|
||||
|
||||
viewModel.onEvent(PasskeysEvent.LearnMoreClicked)
|
||||
|
||||
assertThat(actions.last()).isEqualTo(PasskeysAction.OpenLearnMore)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBackClicked leaves the screen`() = runTest(testDispatcher) {
|
||||
val viewModel = PasskeysViewModel(repository)
|
||||
val actions = collectActions(viewModel.actions)
|
||||
|
||||
viewModel.onEvent(PasskeysEvent.NavigateBackClicked)
|
||||
|
||||
assertThat(actions.last()).isEqualTo(PasskeysAction.NavigateBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rename and remove produce no actions yet`() = runTest(testDispatcher) {
|
||||
val viewModel = PasskeysViewModel(repository)
|
||||
val actions = collectActions(viewModel.actions)
|
||||
|
||||
viewModel.onEvent(PasskeysEvent.RenamePasskeyClicked(passkeyId = 1))
|
||||
viewModel.onEvent(PasskeysEvent.RemovePasskeyClicked(passkeyId = 1))
|
||||
|
||||
assertThat(actions).isEmpty()
|
||||
}
|
||||
|
||||
private fun TestScope.collectActions(actions: Flow<PasskeysAction>): List<PasskeysAction> {
|
||||
val collected = mutableListOf<PasskeysAction>()
|
||||
backgroundScope.launch { actions.toList(collected) }
|
||||
return collected
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user