Updated on 2026-08-14
This commit is contained in:
parent
8ea23a2dd7
commit
4f1710d77c
23 changed files with 884 additions and 29 deletions
|
|
@ -129,6 +129,47 @@ When the user asks to **port** an iOS test to Android:
|
||||||
|
|
||||||
Scenario files orchestrate flows; they must not define page objects or duplicate generic helpers.
|
Scenario files orchestrate flows; they must not define page objects or duplicate generic helpers.
|
||||||
|
|
||||||
|
### Page-object matchers: exhaust the native Kakao API before dropping to raw Compose
|
||||||
|
|
||||||
|
**Reviewers reject raw `composeTestRule` / `semanticsProvider.onNode(...)` / `onAllNodes(...)[i]` and
|
||||||
|
deep nested matchers when a native Kakao-Compose mechanism does the same thing.** Before writing any
|
||||||
|
such construct, look for the built-in KNode / `ViewBuilder` / `KLazyListNode` API — it almost always
|
||||||
|
exists. The raw form is a last resort, and even then it stays **inside the page object**, never in the
|
||||||
|
test body (the test only calls page-object members and scenarios — no `composeTestRule`, no test tags,
|
||||||
|
no `onNode`/`onAllNodes`, no bare matchers leak into it).
|
||||||
|
|
||||||
|
Native first, by need:
|
||||||
|
- **N-th of several identical nodes** → `child { … ; hasPosition(index) }` (Kakao maps
|
||||||
|
`NodeMatcher.position` → `onAllNodes(matcher)[index]` for you). Do **not** hand-roll
|
||||||
|
`semanticsProvider.onAllNodes(matcher)[index]`.
|
||||||
|
- **Scroll to index / matcher / key** → inside a KNode block: `knode { performScrollToIndex(index) }`,
|
||||||
|
`knode { performScrollToNode(matcher) }`, `knode { performScrollToKey(key) }` (mirror
|
||||||
|
`MarketsPageObject.scrollToListedOnBlock`). These wrappers are `@ExperimentalTestApi`, so annotate the
|
||||||
|
page-object method `@OptIn(ExperimentalTestApi::class)` — that opt-in is expected, not a smell.
|
||||||
|
- **Relationship filters** → `ViewBuilder` DSL inside `child { }`: `hasAnyChild`, `hasAnySibling`,
|
||||||
|
`hasAnyAncestor`, `hasAnyDescendant`, `addSemanticsMatcher(matcher)`, `useUnmergedTree = true`.
|
||||||
|
- **Lazy list / pager item (esp. below the fold)** → `KLazyListNode` + `childWith { … }` / `childAt(index)`
|
||||||
|
(see `AddFundsBottomSheetPageObject`, `BuyTokenPageObject`), not a manual scroll + `onAllNodes`.
|
||||||
|
- **A raw Compose-Test op with no KNode wrapper** (e.g. `captureToImage()`, and any other
|
||||||
|
`SemanticsNodeInteraction` extension Kakao doesn't surface) → do **not** fall back to
|
||||||
|
`composeTestRule.onNode(hasTestTag(...))` in the scenario. Every KNode exposes a public `delegate`, and
|
||||||
|
the built-in actions/assertions are all just `delegate.perform(type) { <this: SemanticsNodeInteraction> }`
|
||||||
|
/ `delegate.check(type) { … }`. `ComposeOperationType` is an open interface, so declare a tiny private
|
||||||
|
`enum class Xxx : ComposeOperationType { … }` and reach the underlying `SemanticsNodeInteraction` from a
|
||||||
|
**page-object method** — reusing an existing KNode (its testTag + `useUnmergedTree`). Capture a return
|
||||||
|
value via a `lateinit var` written inside the lambda. Example (`TokenReceiveQrCodeBottomSheetPageObject.captureQrCodeBitmap`):
|
||||||
|
```kotlin
|
||||||
|
fun captureQrCodeBitmap(): Bitmap {
|
||||||
|
lateinit var bitmap: Bitmap
|
||||||
|
qrCode.delegate.perform(QrCodeAction.CAPTURE) { bitmap = captureToImage().asAndroidBitmap() }
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
private enum class QrCodeAction : ComposeOperationType { CAPTURE }
|
||||||
|
```
|
||||||
|
This keeps `composeTestRule` / test tags out of the scenario — the scenario just calls the page-object method.
|
||||||
|
- **Only if truly nothing fits** → `semanticsProvider.onNode(...)` / `onAllNodes(...)` (the escape hatch
|
||||||
|
used by `MainScreenPageObject`), wrapped in a named page-object method with a one-line WHY comment.
|
||||||
|
|
||||||
### Strings
|
### Strings
|
||||||
|
|
||||||
- **No hardcoded UI text** in matchers. Use `getResourceString(R.string.foo)` from
|
- **No hardcoded UI text** in matchers. Use `getResourceString(R.string.foo)` from
|
||||||
|
|
@ -164,20 +205,14 @@ Scenario files orchestrate flows; they must not define page objects or duplicate
|
||||||
that a screen "never idles", **cold-boot a fresh emulator** (`emulator -avd … -no-snapshot -wipe-data
|
that a screen "never idles", **cold-boot a fresh emulator** (`emulator -avd … -no-snapshot -wipe-data
|
||||||
-memory 4096 -cores 2`) and re-run. A suite that flaked across runs on a tired emulator can be a clean
|
-memory 4096 -cores 2`) and re-run. A suite that flaked across runs on a tired emulator can be a clean
|
||||||
10/10 on a fresh one (verified on this exact suite). Don't rewrite waits to work around emulator rot.
|
10/10 on a fresh one (verified on this exact suite). Don't rewrite waits to work around emulator rot.
|
||||||
- **In scenario / `BaseTestCase`-extension code, `flakySafely` is NOT available** regardless — use the
|
- **In scenario / `BaseTestCase`-extension code, `flakySafely` is NOT available** regardless — use
|
||||||
same `composeTestRule.waitUntil` fallback (or `waitUntilAtLeastOneExists(matcher, timeout)` to wait for
|
`BaseTestCase.awaitSuccess(timeoutMillis = WAIT_UNTIL_TIMEOUT) { … }` (a shared member on `BaseTestCase`,
|
||||||
appearance, `{ a exists || b exists }` for either/or).
|
no import needed) which wraps `composeTestRule.waitUntil { runCatching(block).isSuccess }`. Each async step
|
||||||
- **Don't repeat the `composeTestRule.waitUntil(timeoutMillis = …) { runCatching { … }.isSuccess }`
|
reads `awaitSuccess { onXxxScreen { field.assertExists() } }` before the action. For appearance-only waits
|
||||||
block across steps — extract a one-line private helper** in the scenario file and call that instead.
|
`composeTestRule.waitUntilAtLeastOneExists(matcher, timeout)` (or `{ a exists || b exists }` for either/or)
|
||||||
A multi-step scenario that gates every async step this way turns into copy-paste noise (and reviewers
|
is also fine.
|
||||||
flag it). Add once, near the top of the file:
|
**Do not re-declare a private `awaitSuccess` in a scenario file** — the shared `BaseTestCase.awaitSuccess`
|
||||||
```kotlin
|
already exists; older files may still have a private copy, don't copy that pattern.
|
||||||
// flakySafely is unavailable in BaseTestCase extensions — wait until the assertion/action stops throwing.
|
|
||||||
private fun BaseTestCase.awaitSuccess(block: () -> Unit) {
|
|
||||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT) { runCatching(block).isSuccess }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
then each step reads `awaitSuccess { onXxxScreen { field.assertExists() } }` before the action.
|
|
||||||
- **Right-size the timeout — don't stamp `WAIT_UNTIL_TIMEOUT_LONG` on every step.** The timeout is a
|
- **Right-size the timeout — don't stamp `WAIT_UNTIL_TIMEOUT_LONG` on every step.** The timeout is a
|
||||||
*ceiling*, not a sleep (`waitUntil` returns the moment the condition holds), but the default
|
*ceiling*, not a sleep (`waitUntil` returns the moment the condition holds), but the default
|
||||||
`WAIT_UNTIL_TIMEOUT` (20 s) already dwarfs a normal async transition. Reserve `…_LONG` / `…_VERY_LONG`
|
`WAIT_UNTIL_TIMEOUT` (20 s) already dwarfs a normal async transition. Reserve `…_LONG` / `…_VERY_LONG`
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
||||||
import com.tangem.common.allure.FailedStepScreenshotInterceptor
|
import com.tangem.common.allure.FailedStepScreenshotInterceptor
|
||||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME
|
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME
|
||||||
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE
|
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.rules.ApiEnvironmentRule
|
import com.tangem.common.rules.ApiEnvironmentRule
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
|
|
@ -179,6 +180,14 @@ abstract class BaseTestCase : TestCase(
|
||||||
|
|
||||||
fun waitForIdle() = composeTestRule.waitForIdle()
|
fun waitForIdle() = composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until [block] stops throwing (or [timeoutMillis] elapses). Use in scenario (BaseTestCase extension)
|
||||||
|
* code where flakySafely is unavailable; in test bodies prefer flakySafely.
|
||||||
|
*/
|
||||||
|
fun awaitSuccess(timeoutMillis: Long = WAIT_UNTIL_TIMEOUT, block: () -> Unit) {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = timeoutMillis) { runCatching(block).isSuccess }
|
||||||
|
}
|
||||||
|
|
||||||
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
|
||||||
return ApplicationInjectionExecutionRule(
|
return ApplicationInjectionExecutionRule(
|
||||||
toggleStates = mapOf(
|
toggleStates = mapOf(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.tangem.common.utils
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import com.google.zxing.BinaryBitmap
|
||||||
|
import com.google.zxing.DecodeHintType
|
||||||
|
import com.google.zxing.RGBLuminanceSource
|
||||||
|
import com.google.zxing.common.HybridBinarizer
|
||||||
|
import com.google.zxing.qrcode.QRCodeReader
|
||||||
|
|
||||||
|
/** Decodes the text encoded in a QR-code [bitmap] (e.g. captured from a Compose node via captureToImage). */
|
||||||
|
fun decodeQrCode(bitmap: Bitmap): String {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
val pixels = IntArray(width * height)
|
||||||
|
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
|
||||||
|
val source = RGBLuminanceSource(width, height, pixels)
|
||||||
|
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
||||||
|
val hints = mapOf(DecodeHintType.TRY_HARDER to true)
|
||||||
|
|
||||||
|
return QRCodeReader().decode(binaryBitmap, hints).text
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,19 @@ fun BaseTestCase.openMainScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the main screen, synchronizes addresses, and opens the details of the token with [tokenName]. */
|
||||||
|
fun BaseTestCase.openTokenDetails(tokenName: String) {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessCode: String = "") {
|
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessCode: String = "") {
|
||||||
step("Click on 'Get started' button") {
|
step("Click on 'Get started' button") {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.tangem.scenarios
|
package com.tangem.scenarios
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.performTextInputInChunks
|
import com.tangem.common.extensions.performTextInputInChunks
|
||||||
import com.tangem.screens.accounts.onAccountDetailsScreen
|
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||||
|
|
@ -16,11 +15,6 @@ import com.tangem.core.res.R as CoreResR
|
||||||
|
|
||||||
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
|
private fun mainAccountName(): String = getResourceString(CoreResR.string.account_main_account_title)
|
||||||
|
|
||||||
// flakySafely is unavailable in BaseTestCase extensions — wait until the assertion/action stops throwing.
|
|
||||||
private fun BaseTestCase.awaitSuccess(block: () -> Unit) {
|
|
||||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT) { runCatching(block).isSuccess }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
|
fun BaseTestCase.openManageTokens(accountName: String = mainAccountName()) {
|
||||||
openWalletSettingsScreen()
|
openWalletSettingsScreen()
|
||||||
openAccountDetails(accountName)
|
openAccountDetails(accountName)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,58 @@
|
||||||
package com.tangem.scenarios
|
package com.tangem.scenarios
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.extractText
|
||||||
|
import com.tangem.common.utils.decodeQrCode
|
||||||
|
import com.tangem.screens.onAddFundsBottomSheet
|
||||||
import com.tangem.screens.onReceiveAssetsBottomSheet
|
import com.tangem.screens.onReceiveAssetsBottomSheet
|
||||||
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
|
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
|
||||||
import com.tangem.screens.onTokenReceiveWarningBottomSheet
|
import com.tangem.screens.onTokenReceiveWarningBottomSheet
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
import org.junit.Assert
|
||||||
|
|
||||||
|
/** Opens the receive flow from a funded token's details via 'Add funds' → 'Receive'. */
|
||||||
|
fun BaseTestCase.openReceiveViaAddFunds() {
|
||||||
|
step("Click on 'Add funds' button") {
|
||||||
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts the QR code encodes the displayed address for both address types of a two-address-type coin,
|
||||||
|
* and that the two addresses differ.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.assertQrCodesMatchForBothAddressTypes() {
|
||||||
|
step("Go to QR code bottom sheet for the first address type") {
|
||||||
|
goToQrCodeBottomSheet()
|
||||||
|
}
|
||||||
|
var firstAddress = ""
|
||||||
|
step("Assert QR code encodes the first displayed address") {
|
||||||
|
firstAddress = assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
step("Go back to the receive addresses") {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
}
|
||||||
|
step("Switch to the second address type") {
|
||||||
|
awaitSuccess(WAIT_UNTIL_TIMEOUT_LONG) { onReceiveAssetsBottomSheet { addressesPager.assertIsDisplayed() } }
|
||||||
|
onReceiveAssetsBottomSheet { scrollToAddress(1) }
|
||||||
|
}
|
||||||
|
step("Click on 'Show QR code' button for the second address type") {
|
||||||
|
onReceiveAssetsBottomSheet { showQrCodeButton(1).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
var secondAddress = ""
|
||||||
|
step("Assert QR code encodes the second displayed address") {
|
||||||
|
secondAddress = assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
step("Assert the two address types are different") {
|
||||||
|
Assert.assertNotEquals(firstAddress, secondAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.goToQrCodeBottomSheet() {
|
fun BaseTestCase.goToQrCodeBottomSheet() {
|
||||||
step("Assert 'Token receive warning' bottom sheet is displayed") {
|
step("Assert 'Token receive warning' bottom sheet is displayed") {
|
||||||
|
|
@ -15,7 +62,7 @@ fun BaseTestCase.goToQrCodeBottomSheet() {
|
||||||
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
|
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Show QR code' button") {
|
step("Click on 'Show QR code' button") {
|
||||||
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
|
onReceiveAssetsBottomSheet { showQrCodeButton().clickWithAssertion() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,3 +86,16 @@ fun BaseTestCase.checkQrCodeBottomSheetScenario() {
|
||||||
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
|
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Decodes the QR code on the receive bottom sheet and asserts it encodes the displayed address; returns that address. */
|
||||||
|
fun BaseTestCase.assertQrCodeEncodesDisplayedAddress(): String {
|
||||||
|
var displayedAddress = ""
|
||||||
|
step("Assert QR code encodes the displayed address") {
|
||||||
|
onTokenReceiveQrCodeBottomSheet {
|
||||||
|
displayedAddress = address.extractText()
|
||||||
|
val decoded = decodeQrCode(captureQrCodeBitmap())
|
||||||
|
Assert.assertEquals(displayedAddress, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return displayedAddress
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.test.TokenReceiveAssetsBottomSheetTestTags
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
@ -11,10 +13,23 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class ReceiveAssetsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<ReceiveAssetsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
val showQrCodeButton: KNode = child {
|
/** 'Show QR code' button of the address at [index]; both pager cards stay composed, so match by position. */
|
||||||
|
fun showQrCodeButton(index: Int = 0): KNode = child {
|
||||||
hasText(getResourceString(R.string.token_receive_show_qr_code_title))
|
hasText(getResourceString(R.string.token_receive_show_qr_code_title))
|
||||||
|
hasPosition(index)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val addressesPager: KNode = child {
|
||||||
|
hasTestTag(TokenReceiveAssetsBottomSheetTestTags.ADDRESSES_PAGER)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pages the addresses carousel to the address of the given [index]. */
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun scrollToAddress(index: Int) {
|
||||||
|
addressesPager { performScrollToIndex(index) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
|
internal fun BaseTestCase.onReceiveAssetsBottomSheet(function: ReceiveAssetsBottomSheetPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,11 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 'Receive' row of the zero-balance actions block (Buy / Swap / Receive), shown instead of the action buttons. */
|
||||||
|
val receiveButton: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.common_receive))
|
||||||
|
}
|
||||||
|
|
||||||
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
||||||
hasAnySibling(withText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName)))
|
hasAnySibling(withText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCurrencyName)))
|
||||||
hasTestTag(NotificationTestTags.ICON)
|
hasTestTag(NotificationTestTags.ICON)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.TokenMarketBlockTestTags
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
||||||
|
class TokenMarketBlockPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<TokenMarketBlockPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val block: KNode = child {
|
||||||
|
hasTestTag(TokenMarketBlockTestTags.BLOCK)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val title: KNode = child {
|
||||||
|
hasTestTag(TokenMarketBlockTestTags.TITLE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val price: KNode = child {
|
||||||
|
hasTestTag(TokenMarketBlockTestTags.PRICE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val priceChange: KNode = child {
|
||||||
|
hasTestTag(TokenMarketBlockTestTags.PRICE_CHANGE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val chart: KNode = child {
|
||||||
|
hasTestTag(TokenMarketBlockTestTags.CHART)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onTokenMarketBlock(function: TokenMarketBlockPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import androidx.compose.ui.test.captureToImage
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
|
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import io.github.kakaocup.compose.intercept.operation.ComposeOperationType
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
@ -50,6 +54,17 @@ class TokenReceiveQrCodeBottomSheetPageObject(semanticsProvider: SemanticsNodeIn
|
||||||
hasText(getResourceString(R.string.common_share))
|
hasText(getResourceString(R.string.common_share))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Captures the QR code node as a bitmap via the Kakao node delegate (no raw composeTestRule access). */
|
||||||
|
fun captureQrCodeBitmap(): Bitmap {
|
||||||
|
lateinit var bitmap: Bitmap
|
||||||
|
qrCode.delegate.perform(QrCodeAction.CAPTURE) {
|
||||||
|
bitmap = captureToImage().asAndroidBitmap()
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class QrCodeAction : ComposeOperationType { CAPTURE }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onTokenReceiveQrCodeBottomSheet(function: TokenReceiveQrCodeBottomSheetPageObject.() -> Unit) =
|
internal fun BaseTestCase.onTokenReceiveQrCodeBottomSheet(function: TokenReceiveQrCodeBottomSheetPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.tangem.screens
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
|
||||||
import com.tangem.core.ui.test.TokenReceiveWarningBottomSheetTestTags
|
import com.tangem.core.ui.test.TokenReceiveWarningBottomSheetTestTags
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
|
|
@ -18,9 +17,7 @@ class TokenReceiveWarningBottomSheetPageObject(semanticsProvider: SemanticsNodeI
|
||||||
}
|
}
|
||||||
|
|
||||||
val gotItButton: KNode = child {
|
val gotItButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
|
||||||
hasText(getResourceString(R.string.common_got_it))
|
hasText(getResourceString(R.string.common_got_it))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ class TxHistoryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun transactionUnconfirmedStatus(title: String): KNode = transactionItem(title).child {
|
||||||
|
hasTestTag(TransactionHistoryItemTestTags.STATUS_UNCONFIRMED)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transactionAddress(title: String, address: String): KNode = transactionItem(title).child {
|
||||||
|
hasText(text = address, substring = true)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
package com.tangem.tests.actionButtons
|
package com.tangem.tests.actionButtons
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.pullToRefresh
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
||||||
|
import com.tangem.scenarios.enterAmountAndOpenSendConfirm
|
||||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
import com.tangem.scenarios.openSendFromTokenDetails
|
import com.tangem.scenarios.openSendFromTokenDetails
|
||||||
|
import com.tangem.scenarios.openSendScreenWithHotWallet
|
||||||
|
import com.tangem.scenarios.openSendSuccessScreenViaLongClickOnSendButton
|
||||||
|
import com.tangem.scenarios.readNetworkFeeAmount
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.scenarios.waitUntilNetworkFeeIsStable
|
||||||
import com.tangem.screens.onAddFundsBottomSheet
|
import com.tangem.screens.onAddFundsBottomSheet
|
||||||
|
import com.tangem.screens.onDialog
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.screens.onSendScreen
|
import com.tangem.screens.onSendScreen
|
||||||
|
import com.tangem.screens.onSendSuccessScreen
|
||||||
import com.tangem.screens.onSwapStoriesScreen
|
import com.tangem.screens.onSwapStoriesScreen
|
||||||
import com.tangem.screens.onSwapTokenScreen
|
import com.tangem.screens.onSwapTokenScreen
|
||||||
import com.tangem.screens.onTokenDetailsScreen
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
import com.tangem.screens.onTransferBottomSheet
|
import com.tangem.screens.onTransferBottomSheet
|
||||||
|
import com.tangem.screens.onTxHistoryScreen
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
|
|
@ -259,4 +275,146 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("4465")
|
||||||
|
@DisplayName("Action buttons (token details screen): 'Send' blocked while a transaction is active, works after completion")
|
||||||
|
@Test
|
||||||
|
fun sendBlockedWhileTransactionActiveTest() {
|
||||||
|
val tokenName = "XRP Ledger"
|
||||||
|
val amount = "1"
|
||||||
|
val userTokensState = "XRPHotWalletSvS"
|
||||||
|
val quotesState = "Ripple"
|
||||||
|
val startedState = "Started"
|
||||||
|
val rippleAccountInfoScenario = "ripple_account_info"
|
||||||
|
val pendingSendMessagePrefix =
|
||||||
|
getResourceString(R.string.token_button_unavailability_reason_pending_transaction_send).substringBefore("%")
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(rippleAccountInfoScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$rippleAccountInfoScenario' to state: '$startedState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = rippleAccountInfoScenario, state = startedState)
|
||||||
|
}
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Enter amount '$amount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = amount, recipientAddress = XRP_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSendSuccessScreen { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open the transfer bottom sheet") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button is not enabled while the transaction is active") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button") {
|
||||||
|
onTransferBottomSheet { sendButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert pending-transaction notification dialog is displayed") {
|
||||||
|
onDialog { containerWithText(pendingSendMessagePrefix).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' screen is not opened") {
|
||||||
|
onSendScreen { amountInputTextField.assertDoesNotExist() }
|
||||||
|
}
|
||||||
|
// Tapping the 'Send' row dismisses the transfer bottom sheet (onActionDispatched) before the dialog shows.
|
||||||
|
step("Close the notification dialog") {
|
||||||
|
onDialog { okButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Pull to refresh to complete the active transaction") {
|
||||||
|
pullToRefresh()
|
||||||
|
}
|
||||||
|
step("Open the transfer bottom sheet again") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button is enabled after the transaction is completed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button") {
|
||||||
|
onTransferBottomSheet { sendButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' screen is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendScreen { amountInputTextField.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10209")
|
||||||
|
@DisplayName("Action buttons (token details screen): 'Send' unavailable for a zero-balance token with an active transaction")
|
||||||
|
@Test
|
||||||
|
fun sendUnavailableForZeroBalanceWithActiveTransactionTest() {
|
||||||
|
val tokenName = "Dogecoin"
|
||||||
|
val zeroBalanceState = "ZeroBalance"
|
||||||
|
val activeTxHistoryState = "UnconfirmedOutgoing"
|
||||||
|
val balanceScenarioName = "dogecoin_balance"
|
||||||
|
val txHistoryScenarioName = "dogecoin_tx_history"
|
||||||
|
val sendingTitle = getResourceString(R.string.common_sending)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(balanceScenarioName)
|
||||||
|
resetWireMockScenarioState(txHistoryScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$balanceScenarioName' to state: '$zeroBalanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = balanceScenarioName, state = zeroBalanceState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$activeTxHistoryState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = activeTxHistoryState)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert active outgoing '$sendingTitle' transaction block is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Transfer' button is not displayed for the zero-balance token") {
|
||||||
|
onTokenDetailsScreen { transferButton.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
package com.tangem.tests.tokenDetails
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.assertQrCodeEncodesDisplayedAddress
|
||||||
|
import com.tangem.scenarios.assertQrCodesMatchForBothAddressTypes
|
||||||
|
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||||
|
import com.tangem.scenarios.openReceiveViaAddFunds
|
||||||
|
import com.tangem.scenarios.openTokenDetails
|
||||||
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class TokenDetailsAddressesTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("4947")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Bitcoin, 2 address types)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressBitcoinTest() {
|
||||||
|
val tokenName = "Bitcoin"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Open receive via 'Add funds'") {
|
||||||
|
openReceiveViaAddFunds()
|
||||||
|
}
|
||||||
|
step("Assert QR codes match for both address types") {
|
||||||
|
assertQrCodesMatchForBothAddressTypes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10218")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Cosmos)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressCosmosTest() {
|
||||||
|
val tokenName = "Cosmos"
|
||||||
|
val networksProvidersScenario = "networks_providers"
|
||||||
|
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(networksProvidersScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Open receive via 'Add funds'") {
|
||||||
|
openReceiveViaAddFunds()
|
||||||
|
}
|
||||||
|
step("Go to QR code bottom sheet") {
|
||||||
|
goToQrCodeBottomSheet()
|
||||||
|
}
|
||||||
|
step("Assert QR code encodes the displayed address") {
|
||||||
|
assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10219")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressKaspaTest() {
|
||||||
|
val tokenName = "Kaspa"
|
||||||
|
val networksProvidersScenario = "networks_providers"
|
||||||
|
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||||
|
val quotesKaspaState = "Kaspa"
|
||||||
|
val kaspaUtxoScenario = "kaspa_utxo"
|
||||||
|
val kaspaUtxoState = "more_than_84_android"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(networksProvidersScenario)
|
||||||
|
resetWireMockScenarioState(kaspaUtxoScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesKaspaState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesKaspaState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$kaspaUtxoScenario' to state: '$kaspaUtxoState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUtxoScenario, state = kaspaUtxoState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Open receive via 'Add funds'") {
|
||||||
|
openReceiveViaAddFunds()
|
||||||
|
}
|
||||||
|
step("Go to QR code bottom sheet") {
|
||||||
|
goToQrCodeBottomSheet()
|
||||||
|
}
|
||||||
|
step("Assert QR code encodes the displayed address") {
|
||||||
|
assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10215")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Litecoin, 2 address types)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressLitecoinTest() {
|
||||||
|
val tokenName = "Litecoin"
|
||||||
|
val userTokensState = "Litecoin"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' button") {
|
||||||
|
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert QR codes match for both address types") {
|
||||||
|
assertQrCodesMatchForBothAddressTypes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10220")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (XDC Network, 2 address types)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressXdcTest() {
|
||||||
|
val tokenName = "XDC Network"
|
||||||
|
val userTokensState = "XDC"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' button") {
|
||||||
|
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert QR codes match for both address types") {
|
||||||
|
assertQrCodesMatchForBothAddressTypes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10216")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Hedera)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressHederaTest() {
|
||||||
|
val tokenName = "Hedera"
|
||||||
|
val networksProvidersScenario = "networks_providers"
|
||||||
|
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(networksProvidersScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Open receive via 'Add funds'") {
|
||||||
|
openReceiveViaAddFunds()
|
||||||
|
}
|
||||||
|
step("Go to QR code bottom sheet") {
|
||||||
|
goToQrCodeBottomSheet()
|
||||||
|
}
|
||||||
|
step("Assert QR code encodes the displayed address") {
|
||||||
|
assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10217")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Ethereum)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressEthereumTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Open receive via 'Add funds'") {
|
||||||
|
openReceiveViaAddFunds()
|
||||||
|
}
|
||||||
|
step("Go to QR code bottom sheet") {
|
||||||
|
goToQrCodeBottomSheet()
|
||||||
|
}
|
||||||
|
step("Assert QR code encodes the displayed address") {
|
||||||
|
assertQrCodeEncodesDisplayedAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("10214")
|
||||||
|
@DisplayName("Token details (address): QR code encodes the displayed address (Decimal Smart Chain, 2 address types)")
|
||||||
|
@Test
|
||||||
|
fun qrCodeEncodesDisplayedAddressDecimalTest() {
|
||||||
|
val tokenName = "Decimal Smart Chain"
|
||||||
|
val userTokensState = "Decimal"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = { resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) },
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open token details for '$tokenName'") {
|
||||||
|
openTokenDetails(tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' action") {
|
||||||
|
onTokenDetailsScreen { receiveButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert QR codes match for both address types") {
|
||||||
|
assertQrCodesMatchForBothAddressTypes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package com.tangem.tests.tokenDetails
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
|
import com.tangem.screens.onTokenMarketBlock
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class TokenDetailsMarketPriceTests : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("301")
|
||||||
|
@DisplayName("Token details: Market Price block data")
|
||||||
|
@Test
|
||||||
|
fun marketPriceBlockDataTest() {
|
||||||
|
val tokenName = "Dogecoin"
|
||||||
|
val marketPriceTitle = getResourceString(R.string.markets_common_market_price)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Market Price' block is displayed with title '$marketPriceTitle'") {
|
||||||
|
onTokenMarketBlock {
|
||||||
|
block.assertIsDisplayed()
|
||||||
|
title.assertTextEquals(marketPriceTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert price rate is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTokenMarketBlock { price.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 24h price change is displayed") {
|
||||||
|
onTokenMarketBlock { priceChange.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert mini chart is displayed") {
|
||||||
|
onTokenMarketBlock { chart.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.tangem.tests.tokenDetails
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.DOGECOIN_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
|
import com.tangem.screens.onTxHistoryScreen
|
||||||
|
import com.tangem.utils.toBriefAddressFormat
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class TokenDetailsTests : BaseTestCase() {
|
||||||
|
|
||||||
|
private val txHistoryScenarioName = "dogecoin_tx_history"
|
||||||
|
|
||||||
|
@AllureId("304")
|
||||||
|
@DisplayName("Token details: active outgoing transaction block")
|
||||||
|
@Test
|
||||||
|
fun activeOutgoingTransactionBlockTest() {
|
||||||
|
val tokenName = "Dogecoin"
|
||||||
|
val currencySymbol = "DOGE"
|
||||||
|
val txHistoryScenarioState = "UnconfirmedOutgoing"
|
||||||
|
val sendingTitle = getResourceString(R.string.common_sending)
|
||||||
|
val recipientBriefAddress = DOGECOIN_RECIPIENT_ADDRESS.toBriefAddressFormat()
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(txHistoryScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$txHistoryScenarioName' to state: '$txHistoryScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = txHistoryScenarioName, state = txHistoryScenarioState)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert active outgoing '$sendingTitle' transaction is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTxHistoryScreen { transactionItem(sendingTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert active outgoing transaction status is unconfirmed") {
|
||||||
|
onTxHistoryScreen { transactionUnconfirmedStatus(sendingTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert active outgoing transaction amount is displayed in '$currencySymbol'") {
|
||||||
|
onTxHistoryScreen {
|
||||||
|
transactionAmount(sendingTitle).assertIsDisplayed()
|
||||||
|
transactionCurrency(sendingTitle).assertTextEquals(currencySymbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert active outgoing transaction recipient address '$recipientBriefAddress' is displayed") {
|
||||||
|
onTxHistoryScreen { transactionAddress(sendingTitle, recipientBriefAddress).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -160,6 +160,10 @@ object WalletMockContent : MockContent {
|
||||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/111111'/0'/0/0") to ExtendedPublicKey( // Kaspa
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
extendedPublicKey = ExtendedPublicKey(
|
extendedPublicKey = ExtendedPublicKey(
|
||||||
publicKey = secp256k1WalletPublicKey,
|
publicKey = secp256k1WalletPublicKey,
|
||||||
|
|
@ -254,6 +258,13 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/84'/2'/0'/0/0") to ExtendedPublicKey( // ltc (reuses valid btc key)
|
||||||
|
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
|
||||||
|
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
|
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
|
||||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||||
|
|
@ -261,6 +272,13 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/550'/0'/0/0") to ExtendedPublicKey( // xdc (EVM, reuses valid eth key)
|
||||||
|
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||||
|
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
DerivationPath("m/44'/60'/0'/0/1") to ExtendedPublicKey( // eth (account 2)
|
DerivationPath("m/44'/60'/0'/0/1") to ExtendedPublicKey( // eth (account 2)
|
||||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
|
||||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||||
|
|
@ -486,6 +504,13 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/3030'/0'/0'/0'") to ExtendedPublicKey( // Hedera (address resolves via network)
|
||||||
|
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
|
||||||
|
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
fun RowScope.TokenRowPriceChangeContent(
|
fun RowScope.TokenRowPriceChangeContent(
|
||||||
priceChangeState: PriceChangeState.Content,
|
priceChangeState: PriceChangeState.Content,
|
||||||
isFlickering: Boolean,
|
isFlickering: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
isAvailable: Boolean = true,
|
isAvailable: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val color = when (priceChangeState.type) {
|
val color = when (priceChangeState.type) {
|
||||||
|
|
@ -54,7 +55,7 @@ fun RowScope.TokenRowPriceChangeContent(
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = priceChangeState.valueInPercent,
|
targetState = priceChangeState.valueInPercent,
|
||||||
label = "Update the price text",
|
label = "Update the price text",
|
||||||
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
|
modifier = modifier.padding(start = TangemTheme.dimens2.x0_5),
|
||||||
) { animatedText ->
|
) { animatedText ->
|
||||||
Text(
|
Text(
|
||||||
text = animatedText,
|
text = animatedText,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.tangem.core.ui.test
|
||||||
|
|
||||||
|
object TokenMarketBlockTestTags {
|
||||||
|
const val BLOCK = "TOKEN_MARKET_BLOCK"
|
||||||
|
const val TITLE = "TOKEN_MARKET_BLOCK_TITLE"
|
||||||
|
const val PRICE = "TOKEN_MARKET_BLOCK_PRICE"
|
||||||
|
const val PRICE_CHANGE = "TOKEN_MARKET_BLOCK_PRICE_CHANGE"
|
||||||
|
const val CHART = "TOKEN_MARKET_BLOCK_CHART"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.core.ui.test
|
||||||
|
|
||||||
|
object TokenReceiveAssetsBottomSheetTestTags {
|
||||||
|
const val ADDRESSES_PAGER = "TOKEN_RECEIVE_ASSETS_ADDRESSES_PAGER"
|
||||||
|
}
|
||||||
|
|
@ -9,4 +9,5 @@ object TransactionHistoryItemTestTags {
|
||||||
/** Status is conveyed visually (icon + color), so it is exposed via a status-suffixed tag. */
|
/** Status is conveyed visually (icon + color), so it is exposed via a status-suffixed tag. */
|
||||||
const val STATUS_PREFIX = "TRANSACTION_HISTORY_ITEM_STATUS_"
|
const val STATUS_PREFIX = "TRANSACTION_HISTORY_ITEM_STATUS_"
|
||||||
const val STATUS_CONFIRMED = STATUS_PREFIX + "CONFIRMED"
|
const val STATUS_CONFIRMED = STATUS_PREFIX + "CONFIRMED"
|
||||||
|
const val STATUS_UNCONFIRMED = STATUS_PREFIX + "UNCONFIRMED"
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
|
@ -36,6 +37,7 @@ import com.tangem.core.ui.ds.row.token.internal.TokenRowPriceChangeContent
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.test.TokenMarketBlockTestTags
|
||||||
import com.tangem.core.ui.R as CoreR
|
import com.tangem.core.ui.R as CoreR
|
||||||
import com.tangem.features.markets.impl.R
|
import com.tangem.features.markets.impl.R
|
||||||
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
|
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
|
||||||
|
|
@ -53,13 +55,16 @@ internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier:
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
|
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
|
||||||
.background(TangemTheme.colors2.surface.level3)
|
.background(TangemTheme.colors2.surface.level3)
|
||||||
.clickable(onClick = tokenMarketBlockUM.onClick),
|
.clickable(onClick = tokenMarketBlockUM.onClick)
|
||||||
|
.testTag(TokenMarketBlockTestTags.BLOCK),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResourceSafe(id = R.string.markets_common_market_price),
|
text = stringResourceSafe(id = R.string.markets_common_market_price),
|
||||||
style = TangemTheme.typography2.bodySemibold16,
|
style = TangemTheme.typography2.bodySemibold16,
|
||||||
color = TangemTheme.colors2.text.neutral.primary,
|
color = TangemTheme.colors2.text.neutral.primary,
|
||||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
modifier = Modifier
|
||||||
|
.layoutId(TangemRowLayoutId.START_TOP)
|
||||||
|
.testTag(TokenMarketBlockTestTags.TITLE),
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -72,6 +77,7 @@ internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier:
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = TangemTheme.typography2.captionMedium12,
|
style = TangemTheme.typography2.captionMedium12,
|
||||||
color = TangemTheme.colors2.text.neutral.primary,
|
color = TangemTheme.colors2.text.neutral.primary,
|
||||||
|
modifier = Modifier.testTag(TokenMarketBlockTestTags.PRICE),
|
||||||
)
|
)
|
||||||
TokenRowPriceChangeContent(
|
TokenRowPriceChangeContent(
|
||||||
priceChangeState = PriceChangeState.Content(
|
priceChangeState = PriceChangeState.Content(
|
||||||
|
|
@ -79,11 +85,14 @@ internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier:
|
||||||
valueInPercent = tokenMarketBlockUM.h24Percent.orEmpty(),
|
valueInPercent = tokenMarketBlockUM.h24Percent.orEmpty(),
|
||||||
),
|
),
|
||||||
isFlickering = false,
|
isFlickering = false,
|
||||||
|
modifier = Modifier.testTag(TokenMarketBlockTestTags.PRICE_CHANGE),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
|
modifier = Modifier
|
||||||
|
.layoutId(TangemRowLayoutId.END_BOTTOM)
|
||||||
|
.testTag(TokenMarketBlockTestTags.CHART),
|
||||||
) {
|
) {
|
||||||
val chartModifier = Modifier.requiredSize(width = ChartWidth, height = ChartHeight)
|
val chartModifier = Modifier.requiredSize(width = ChartWidth, height = ChartHeight)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -50,6 +51,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
import com.tangem.core.ui.res.generated.icons.Icons
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
import com.tangem.core.ui.res.generated.icons.ic_copy_24
|
import com.tangem.core.ui.res.generated.icons.ic_copy_24
|
||||||
import com.tangem.core.ui.res.generated.icons.ic_share_android_24
|
import com.tangem.core.ui.res.generated.icons.ic_share_android_24
|
||||||
|
import com.tangem.core.ui.test.TokenReceiveAssetsBottomSheetTestTags
|
||||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||||
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
|
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -175,6 +177,7 @@ private fun PrimaryAddressesItems(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
pageSpacing = 16.dp,
|
pageSpacing = 16.dp,
|
||||||
|
modifier = Modifier.testTag(TokenReceiveAssetsBottomSheetTestTags.ADDRESSES_PAGER),
|
||||||
) { page ->
|
) { page ->
|
||||||
val address = addresses[page]
|
val address = addresses[page]
|
||||||
AddressItem(
|
AddressItem(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue