Updated on 2026-08-14
This commit is contained in:
commit
6f82ba15b7
25 changed files with 699 additions and 68 deletions
135
.claude/skills/write-ui-test/SKILL.md
Normal file
135
.claude/skills/write-ui-test/SKILL.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
name: write-ui-test
|
||||
description: Write a Kaspresso/Compose instrumentation UI test for the Tangem Android app following project conventions — test class shape, page-object locations, Allure step naming, WireMock scenario setup, synchronization, and meaningful assertions. Covers known Compose traps (PullToRefreshBox swipe, TangemHoldToConfirmButton, Decompose lifecycle, hot-wallet access code) and the build/run/debug flow. Use when the user asks to write, add, port, or fix an instrumentation / androidTest / UI test, a page object, or a test scenario ("напиши UI-тест", "добавь инструментальный тест", "напиши тест в androidTest", "page object", "автотест на экран").
|
||||
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent
|
||||
argument-hint: [screen/flow or TC# to cover, e.g. "TangemPay freeze card"]
|
||||
---
|
||||
|
||||
Write an instrumentation (androidTest) UI test for the Tangem Android app. These conventions are
|
||||
enforced by reviewers (tnagmetulla, dpodoynikov) — applying them up front skips a review round.
|
||||
|
||||
This is an **interactive** skill: if scope is ambiguous (which screen, which flow, what the final
|
||||
assertion should verify), ask before writing. Do not invent UI text or test tags — read the real
|
||||
production source and reuse existing patterns.
|
||||
|
||||
## When to use
|
||||
|
||||
Use for instrumented UI tests under `app/src/androidTest/` (Kaspresso + Kakao-Compose), page objects,
|
||||
and test scenarios. **Not** for JVM/Robolectric unit tests (`testDebugUnitTest`) — those follow a
|
||||
different setup.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Clarify scope.** Which screen/flow, which Allure TC#, and what the *final assertion* verifies.
|
||||
Ask if any of these is unclear.
|
||||
2. **Find an existing sibling test to mirror.** Grep `app/src/androidTest/` for a test on a similar
|
||||
screen (e.g. `SendViaSwapTest`). Match its structure rather than inventing one. Read the real
|
||||
production composable to get the actual `testTag`s and string resources — never guess UI text.
|
||||
3. **Locate / extend page objects** in `com/tangem/screens/` (see Locations). Add new ones there,
|
||||
never inside the scenario or test file.
|
||||
4. **Set up WireMock scenarios** in the *test body* if the flow depends on backend state
|
||||
(see `reference/running-and-debugging.md`).
|
||||
5. **Write the test** per Conventions below.
|
||||
6. **Build BOTH APKs, install, run, and classify the result** correctly — Allure post-run hook
|
||||
failures are not test failures (see `reference/running-and-debugging.md`).
|
||||
|
||||
## Porting a test from iOS
|
||||
|
||||
When the user asks to **port** an iOS test to Android:
|
||||
|
||||
- **Default to the sibling iOS repo `../tangem-app-ios/`** (next to `tangem-app-android`). If that path
|
||||
doesn't exist, **ask the user** where the iOS repo is — don't guess.
|
||||
- iOS UI tests live under `TangemUITests/`; look there for the source test, its page objects
|
||||
(`*Screen`), and accessibility identifiers (`*AccessibilityIdentifiers`).
|
||||
- Port the *intent and steps*, not the API. Map the iOS stack to the Android one:
|
||||
XCUITest/accessibility identifiers → Compose `testTag`; iOS `*Screen` page objects → Kotlin page
|
||||
objects in `com/tangem/screens/`; XCTest assertions → Kaspresso/Truth assertions. Re-derive the real
|
||||
Android `testTag`s and string resources from production source — never reuse iOS identifier strings.
|
||||
- **Card/wallet mock mapping — where iOS uses `wallet2`, Android uses the default `Wallet`**
|
||||
(`openMainScreen()` with no `productType` → `ProductType.Wallet`). Do NOT port iOS `.wallet2` to
|
||||
`ProductType.Wallet2`. Other cards map directly: iOS `.twin` → `ProductType.Twins`, `.xrpNote` →
|
||||
`ProductType.Note`, `.four12` → `Firmware412MockContent` (via the `mockContent` param). `ProductType.Wallet2`
|
||||
exists but is a distinct Wallet-2.0-card case, not the iOS-`wallet2` analog.
|
||||
- **A wallet has no balance until you sync.** The default `Wallet` mock starts with missing derivations
|
||||
("Some addresses are missing"); the fiat balance shows `—` until you call `synchronizeAddresses()` after
|
||||
`openMainScreen()` (mirror `TotalBalanceUpdateTest`). Any test asserting a balance/fiat-equivalent must
|
||||
sync first, then `waitUntil` the value loads — balances re-load asynchronously (e.g. after an app-currency
|
||||
change the equivalent repaints with a delay).
|
||||
- The WireMock scenarios are usually shared across platforms, but the branch may differ
|
||||
(see `reference/running-and-debugging.md`).
|
||||
|
||||
## Conventions (must-follow)
|
||||
|
||||
### Test class shape
|
||||
|
||||
- **Scenario state setup goes in the test body**, not inside the open-the-feature helper. Each test
|
||||
starts with explicit `step("Set WireMock scenario '$name' to '$state'") { setWireMockScenarioState(name, state) }`
|
||||
calls, then calls a thin helper (e.g. `openTangemPay()`) that only opens the screen. Mirror the
|
||||
`SendViaSwapTest` pattern.
|
||||
- **Open-the-feature helpers stay thin** — no scenarios-as-parameters, no scenario juggling inside.
|
||||
- **Every scenario name + state is a `val`** at the top of the test method. Reviewers reject magic
|
||||
strings inside `step(...)`.
|
||||
- **Each click is its own** `step("Click on '$x' button")`. Combining clicks into one step hides which
|
||||
click failed in the Allure report.
|
||||
- **Step naming**: `Click on 'X' button` (not "Tap X"); `Assert <thing> is displayed` (not "Check X
|
||||
visible"). Keep it consistent with the existing suite.
|
||||
- **No conditional `if (foo.isDisplayedSafely()) foo.performClick()`** for elements that are
|
||||
deterministically present after `pm clear` — the `if` is dead code. Use a straight `performClick()`.
|
||||
|
||||
### Locations
|
||||
|
||||
| What | Where |
|
||||
|------|-------|
|
||||
| Page objects | `app/src/androidTest/kotlin/com/tangem/screens/…` — **always** |
|
||||
| Common test helpers | `app/src/androidTest/kotlin/com/tangem/common/utils/` |
|
||||
| Feature scenarios | `app/src/androidTest/kotlin/com/tangem/scenarios/` |
|
||||
| Cross-feature helper (e.g. `confirmSwapByHolding`) | the **feature-of-origin** scenarios file (e.g. `SwapScenarios.kt`), not the consumer's |
|
||||
|
||||
Scenario files orchestrate flows; they must not define page objects or duplicate generic helpers.
|
||||
|
||||
### Strings
|
||||
|
||||
- **No hardcoded UI text** in matchers. Use `getResourceString(R.string.foo)` from
|
||||
`com.tangem.core.res.R` or `com.tangem.core.ui.R`. The Detekt rule `UnsafeStringResourceUsage`
|
||||
enforces this for production code; reviewers extend it to test code informally.
|
||||
|
||||
### Allure IDs
|
||||
|
||||
- **Every test method gets its own unique `@AllureId`.** Never reuse the same id across two test methods —
|
||||
not even for two variants of one manual case. If a manual case is split into multiple automated tests
|
||||
(e.g. a positive and a negative variant), each test must be linked to its own distinct Allure case/id.
|
||||
(Note: iOS sometimes shares one id across methods — do NOT mirror that here.)
|
||||
|
||||
### Assertions
|
||||
|
||||
- **Never** use Kotlin's built-in `assert(...)` — Android instrumentation runs don't enable JVM
|
||||
assertions, so `assert(false)` is a silent no-op. Use Truth / JUnit / Kaspresso / Kakao assertions.
|
||||
- **Clipboard checks**: `assertClipboardTextEquals(expected, context)` from `common/utils/ClipboardUtils.kt`.
|
||||
Read displayed text via `KNode.extractText()` first if you need to compare against UI state.
|
||||
- **Every test ends with a meaningful assertion**, not just an action. A test whose last step is
|
||||
"Click Submit" without verifying the result gets rejected.
|
||||
|
||||
### Waits and synchronization
|
||||
|
||||
- **Manual polls are banned** (`onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()` in a loop). Use:
|
||||
- `composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMillis)` — wait for one thing to appear.
|
||||
- `composeTestRule.waitUntil(timeout) { runCatching { someAssertion() }.isSuccess }` — wait until an
|
||||
action no longer throws.
|
||||
- `composeTestRule.waitUntil(timeout) { matcherA exists || matcherB exists }` — the either/or case.
|
||||
- **`flakySafely(timeout)`** (Kaspresso) is reachable only from `TestCase` subclasses, NOT from
|
||||
extension functions on `BaseTestCase`. In extension code use the `waitUntil` variants above.
|
||||
|
||||
### Comment hygiene
|
||||
|
||||
This repo enforces "no comments unless WHY is non-obvious", in test code too. One line max, WHY-only —
|
||||
encode a hidden constraint, not what the code does. Example that earns its keep:
|
||||
`// Create+confirm screens share ACCESS_CODE_INPUT — gate on confirm-screen title.`
|
||||
Delete anything explaining WHAT a step does.
|
||||
|
||||
## Reference docs
|
||||
|
||||
- **`reference/compose-traps.md`** — read when the screen uses `PullToRefreshBox`,
|
||||
`TangemHoldToConfirmButton`, a Decompose model that fetches in `init {}`, or a hot-wallet import with
|
||||
an access code. These have silent failure modes that look like passing tests.
|
||||
- **`reference/running-and-debugging.md`** — read when building, installing, running a single test,
|
||||
interpreting CLI/Allure output, using `@Ignore`, or driving WireMock scenarios.
|
||||
96
.claude/skills/write-ui-test/reference/compose-traps.md
Normal file
96
.claude/skills/write-ui-test/reference/compose-traps.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Compose UI test traps
|
||||
|
||||
Each of these has a **silent** failure mode: the gesture/action appears to run, the test stays green
|
||||
(or fails for the wrong reason), but the intended behavior never fired. Diagnose with logcat network
|
||||
traces or a semantics-tree snapshot, not by visually watching the swipe.
|
||||
|
||||
## Material3 `PullToRefreshBox` + UiAutomator swipe = silent no-op
|
||||
|
||||
`androidx.compose.material3.pulltorefresh.PullToRefreshBox` reacts to overscroll deltas via Compose's
|
||||
`NestedScrollConnection` from the inner `LazyColumn`. UiAutomator's `device.swipe(x1,y1,x2,y2,steps)`
|
||||
dispatches platform `MotionEvent`s; the `LazyColumn` receives them as an ordinary scroll, never
|
||||
produces overscroll, and `onRefresh` never fires — regardless of `steps=30` (fling) or `steps=1000`
|
||||
(slow drag). Confirmed by `NetworkLogs`: zero refresh calls after the UiAutomator swipe, vs. one
|
||||
immediate call via the Compose Test API.
|
||||
|
||||
**Use the Compose Test API:**
|
||||
|
||||
```kotlin
|
||||
composeTestRule.onNode(hasTestTag(SOME_TAG_INSIDE_THE_BOX))
|
||||
.performTouchInput {
|
||||
swipeDown(startY = 0f, endY = visibleSize.height.toFloat() * 6f, durationMillis = 800)
|
||||
}
|
||||
```
|
||||
|
||||
The shared `pullToRefresh()` in `common/extensions/UiDeviceExt.kt` is UiAutomator-based and works for
|
||||
*some* screens (a different refresh container), but **not** for Material3 `PullToRefreshBox`. When
|
||||
porting a test, verify with a logcat network trace, not visual inspection.
|
||||
|
||||
## `TangemHoldToConfirmButton` semantics are minimal
|
||||
|
||||
The component exposes ONLY `TestTag`, `IsContainer`, `Shape` in Compose semantics — no `Disabled`,
|
||||
`Role`, or `OnClick`. `assertIsEnabled()` / `assertHasClickAction()` are useless on it.
|
||||
|
||||
`Modifier.holdToConfirmGestures(enabled, ...)` early-returns from `pointerInput` when `enabled=false`,
|
||||
so the hold gesture is silently swallowed: the button looks fine, the user holds, nothing happens,
|
||||
`onConfirm` never fires.
|
||||
|
||||
**Diagnose "silently disabled" from a test:**
|
||||
1. Snapshot the Compose semantics tree before the hold.
|
||||
2. Perform the hold: `performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }`.
|
||||
3. Snapshot again — byte-identical trees mean `onConfirm` didn't run.
|
||||
4. Or check WireMock request stats for the downstream API call expected after `onConfirm`.
|
||||
|
||||
## `assertTextContains(x)` defaults to exact-segment match, not substring
|
||||
|
||||
`SemanticsNodeInteraction.assertTextContains(value, substring = false, ignoreCase = false)` defaults to
|
||||
`substring = false` — it asserts that some text **segment of the node equals `value` exactly**. Matching
|
||||
a symbol or fragment inside a larger string (e.g. `"€"` against a balance `"€108,474.21"`) silently
|
||||
never matches and times out inside a `waitUntil`. Pass `substring = true`:
|
||||
|
||||
```kotlin
|
||||
totalBalanceText.assertTextContains("€", substring = true)
|
||||
```
|
||||
|
||||
Reference tests that pass the *full* string (`assertTextContains("€108,474.21")`) work with the default,
|
||||
which is why a copy-pasted matcher can mislead.
|
||||
|
||||
## Kakao-Compose `child { }`: use DSL matchers, not raw Compose matcher aliases
|
||||
|
||||
Inside a `child { … }` / `ComposeScreen` element builder, call the DSL methods (`hasText(...)`,
|
||||
`hasTestTag(...)`, `hasAnyDescendant(...)`). A common alias is `import androidx.compose.ui.test.hasText
|
||||
as withText` — but `withText(x)` as a **bare statement** inside the builder just creates a
|
||||
`SemanticsMatcher` and discards it, registering nothing → `ViewBuilderException: Please set matchers for
|
||||
your Element!` at run time. `withText`/raw matchers are only valid as *arguments* to a DSL method
|
||||
(`hasAnyDescendant(withText(name))`), never as a standalone line.
|
||||
|
||||
```kotlin
|
||||
// WRONG — no matcher registered
|
||||
fun walletNameValue(name: String) = child { withText(name); useUnmergedTree = true }
|
||||
// RIGHT
|
||||
fun walletNameValue(name: String) = child { hasText(name); useUnmergedTree = true }
|
||||
```
|
||||
|
||||
## Decompose model lifecycle vs. data refresh
|
||||
|
||||
Models (e.g. `TangemPayDetailsModel`) call data fetches from `init {}`, NOT on `ON_RESUME`. Returning
|
||||
to a screen via `router::pop` does NOT re-fetch. A test that switches WireMock scenarios between an
|
||||
action and the assertion MUST explicitly trigger a refresh on the now-frontmost screen — otherwise the
|
||||
stale in-memory data wins.
|
||||
|
||||
## Hot wallet imports with access code
|
||||
|
||||
- `openMainScreenWithExistingHotWallet(seedPhrase, accessCode: String = "")` in `BaseScenarios.kt`
|
||||
handles both flows via the optional param — DO NOT introduce a parallel `importHotWalletWithAccessCode`.
|
||||
- Access-code **create** and **confirm** screens share the same `ACCESS_CODE_INPUT` testTag. Gate the
|
||||
confirm-screen action on the confirm-screen's unique title:
|
||||
|
||||
```kotlin
|
||||
composeTestRule.waitUntilAtLeastOneExists(
|
||||
hasText(getResourceString(CoreUiR.string.access_code_confirm_title)),
|
||||
timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG,
|
||||
)
|
||||
```
|
||||
|
||||
- Tangem Pay eligibility (`PaeraCustomer`) rejects hot wallets with `authType=NoPassword` — those tests
|
||||
must use the access-code path.
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# Building, running, and debugging instrumentation tests
|
||||
|
||||
## Both APKs matter
|
||||
|
||||
Instrumentation tests need TWO APKs:
|
||||
|
||||
- `:app:assembleGoogleMocked` → `app-google-mocked.apk` — production code under test
|
||||
- `:app:assembleGoogleMockedAndroidTest` → `app-google-mocked-androidTest.apk` — the test code
|
||||
|
||||
If you change production code and rebuild only the test APK, **the installed main APK stays old** and
|
||||
your fix doesn't take effect. Symptom: "the fix doesn't help" — except it does, you just ran the
|
||||
unfixed build.
|
||||
|
||||
```bash
|
||||
# Build both
|
||||
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
|
||||
# Install each
|
||||
adb install -r -t <path-to-app-google-mocked.apk>
|
||||
adb install -r -t <path-to-app-google-mocked-androidTest.apk>
|
||||
```
|
||||
|
||||
## Run a single test (manual)
|
||||
|
||||
```bash
|
||||
adb shell pm clear com.tangem.wallet.mocked
|
||||
curl -X POST http://localhost:8081/__admin/scenarios/reset
|
||||
adb shell am instrument -w \
|
||||
-e class "com.tangem.tests.tangempay.TangemPayTest#freezeUnfreezeCard_TogglesCardState" \
|
||||
com.tangem.wallet.mocked.test/com.tangem.common.HiltTestRunner
|
||||
```
|
||||
|
||||
## Classify the result — Allure noise vs. real failure
|
||||
|
||||
After `pm clear`, `/data/user/0/<pkg>/files/original_screenshots` doesn't exist →
|
||||
`AllureResultsHack.testRunFinished` throws `NoSuchFileException` → reported as
|
||||
`Tests run: 1, Failures: 1` with a stack trace starting at `AllureResultsHack`. **This is a post-run
|
||||
hook failure, NOT a test logic failure.**
|
||||
|
||||
Distinguish:
|
||||
- First stack frame is `AllureResultsHack.testRunFinished` → infra hook noise; ignore it.
|
||||
- Kaspresso step logs show all `SUCCEED` for steps 1..N → the test passed.
|
||||
- A REAL failure shows `java.lang.AssertionError` inside the test's own classes
|
||||
(e.g. `at com.tangem.tests.X.foo$lambda…`). When auto-classifying CLI output, key off the presence
|
||||
of `java.lang.AssertionError` vs. only `original_screenshots`.
|
||||
|
||||
## `@Ignore` on instrumentation tests
|
||||
|
||||
- Pattern: `@Ignore("https://tangem.atlassian.net/browse/AND-XXXXX")` above `@Test`.
|
||||
- When ignored, `am instrument -e class …` reports `OK (0 tests)` with `Tests run: 0`
|
||||
(NOT `Skipped: 1`). Auto-detection should match the zero-test count.
|
||||
|
||||
## WireMock cheatsheet
|
||||
|
||||
Local override is detected; otherwise hits remote. Default local port: `8081`.
|
||||
|
||||
```bash
|
||||
# Set a scenario state — PUT, not POST
|
||||
curl -X PUT http://localhost:8081/__admin/scenarios/<name>/state \
|
||||
-H "Content-Type: application/json" -d '{"state":"<state>"}'
|
||||
|
||||
# Reset all scenarios
|
||||
curl -X POST http://localhost:8081/__admin/scenarios/reset
|
||||
|
||||
# Inspect
|
||||
curl http://localhost:8081/__admin/mappings | jq
|
||||
curl http://localhost:8081/__admin/scenarios | jq '.scenarios[] | {name, state}'
|
||||
```
|
||||
|
||||
- Mocks repo: default to the sibling directory `../tangem-api-mocks/` (i.e. next to
|
||||
`tangem-app-android`). If that path doesn't exist, **ask the user** where the mocks repo is rather
|
||||
than guessing.
|
||||
- The repo is **branch-per-suite** — dozens of feature branches (e.g. `send-via-swap-p1`,
|
||||
`account-creation`, `swap-express-mocks`, `android-tangem-pay-mocks`). There is no universal
|
||||
default branch; check out the one the suite under test expects. If it's unclear which branch holds
|
||||
the mappings for your flow, ask the user. Mappings live under `mocks/mappings/`, response bodies
|
||||
under `mocks/__files/`.
|
||||
- **State transitions are atomic per `requiredScenarioState`.** If a scenario defines an `AfterDeposit`
|
||||
mapping for `/customer/balance` but not `/customer/me`, a request to `/customer/me` after switching
|
||||
to `AfterDeposit` falls through. Check *both* endpoints when an "after" assertion fails.
|
||||
|
||||
## Misc
|
||||
|
||||
- `./gradlew unitTest` aggregates all debug/googleDebug + JVM-module tests — faster than per-module
|
||||
tasks for verifying a broad change (but it's for *unit* tests, not instrumentation).
|
||||
- Detekt config lives in the `tangem-android-tools` git submodule — look there before assuming a local
|
||||
`.detekt.yml`.
|
||||
- Path discipline: stay at the repo root; `cd` into the mocks repo only when needed and prefer absolute
|
||||
paths (the shell session resets cwd).
|
||||
|
|
@ -175,7 +175,10 @@ fun BaseTestCase.openDeviceSettingsScreen() {
|
|||
onDetailsScreen { walletNameButton.performClick() }
|
||||
}
|
||||
step("Click on 'Device settings' button") {
|
||||
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
|
||||
onWalletSettingsScreen {
|
||||
scrollToDeviceSettings()
|
||||
deviceSettingsButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AppCurrencySelectorScreenTestTags
|
||||
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
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class AppCurrencySelectorPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AppCurrencySelectorPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val searchActionButton: KNode = child {
|
||||
hasTestTag(AppCurrencySelectorScreenTestTags.TOP_BAR_ACTION_BUTTON)
|
||||
}
|
||||
|
||||
val searchField: KNode = child {
|
||||
hasTestTag(AppCurrencySelectorScreenTestTags.SEARCH_FIELD)
|
||||
}
|
||||
|
||||
fun currencyItem(code: String): KNode = child {
|
||||
hasTestTag(AppCurrencySelectorScreenTestTags.CURRENCY_ITEM)
|
||||
hasAnyDescendant(withText(code, substring = true))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAppCurrencySelectorScreen(function: AppCurrencySelectorPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AppSettingsScreenTestTags
|
||||
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 AppSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AppSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val currencyButton: KNode = child {
|
||||
hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -9,6 +9,7 @@ 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
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -22,17 +23,9 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasText(getResourceString(R.string.wallet_connect_title))
|
||||
}
|
||||
|
||||
private val walletBlock: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
}
|
||||
|
||||
val walletNameButton: KNode = walletBlock.child {
|
||||
hasClickAction()
|
||||
hasPosition(0)
|
||||
}
|
||||
|
||||
val scanCardButton: KNode = walletBlock.child {
|
||||
hasText(getResourceString(R.string.scan_card_settings_button))
|
||||
val walletNameButton: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val buyTangemButton: KNode = child {
|
||||
|
|
@ -58,6 +51,12 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasTestTag(DetailsScreenTestTags.VERSION_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun walletNameValue(name: String): KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
hasAnyDescendant(withText(name))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
|
||||
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -25,6 +26,12 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
hasTestTag(BaseDialogTestTags.TEXT)
|
||||
}
|
||||
|
||||
val inputField: KNode = child {
|
||||
hasSetTextAction()
|
||||
hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val cancelButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_cancel))
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
|||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||
}
|
||||
|
||||
val fiatBalance: KNode = child {
|
||||
hasTestTag(TokenDetailsScreenTestTags.BALANCE_FIAT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addFundsButton: KNode = child {
|
||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||
hasAnyDescendant(withText(getResourceString(R.string.tangempay_card_details_add_funds)))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
|
|
@ -9,6 +10,7 @@ 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
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
|
|
@ -22,6 +24,31 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
|||
hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM)
|
||||
}
|
||||
|
||||
// The Accounts section loads async and can push rows below the fold — scroll before asserting/clicking.
|
||||
private val scrollableContainer: KNode = child {
|
||||
hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToText(text: String) = scrollableContainer { performScrollToNode(withText(text)) }
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToDeviceSettings() = scrollToText(getResourceString(R.string.card_settings_title))
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToLinkMoreCards() = scrollToText(getResourceString(R.string.details_row_title_create_backup))
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToReferralProgram() = scrollToText(getResourceString(R.string.details_referral_title))
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToForgetWallet() = scrollToText(getResourceString(R.string.settings_forget_wallet))
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun scrollToRenameButton() = scrollableContainer {
|
||||
performScrollToNode(withTestTag(WalletSettingsScreenTestTags.RENAME_BUTTON))
|
||||
}
|
||||
|
||||
val linkMoreCardsButton: KNode = walletSettingsItem.child {
|
||||
hasText(getResourceString(R.string.details_row_title_create_backup))
|
||||
}
|
||||
|
|
@ -38,6 +65,16 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
|||
hasText(getResourceString(R.string.settings_forget_wallet))
|
||||
}
|
||||
|
||||
val renameWalletButton: KNode = child {
|
||||
hasTestTag(WalletSettingsScreenTestTags.RENAME_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun walletNameValue(name: String): KNode = walletSettingsItem.child {
|
||||
hasText(name)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val accountsListContainer: KNode = walletSettingsItem.child {
|
||||
hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.tests
|
||||
|
||||
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.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
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 AppCurrencyTest : BaseTestCase() {
|
||||
|
||||
@AllureId("781")
|
||||
@DisplayName("App Currency: change of equivalent")
|
||||
@Test
|
||||
fun changeAppCurrencyTest() {
|
||||
val currenciesScenario = "currencies_api"
|
||||
val appSettingsState = "AppSettings"
|
||||
val targetCurrency = "EUR"
|
||||
val targetSymbol = "€"
|
||||
val token = "Bitcoin"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) },
|
||||
).run {
|
||||
step("Set WireMock scenario '$currenciesScenario' to '$appSettingsState'") {
|
||||
setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState)
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
synchronizeAddresses()
|
||||
step("Open wallet details") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'App settings' button") {
|
||||
onDetailsScreen { appSettingsButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'App currency' button") {
|
||||
onAppSettingsScreen { currencyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on search button") {
|
||||
onAppCurrencySelectorScreen { searchActionButton.clickWithAssertion() }
|
||||
}
|
||||
step("Search currency '$targetCurrency'") {
|
||||
onAppCurrencySelectorScreen { searchField.performTextInput(targetCurrency) }
|
||||
}
|
||||
step("Click on currency '$targetCurrency'") {
|
||||
onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() }
|
||||
}
|
||||
step("Press 'Back' button to return to 'Details' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Press 'Back' button to return to 'Main' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Assert total balance contains '$targetSymbol' on 'Main' screen") {
|
||||
// Balance re-loads in the new currency async after the switch — wait for the € equivalent.
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { totalBalanceText.assertTextContains(targetSymbol, substring = true) }
|
||||
}
|
||||
}
|
||||
step("Click on token '$token'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||
}
|
||||
step("Assert token fiat balance contains '$targetSymbol'") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol, substring = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.extensions.clickWithAssertion
|
|||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Firmware412MockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
|
|
@ -13,6 +14,8 @@ import org.junit.Test
|
|||
@HiltAndroidTest
|
||||
class DetailsTest : BaseTestCase() {
|
||||
|
||||
@AllureId("836")
|
||||
@DisplayName("Details: (Wallet) fields")
|
||||
@Test
|
||||
fun walletWithoutBackupDetailsTest() =
|
||||
setupHooks().run {
|
||||
|
|
@ -46,70 +49,26 @@ class DetailsTest : BaseTestCase() {
|
|||
}
|
||||
onWalletSettingsScreen {
|
||||
step("Assert 'Link more cards' button is visible") {
|
||||
scrollToLinkMoreCards()
|
||||
linkMoreCardsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Card Settings' button is visible") {
|
||||
scrollToDeviceSettings()
|
||||
deviceSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Referral program' button is visible") {
|
||||
scrollToReferralProgram()
|
||||
referralProgramButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Forget wallet' button is visible") {
|
||||
scrollToForgetWallet()
|
||||
forgetWalletButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun wallet2DetailsTest() =
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Wallet2)
|
||||
}
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
onDetailsScreen {
|
||||
step("Assert 'Wallet connect' button is visible") {
|
||||
walletConnectButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Scan card' button is visible") {
|
||||
scanCardButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Buy Tangem card' button is visible") {
|
||||
buyTangemButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'App settings' button is visible") {
|
||||
appSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Contact support' button is visible") {
|
||||
contactSupportButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Terms or service' button is visible") {
|
||||
toSButton.assertIsDisplayed()
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
walletNameButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
onWalletSettingsScreen {
|
||||
step("Assert 'Link more cards' button does not exist") {
|
||||
linkMoreCardsButton.assertIsNotDisplayed()
|
||||
}
|
||||
step("Assert 'Card Settings' button is visible") {
|
||||
deviceSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Referral program' button is visible") {
|
||||
referralProgramButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Forget wallet' button is visible") {
|
||||
forgetWalletButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("837")
|
||||
@DisplayName("Details: (Note) fields")
|
||||
@Test
|
||||
fun noteDetailsTest() =
|
||||
setupHooks().run {
|
||||
|
|
@ -154,6 +113,76 @@ class DetailsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@AllureId("840")
|
||||
@DisplayName("Details: (Twins) fields")
|
||||
@Test
|
||||
fun twinsDetailsTest() =
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Twins, isTwinsCard = true)
|
||||
}
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
onDetailsScreen {
|
||||
step("Assert 'Wallet connect' button does not exist") {
|
||||
walletConnectButton.assertIsNotDisplayed()
|
||||
}
|
||||
step("Assert 'Buy Tangem card' button is visible") {
|
||||
buyTangemButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'App settings' button is visible") {
|
||||
appSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Contact support' button is visible") {
|
||||
contactSupportButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Terms of service' button is visible") {
|
||||
toSButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert app version is visible") {
|
||||
versionName.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("839")
|
||||
@DisplayName("Details: (v4.12) fields")
|
||||
@Test
|
||||
fun firmware412DetailsTest() =
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(mockContent = Firmware412MockContent)
|
||||
}
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
onDetailsScreen {
|
||||
step("Assert 'Wallet connect' button is visible") {
|
||||
walletConnectButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Buy Tangem card' button is visible") {
|
||||
buyTangemButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'App settings' button is visible") {
|
||||
appSettingsButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Contact support' button is visible") {
|
||||
contactSupportButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert 'Terms of service' button is visible") {
|
||||
toSButton.assertIsDisplayed()
|
||||
}
|
||||
step("Assert app version is visible") {
|
||||
versionName.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3647")
|
||||
@DisplayName("Referral program: validate screen")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.tests
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.screens.*
|
||||
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 WalletRenameTest : BaseTestCase() {
|
||||
|
||||
@AllureId("2264")
|
||||
@DisplayName("Wallet details: rename wallet")
|
||||
@Test
|
||||
fun renameWalletTest() =
|
||||
setupHooks().run {
|
||||
val newWalletName = "Tangem QA"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Open wallet details") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Rename' button") {
|
||||
onWalletSettingsScreen {
|
||||
scrollToRenameButton()
|
||||
renameWalletButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Enter new wallet name '$newWalletName'") {
|
||||
onDialog { inputField.performTextReplacement(newWalletName) }
|
||||
}
|
||||
step("Click on 'OK' button") {
|
||||
onDialog { okButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert new wallet name '$newWalletName' is displayed on 'Wallet settings' screen") {
|
||||
onWalletSettingsScreen { walletNameValue(newWalletName).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Back' button") {
|
||||
onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert new wallet name '$newWalletName' is displayed on 'Details' screen") {
|
||||
onDetailsScreen { walletNameValue(newWalletName).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Back' button") {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert new wallet name '$newWalletName' is displayed on 'Main' screen") {
|
||||
onMainScreen { walletNameText.assertTextContains(newWalletName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -29,6 +30,7 @@ import com.tangem.core.ui.event.consumedEvent
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.AppCurrencySelectorScreenTestTags
|
||||
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -123,7 +125,9 @@ private fun TopBar(
|
|||
when (state) {
|
||||
is AppCurrencySelectorState.Content -> {
|
||||
IconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size32),
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size32)
|
||||
.testTag(AppCurrencySelectorScreenTestTags.TOP_BAR_ACTION_BUTTON),
|
||||
onClick = state.onTopBarActionClick,
|
||||
) {
|
||||
val iconResId = when (state) {
|
||||
|
|
@ -157,7 +161,8 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
|
|||
|
||||
TextField(
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(focusRequester)
|
||||
.testTag(AppCurrencySelectorScreenTestTags.SEARCH_FIELD),
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
singleLine = true,
|
||||
|
|
@ -218,7 +223,7 @@ private fun CurrenciesList(
|
|||
) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
modifier = modifier.testTag(AppCurrencySelectorScreenTestTags.LAZY_LIST),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
) {
|
||||
|
|
@ -246,6 +251,7 @@ private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit,
|
|||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.testTag(AppCurrencySelectorScreenTestTags.CURRENCY_ITEM)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = LocalIndication.current,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.AppSettingsScreenTestTags
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.tap.features.details.ui.appsettings.components.*
|
||||
|
|
@ -55,7 +57,15 @@ private fun AppSettings(state: AppSettingsScreenState.Content) {
|
|||
item = item,
|
||||
)
|
||||
is Item.Button -> SettingsButtonItem(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8)
|
||||
.then(
|
||||
if (item.id == AppSettingsItemsFactory.ID_SELECT_APP_CURRENCY_BUTTON) {
|
||||
Modifier.testTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
item = item,
|
||||
)
|
||||
is Item.Switch -> SettingsSwitchItem(
|
||||
|
|
|
|||
|
|
@ -245,7 +245,8 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
|
|||
OutlineTextField(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing24)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.testTag(BaseDialogTestTags.TEXT_INPUT_FIELD),
|
||||
value = type.value,
|
||||
label = type.params.label,
|
||||
placeholder = type.params.placeholder,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object AppCurrencySelectorScreenTestTags {
|
||||
const val LAZY_LIST = "APP_CURRENCY_SELECTOR_SCREEN_LAZY_LIST"
|
||||
const val CURRENCY_ITEM = "APP_CURRENCY_SELECTOR_SCREEN_CURRENCY_ITEM"
|
||||
const val SEARCH_FIELD = "APP_CURRENCY_SELECTOR_SCREEN_SEARCH_FIELD"
|
||||
const val TOP_BAR_ACTION_BUTTON = "APP_CURRENCY_SELECTOR_SCREEN_TOP_BAR_ACTION_BUTTON"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object AppSettingsScreenTestTags {
|
||||
const val CURRENCY_BUTTON = "APP_SETTINGS_SCREEN_CURRENCY_BUTTON"
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ object BaseDialogTestTags {
|
|||
const val CONTAINER = "BASE_DIALOG_CONTAINER"
|
||||
const val TITLE = "BASE_DIALOG_TITLE"
|
||||
const val TEXT = "BASE_DIALOG_TEXT"
|
||||
const val TEXT_INPUT_FIELD = "BASE_DIALOG_TEXT_INPUT_FIELD"
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ object DetailsScreenTestTags {
|
|||
const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER"
|
||||
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
|
||||
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
|
||||
const val USER_WALLET_ITEM = "DETAILS_SCREEN_USER_WALLET_ITEM"
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ object TokenDetailsScreenTestTags {
|
|||
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
|
||||
|
||||
const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE"
|
||||
const val BALANCE_FIAT = "TOKEN_DETAILS_SCREEN_BALANCE_FIAT"
|
||||
|
||||
const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK"
|
||||
const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK"
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ object WalletSettingsScreenTestTags {
|
|||
const val ACCOUNTS_CONTAINER = "WALLET_SETTINGS_SCREEN_ACCOUNTS_CONTAINER"
|
||||
const val SCREEN_ITEM = "WALLET_SETTINGS_SCREEN_ITEM"
|
||||
const val USER_ACCOUNT_ITEM = "WALLET_SETTINGS_USER_ACCOUNT_ITEM"
|
||||
const val RENAME_BUTTON = "WALLET_SETTINGS_RENAME_BUTTON"
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -34,6 +35,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.DetailsScreenTestTags
|
||||
import com.tangem.features.details.component.UserWalletListComponent
|
||||
import com.tangem.features.details.component.preview.PreviewUserWalletListComponent
|
||||
import com.tangem.features.details.entity.UserWalletListUM
|
||||
|
|
@ -69,7 +71,9 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
|
|||
model = walletState,
|
||||
reorderableListState = reorderableListState,
|
||||
walletReorderUM = state.walletReorderUM,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(DetailsScreenTestTags.USER_WALLET_ITEM),
|
||||
)
|
||||
}
|
||||
item(key = "add_wallet_button") {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
|
@ -123,6 +125,7 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
|
|||
}
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
Text(
|
||||
modifier = Modifier.testTag(TokenDetailsScreenTestTags.BALANCE_FIAT),
|
||||
text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.titleRegular44,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier
|
|||
)
|
||||
}
|
||||
SecondarySmallButton(
|
||||
modifier = Modifier.testTag(WalletSettingsScreenTestTags.RENAME_BUTTON),
|
||||
config = SmallButtonConfig(
|
||||
isEnabled = model.isEnabled,
|
||||
text = resourceReference(R.string.common_rename),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue