[#218] Prevent illegal number on ZEC amount

- Prevent illegal input in ZEC Send/Request form.
- New SDL-EXT-UI module for UI related SDK helper components.
- Regex for continuous validation of the entered ZEC amount.
- Added a new unit tests for validation of the regex too.
- Using the regex on ZEC Request and ZEC Send screens.
- Updated existing and created a new UI tests for validating entered ZEC amount values on Request and Send screens.
- Improve code to be validated with DetektAll static analyzation.
- Architecture documentation update with the newly added sdk-ext-ui module.
- Added run configuration sdk-ext-lib:connectedCheck for AS.
- Added check for digits count between grouping separators + tests.
- Refactoring test class name and its separator value.
This commit is contained in:
Honza Rychnovsky 2022-04-01 16:28:16 +02:00 committed by GitHub
parent 39949d8632
commit db13435d52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 542 additions and 76 deletions

View File

@ -0,0 +1,57 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sdk-ext-ui:connectedCheck" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-app.sdk-ext-ui" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@ -33,6 +33,7 @@ The logical components of the app are implemented as a number of Gradle modules.
* `preference-api-lib` — Multiplatform interfaces for key-value storage of preferences.
* `preference-impl-android-lib` — Android-specific implementation for preference storage.
* `sdk-ext-lib` — Contains extensions on top of the to the Zcash SDK. Some of these extensions might be migrated into the SDK eventually, while others might represent Android-centric idioms. Depending on how this module evolves, it could adopt another name such as `wallet-lib` or be split into two.
* `sdk-ext-ui` — Place for Zcash SDK components (same as `sdk-ext-lib`), which are related to the UI (e.g. depend on user locale and thus need to be translated via `strings.xml`).
* `spackle-lib` — Random utilities, to fill in the cracks in the Kotlin and Android frameworks.
The following diagram shows a rough depiction of dependencies between the modules. Two notes on this diagram:
@ -44,8 +45,9 @@ The following diagram shows a rough depiction of dependencies between the module
subgraph sdk
sdkLib[[sdk-lib]];
sdkExtLib[[sdk-ext-lib]];
sdkExtUI[[sdk-ext-ui]];
end
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]];
sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]] --> sdkExtUI[[sdk-ext-ui]];
subgraph preference
preference-api-lib[[preference-api-lib]];
preference-impl-android-lib[[preference-impl-android-lib]];

View File

@ -75,6 +75,7 @@ fun Zatoshi.Companion.fromZecString(zecString: String, monetarySeparators: Monet
}
val localizedPattern = "#${monetarySeparators.grouping}##0${monetarySeparators.decimal}0#"
// TODO [#321]: https://github.com/zcash/secant-android-wallet/issues/321
val decimalFormat = DecimalFormat(localizedPattern, symbols).apply {
isParseBigDecimal = true
roundingMode = RoundingMode.HALF_EVEN // aka Bankers rounding

View File

@ -0,0 +1,29 @@
plugins {
id("com.android.library")
kotlin("android")
id("zcash.android-build-conventions")
}
android {
// TODO [#6]: Figure out how to move this into the build-conventions
kotlinOptions {
jvmTarget = libs.versions.java.get()
allWarningsAsErrors = project.property("ZCASH_IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
freeCompilerArgs = freeCompilerArgs.plus("-Xopt-in=kotlin.RequiresOptIn")
}
}
dependencies {
implementation(projects.sdkExtLib)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.kotlin.test)
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
androidTestUtil(libs.androidx.test.orchestrator) {
artifact {
type = "apk"
}
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="cash.z.ecc.sdk.ext"
xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="sdk-ext-ui-test"/>
</manifest>

View File

@ -0,0 +1,101 @@
package cash.z.ecc.sdk.ext.ui.regex
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.ext.ui.R
import cash.z.ecc.sdk.ext.ui.ZecStringExt
import cash.z.ecc.sdk.ext.ui.test.getStringResourceWithArgs
import cash.z.ecc.sdk.model.MonetarySeparators
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ZecStringExtTest {
companion object {
private val EN_US_SEPARATORS = MonetarySeparators(',', '.')
}
private fun getContinuousRegex(): Regex {
return getStringResourceWithArgs(
R.string.zec_amount_regex_continuous_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
).toRegex()
}
@Test
@SmallTest
fun check_regex_validity() {
val regexString = getStringResourceWithArgs(
R.string.zec_amount_regex_continuous_filter,
arrayOf(
EN_US_SEPARATORS.grouping,
EN_US_SEPARATORS.decimal
)
)
assertNotNull(regexString)
val regexAmountChecker = regexString.toRegex()
regexAmountChecker.also {
assertNotNull(regexAmountChecker)
assertTrue(regexAmountChecker.pattern.isNotEmpty())
}
}
@Test
@SmallTest
fun check_regex_functionality_valid_inputs() {
getContinuousRegex().also {
assertTrue(it.matches(""))
assertTrue(it.matches("123"))
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("${EN_US_SEPARATORS.decimal}123"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456"))
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.decimal}456"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}"))
assertTrue(it.matches("123${EN_US_SEPARATORS.grouping}456${EN_US_SEPARATORS.decimal}789"))
assertTrue(it.matches("1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.decimal}00"))
}
}
@Test
@SmallTest
fun check_regex_functionality_invalid_inputs() {
getContinuousRegex().also {
assertFalse(it.matches("aaa"))
assertFalse(it.matches("123aaa"))
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}"))
assertFalse(it.matches("${EN_US_SEPARATORS.grouping}123"))
assertFalse(it.matches("123${EN_US_SEPARATORS.grouping}${EN_US_SEPARATORS.grouping}"))
assertFalse(it.matches("123${EN_US_SEPARATORS.decimal}${EN_US_SEPARATORS.decimal}"))
assertFalse(it.matches("1${EN_US_SEPARATORS.grouping}2${EN_US_SEPARATORS.grouping}3"))
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.decimal}3"))
assertFalse(it.matches("1${EN_US_SEPARATORS.decimal}2${EN_US_SEPARATORS.grouping}3"))
}
}
@Test
@SmallTest
fun check_digits_between_grouping_separators_valid_test() {
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "123"))
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234"))
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}"))
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}5"))
assertTrue(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}234${EN_US_SEPARATORS.grouping}567${EN_US_SEPARATORS.grouping}8"))
}
@Test
@SmallTest
fun check_digits_between_grouping_separators_invalid_test() {
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1${EN_US_SEPARATORS.grouping}2"))
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}12${EN_US_SEPARATORS.grouping}3"))
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}1234${EN_US_SEPARATORS.grouping}"))
assertFalse(ZecStringExt.checkFor3Digits(EN_US_SEPARATORS, "1${EN_US_SEPARATORS.grouping}123${EN_US_SEPARATORS.grouping}4${EN_US_SEPARATORS.grouping}"))
}
}

View File

@ -0,0 +1,14 @@
package cash.z.ecc.sdk.ext.ui.test
import android.content.Context
import android.text.TextUtils
import android.view.View
import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
import java.util.Locale
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
fun isLocaleRTL(locale: Locale) = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="cash.z.ecc.sdk.ext.ui">
<application />
</manifest>

View File

@ -0,0 +1,57 @@
package cash.z.ecc.sdk.ext.ui
import android.content.Context
import cash.z.ecc.sdk.model.MonetarySeparators
object ZecStringExt {
private const val DIGITS_BETWEEN_GROUP_SEPARATORS = 3
/**
* Builds filter with current local monetary separators for continuous input checking. Solution
* is build upon regex validation and character checking.
*
* Regex example: ^([0-9]*([0-9]+([,]$|[,][0-9]+))*([.]$|[.][0-9]+)?)?$
* Inputs may differ according to user locale.
*
* Valid amounts: "" . | .123 | 123, | 123. | 123,456 | 123.456 | 123,456.789 | 123,456,789 | 123,456,789.123 | etc.
* Invalid amounts: 123,, | 123,. | 123.. | .123 | ,123 | 123.456.789 | etc.
*
* @param context used for loading localized pattern from strings.xml
* @param separators which consist of localized monetary separators
* @param zecString to be validated
*
* @return true in case of validation success, false otherwise
*/
fun filterContinuous(context: Context, separators: MonetarySeparators, zecString: String): Boolean {
if (!context.getString(
R.string.zec_amount_regex_continuous_filter,
separators.grouping,
separators.decimal
).toRegex().matches(zecString) || !checkFor3Digits(separators, zecString)
) {
return false
}
return true
}
/**
* Checks for at least 3 digits between grouping separators.
*
* @param separators which consist of localized monetary separators
* @param zecString to be validated
*
* @return true in case of validation success, false otherwise
*/
fun checkFor3Digits(separators: MonetarySeparators, zecString: String): Boolean {
if (zecString.count { it == separators.grouping } >= 2) {
val groups = zecString.split(separators.grouping)
for (i in 1 until (groups.size - 1)) {
if (groups[i].length != DIGITS_BETWEEN_GROUP_SEPARATORS) {
return false
}
}
}
return true
}
}

View File

@ -0,0 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="zec_amount_regex_continuous_filter" formatted="true" tools:ignore="TypographyDashes">^([0-9]*([0-9]+([<xliff:g id="group" example=",">%1$s</xliff:g>]$|[<xliff:g id="group" example=",">%1$s</xliff:g>][0-9]+))*([<xliff:g id="dec" example=".">%2$s</xliff:g>]$|[<xliff:g id="dec" example=".">%2$s</xliff:g>][0-9]+)?)?$</string>
</resources>

View File

@ -210,6 +210,7 @@ include("build-info-lib")
include("preference-api-lib")
include("preference-impl-android-lib")
include("sdk-ext-lib")
include("sdk-ext-ui")
include("spackle-lib")
include("ui-design-lib")
include("ui-lib")

View File

@ -63,6 +63,7 @@ dependencies {
implementation(projects.preferenceApiLib)
implementation(projects.preferenceImplAndroidLib)
implementation(projects.sdkExtLib)
implementation(projects.sdkExtUi)
implementation(projects.spackleLib)
implementation(projects.uiDesignLib)

View File

@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.filters.MediumTest
import cash.z.ecc.sdk.fixture.WalletAddressFixture
@ -20,7 +21,6 @@ import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@ -38,9 +38,7 @@ class RequestViewTest {
@Suppress("UNUSED_VARIABLE")
val testSetup = TestSetup(composeTestRule)
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
it.assertIsNotEnabled()
}
composeTestRule.assertSendDisabled()
}
@Test
@ -52,22 +50,16 @@ class RequestViewTest {
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastCreateZecRequest())
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
val separators = MonetarySeparators.current()
composeTestRule.setValidAmount()
it.performTextInput("{${separators.decimal}}123")
}
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
it.performClick()
}
composeTestRule.clickCreateAndSend()
assertEquals(1, testSetup.getOnCreateCount())
testSetup.getLastCreateZecRequest().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.address)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertTrue(it.message.value.isEmpty())
}
}
@ -81,49 +73,91 @@ class RequestViewTest {
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastCreateZecRequest())
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
val separators = MonetarySeparators.current()
composeTestRule.setValidAmount()
it.performTextInput("{${separators.decimal}}123")
}
composeTestRule.setValidMessage()
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
it.performTextInput(ZecRequestFixture.MESSAGE.value)
}
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
it.performClick()
}
composeTestRule.clickCreateAndSend()
assertEquals(1, testSetup.getOnCreateCount())
testSetup.getLastCreateZecRequest().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.address)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertEquals(ZecRequestFixture.MESSAGE.value, it.message.value)
}
}
@Test
@MediumTest
@Ignore("https://github.com/zcash/secant-android-wallet/issues/218")
fun create_request_illegal_input() {
fun check_regex_functionality_valid_inputs() {
val testSetup = TestSetup(composeTestRule)
val separators = MonetarySeparators.current()
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastCreateZecRequest())
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
val separators = MonetarySeparators.current()
composeTestRule.setAmount("123")
composeTestRule.clickCreateAndSend()
assertEquals(1, testSetup.getOnCreateCount())
it.performTextInput("{${separators.decimal}}1{${separators.decimal}}2{${separators.decimal}}3{${separators.decimal}}4")
}
// e.g. 123,
composeTestRule.setAmount("123${separators.grouping}")
composeTestRule.clickCreateAndSend()
assertEquals(2, testSetup.getOnCreateCount())
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
it.performClick()
}
// e.g. 123.
composeTestRule.setAmount("123${separators.decimal}")
composeTestRule.clickCreateAndSend()
assertEquals(3, testSetup.getOnCreateCount())
// e.g. 123,456.
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}")
composeTestRule.clickCreateAndSend()
assertEquals(4, testSetup.getOnCreateCount())
// e.g. 123,456.789
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}789")
composeTestRule.clickCreateAndSend()
assertEquals(5, testSetup.getOnCreateCount())
}
@Test
@MediumTest
fun check_regex_functionality_invalid_inputs() {
val testSetup = TestSetup(composeTestRule)
val separators = MonetarySeparators.current()
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastCreateZecRequest())
composeTestRule.setAmount("aaa")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
composeTestRule.setAmount("123aaa")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
// e.g. ,.
composeTestRule.setAmount("${separators.grouping}${separators.decimal}")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
// e.g. 123,.
composeTestRule.setAmount("123${separators.grouping}${separators.decimal}")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
// e.g. 1,2,3
composeTestRule.setAmount("1${separators.grouping}2${separators.grouping}3")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
// e.g. 1.2.3
composeTestRule.setAmount("1${separators.decimal}2${separators.decimal}3")
composeTestRule.clickCreateAndSend()
assertEquals(0, testSetup.getOnCreateCount())
}
@ -133,32 +167,24 @@ class RequestViewTest {
fun max_message_length() = runTest {
val testSetup = TestSetup(composeTestRule)
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
val separators = MonetarySeparators.current()
composeTestRule.setValidAmount()
it.performTextInput("{${separators.decimal}}123")
}
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
val input = buildString {
repeat(ZecRequestMessage.MAX_MESSAGE_LENGTH + 1) { _ ->
append("$it")
composeTestRule.setMessage(
buildString {
repeat(ZecRequestMessage.MAX_MESSAGE_LENGTH + 1) { number ->
append("$number")
}
}
)
it.performTextInput(input)
}
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
it.performClick()
}
composeTestRule.clickCreateAndSend()
assertEquals(1, testSetup.getOnCreateCount())
testSetup.getLastCreateZecRequest().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.address)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertTrue(it.message.value.isEmpty())
}
}
@ -170,9 +196,7 @@ class RequestViewTest {
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.request_back_content_description)).also {
it.performClick()
}
composeTestRule.clickBack()
assertEquals(1, testSetup.getOnBackCount())
}
@ -217,3 +241,50 @@ class RequestViewTest {
}
}
}
private fun ComposeContentTestRule.clickBack() {
onNodeWithContentDescription(getStringResource(R.string.request_back_content_description)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.setValidAmount() {
onNodeWithText(getStringResource(R.string.request_amount)).also {
val separators = MonetarySeparators.current()
it.performTextClearance()
it.performTextInput("123${separators.decimal}456")
}
}
private fun ComposeContentTestRule.setAmount(amount: String) {
onNodeWithText(getStringResource(R.string.request_amount)).also {
it.performTextClearance()
it.performTextInput(amount)
}
}
private fun ComposeContentTestRule.setValidMessage() {
onNodeWithText(getStringResource(R.string.request_message)).also {
it.performTextClearance()
it.performTextInput(ZecRequestFixture.MESSAGE.value)
}
}
private fun ComposeContentTestRule.setMessage(message: String) {
onNodeWithText(getStringResource(R.string.request_message)).also {
it.performTextClearance()
it.performTextInput(message)
}
}
private fun ComposeContentTestRule.clickCreateAndSend() {
onNodeWithText(getStringResource(R.string.request_create)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.assertSendDisabled() {
onNodeWithText(getStringResource(R.string.request_create)).also {
it.assertIsNotEnabled()
}
}

View File

@ -1,11 +1,13 @@
package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.filters.MediumTest
import cash.z.ecc.sdk.fixture.MemoFixture
@ -21,7 +23,6 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@ -65,7 +66,7 @@ class SendViewTest {
testSetup.getLastSend().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.destination)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertTrue(it.memo.value.isEmpty())
}
}
@ -81,10 +82,7 @@ class SendViewTest {
composeTestRule.setValidAmount()
composeTestRule.setValidAddress()
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
it.performTextInput(MemoFixture.MEMO_STRING)
}
composeTestRule.setValidMemo()
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
@ -95,31 +93,96 @@ class SendViewTest {
testSetup.getLastSend().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.destination)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
}
}
@Test
@MediumTest
@Ignore("https://github.com/zcash/secant-android-wallet/issues/218")
fun create_request_illegal_amount() {
@OptIn(ExperimentalCoroutinesApi::class)
fun check_regex_functionality_valid_inputs() = runTest {
val testSetup = TestSetup(composeTestRule)
val separators = MonetarySeparators.current()
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastSend())
composeTestRule.assertSendDisabled()
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also {
val separators = MonetarySeparators.current()
it.performTextInput("{${separators.decimal}}1{${separators.decimal}}2{${separators.decimal}}3{${separators.decimal}}4")
}
composeTestRule.setValidAmount()
composeTestRule.setValidAddress()
composeTestRule.setValidMemo()
composeTestRule.assertSendEnabled()
composeTestRule.setAmount("123")
composeTestRule.assertSendEnabled()
// e.g. 123,
composeTestRule.setAmount("123${separators.grouping}")
composeTestRule.assertSendEnabled()
// e.g. 123.
composeTestRule.setAmount("123${separators.decimal}")
composeTestRule.assertSendEnabled()
// e.g. 123,456.
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}")
composeTestRule.assertSendEnabled()
// e.g. 123,456.789
composeTestRule.setAmount("123${separators.grouping}456${separators.decimal}789")
composeTestRule.assertSendEnabled()
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
composeTestRule.clickConfirmation()
assertEquals(1, testSetup.getOnCreateCount())
testSetup.getLastSend().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.destination)
assertEquals(Zatoshi(12345678900000), it.amount)
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
}
}
@Test
@MediumTest
@OptIn(ExperimentalCoroutinesApi::class)
fun check_regex_functionality_invalid_inputs() = runTest {
val testSetup = TestSetup(composeTestRule)
val separators = MonetarySeparators.current()
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastSend())
composeTestRule.assertSendDisabled()
composeTestRule.setAmount("aaa")
composeTestRule.assertSendDisabled()
composeTestRule.setAmount("123aaa")
composeTestRule.assertSendDisabled()
// e.g. ,.
composeTestRule.setAmount("${separators.grouping}${separators.decimal}")
composeTestRule.assertSendDisabled()
// e.g. 123,.
composeTestRule.setAmount("123${separators.grouping}${separators.decimal}")
composeTestRule.assertSendDisabled()
// e.g. 1,2,3
composeTestRule.setAmount("1${separators.grouping}2${separators.grouping}3")
composeTestRule.assertSendDisabled()
// e.g. 1.2.3
composeTestRule.setAmount("1${separators.decimal}2${separators.decimal}3")
composeTestRule.assertSendDisabled()
assertEquals(0, testSetup.getOnCreateCount())
assertEquals(null, testSetup.getLastSend())
composeTestRule.assertSendDisabled()
}
@Test
@ -137,9 +200,7 @@ class SendViewTest {
}
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
it.performTextInput(input)
}
composeTestRule.setMemo(input)
composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation()
@ -150,7 +211,7 @@ class SendViewTest {
testSetup.getLastSend().also {
assertNotNull(it)
assertEquals(WalletAddressFixture.unified(), it.destination)
assertEquals(Zatoshi(12300000), it.amount)
assertEquals(Zatoshi(12345600000), it.amount)
assertTrue(it.memo.value.isEmpty())
}
}
@ -235,17 +296,39 @@ private fun ComposeContentTestRule.clickBack() {
private fun ComposeContentTestRule.setValidAmount() {
onNodeWithText(getStringResource(R.string.send_amount)).also {
val separators = MonetarySeparators.current()
it.performTextClearance()
it.performTextInput("123${separators.decimal}456")
}
}
it.performTextInput("{${separators.decimal}}123")
private fun ComposeContentTestRule.setAmount(amount: String) {
onNodeWithText(getStringResource(R.string.send_amount)).also {
it.performTextClearance()
it.performTextInput(amount)
}
}
private fun ComposeContentTestRule.setValidAddress() {
onNodeWithText(getStringResource(R.string.send_to)).also {
it.performTextClearance()
it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
}
}
private fun ComposeContentTestRule.setValidMemo() {
onNodeWithText(getStringResource(R.string.send_memo)).also {
it.performTextClearance()
it.performTextInput(MemoFixture.MEMO_STRING)
}
}
private fun ComposeContentTestRule.setMemo(memo: String) {
onNodeWithText(getStringResource(R.string.send_memo)).also {
it.performTextClearance()
it.performTextInput(memo)
}
}
private fun ComposeContentTestRule.clickCreateAndSend() {
onNodeWithText(getStringResource(R.string.send_create)).also {
it.performClick()
@ -269,3 +352,15 @@ private fun ComposeContentTestRule.assertOnConfirmation() {
it.assertExists()
}
}
private fun ComposeContentTestRule.assertSendEnabled() {
onNodeWithText(getStringResource(R.string.send_create)).also {
it.assertIsEnabled()
}
}
private fun ComposeContentTestRule.assertSendDisabled() {
onNodeWithText(getStringResource(R.string.send_create)).also {
it.assertIsNotEnabled()
}
}

View File

@ -5,3 +5,5 @@ import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, formatArgs: Array<Any>) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)

View File

@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.sdk.model.Zatoshi
// TODO [#292]: Should be moved to SDK-EXT-UI module.
data class WalletSnapshot(
val status: Synchronizer.Status,
val processorInfo: CompactBlockProcessor.ProcessorInfo,

View File

@ -42,6 +42,7 @@ import kotlinx.coroutines.withContext
// To make this more multiplatform compatible, we need to remove the dependency on Context
// for loading the preferences.
// TODO [#292]: Should be moved to SDK-EXT-UI module.
class WalletViewModel(application: Application) : AndroidViewModel(application) {
private val walletCoordinator = co.electriccoin.zcash.global.WalletCoordinator.getInstance(application)

View File

@ -19,9 +19,11 @@ import androidx.compose.runtime.mutableStateOf
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.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import cash.z.ecc.sdk.ext.ui.ZecStringExt
import cash.z.ecc.sdk.fixture.WalletAddressFixture
import cash.z.ecc.sdk.model.MonetarySeparators
import cash.z.ecc.sdk.model.WalletAddress
@ -90,11 +92,13 @@ private fun RequestTopAppBar(onBack: () -> Unit) {
// TODO [#215]: Need to add some UI to explain to the user if a request is invalid
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
// TODO [#288]: TextField component can't do long-press backspace.
@Composable
private fun RequestMainContent(
myAddress: WalletAddress.Unified,
onCreateAndSend: (ZecRequest) -> Unit
) {
val context = LocalContext.current
val monetarySeparators = MonetarySeparators.current()
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
@ -102,10 +106,13 @@ private fun RequestMainContent(
var message by rememberSaveable { mutableStateOf("") }
Column(Modifier.fillMaxHeight()) {
// TODO [#289]: Crash occurs while typed more than some acceptable amount to this field.
TextField(
value = amountZecString,
onValueChange = { newValue ->
// TODO [#218]: this doesn't prevent illegal input. So users could still type `1.2.3.4`
if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) {
return@TextField
}
amountZecString = newValue.filter { allowedCharacters.contains(it) }
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),

View File

@ -247,6 +247,7 @@ private fun ChipGridWithText(
}
}
// TODO [#288]: TextField component can't do long-press backspace.
@Composable
private fun NextWordTextField(modifier: Modifier = Modifier, text: String, setText: (String) -> Unit) {
/*

View File

@ -22,10 +22,12 @@ import androidx.compose.runtime.mutableStateOf
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.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.sdk.ext.ui.ZecStringExt
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import cash.z.ecc.sdk.model.Memo
import cash.z.ecc.sdk.model.MonetarySeparators
@ -134,12 +136,16 @@ private fun SendMainContent(
}
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
// TODO [#288]: TextField component can't do long-press backspace.
// TODO [#294]: DetektAll failed LongMethod
@Suppress("LongMethod")
@Composable
private fun SendForm(
myBalance: Zatoshi,
previousZecSend: ZecSend?,
onCreateAndSend: (ZecSend) -> Unit
) {
val context = LocalContext.current
val monetarySeparators = MonetarySeparators.current()
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
@ -161,7 +167,9 @@ private fun SendForm(
TextField(
value = amountZecString,
onValueChange = { newValue ->
// TODO [#218]: this doesn't prevent illegal input. So users could still type `1.2.3.4`
if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) {
return@TextField
}
amountZecString = newValue.filter { allowedCharacters.contains(it) }
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),