From a121268ce063481d84bcc5d6fd4d27185cc25dba Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 May 2026 14:00:48 +0200 Subject: [PATCH 1/8] Updated on 2026-08-14 --- .claude/skills/write-ui-test/SKILL.md | 118 ++++++++++++++++++ .../write-ui-test/reference/compose-traps.md | 66 ++++++++++ .../reference/running-and-debugging.md | 88 +++++++++++++ .../screens/AppCurrencySelectorPageObject.kt | 30 +++++ .../tangem/screens/AppSettingsPageObject.kt | 20 +++ .../com/tangem/screens/DetailsPageObject.kt | 7 ++ .../screens/DeviceSettingsPageObject.kt | 4 + .../com/tangem/screens/DialogPageObject.kt | 5 + .../tangem/screens/SecurityModePageObject.kt | 19 +++ .../tangem/screens/TokenDetailsPageObject.kt | 5 + .../screens/WalletSettingsPageObject.kt | 10 ++ .../com/tangem/tests/AppCurrencyTest.kt | 71 +++++++++++ .../kotlin/com/tangem/tests/DetailsTest.kt | 78 +++++++++++- .../com/tangem/tests/SecurityModeTest.kt | 55 ++++++++ .../com/tangem/tests/WalletRenameTest.kt | 56 +++++++++ .../appcurrency/AppCurrencySelectorScreen.kt | 12 +- .../ui/appsettings/AppSettingsScreen.kt | 5 +- .../ui/cardsettings/CardSettingsScreen.kt | 9 +- .../ui/securitymode/SecurityModeScreen.kt | 5 +- .../com/tangem/core/ui/components/Dialogs.kt | 3 +- .../test/AppCurrencySelectorScreenTestTags.kt | 8 ++ .../core/ui/test/AppSettingsScreenTestTags.kt | 5 + .../tangem/core/ui/test/BaseDialogTestTags.kt | 1 + .../ui/test/DeviceSettingsScreenTestTags.kt | 1 + .../ui/test/SecurityModeScreenTestTags.kt | 5 + .../ui/test/TokenDetailsScreenTestTags.kt | 1 + .../ui/test/WalletSettingsScreenTestTags.kt | 1 + ...MockAwareTangemPayCardDetailsRepository.kt | 2 +- .../ui/components/TokenDetailsBalanceBlock.kt | 3 + .../walletsettings/ui/WalletSettingsScreen.kt | 1 + 30 files changed, 685 insertions(+), 9 deletions(-) create mode 100644 .claude/skills/write-ui-test/SKILL.md create mode 100644 .claude/skills/write-ui-test/reference/compose-traps.md create mode 100644 .claude/skills/write-ui-test/reference/running-and-debugging.md create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AppCurrencySelectorPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AppSettingsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/AppCurrencySelectorScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt diff --git a/.claude/skills/write-ui-test/SKILL.md b/.claude/skills/write-ui-test/SKILL.md new file mode 100644 index 0000000000..77b244bafd --- /dev/null +++ b/.claude/skills/write-ui-test/SKILL.md @@ -0,0 +1,118 @@ +--- +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. +- 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 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. + +### 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. \ No newline at end of file diff --git a/.claude/skills/write-ui-test/reference/compose-traps.md b/.claude/skills/write-ui-test/reference/compose-traps.md new file mode 100644 index 0000000000..e6e4beef19 --- /dev/null +++ b/.claude/skills/write-ui-test/reference/compose-traps.md @@ -0,0 +1,66 @@ +# 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`. + +## 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. \ No newline at end of file diff --git a/.claude/skills/write-ui-test/reference/running-and-debugging.md b/.claude/skills/write-ui-test/reference/running-and-debugging.md new file mode 100644 index 0000000000..c7e934192c --- /dev/null +++ b/.claude/skills/write-ui-test/reference/running-and-debugging.md @@ -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 +adb install -r -t +``` + +## 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//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//state \ + -H "Content-Type: application/json" -d '{"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 in `/Users/maxibello/dev/tangem-app-android`; `cd` into the mocks repo only when + needed and prefer absolute paths (the shell session resets cwd). \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AppCurrencySelectorPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AppCurrencySelectorPageObject.kt new file mode 100644 index 0000000000..11b69d0e53 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AppCurrencySelectorPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AppSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AppSettingsPageObject.kt new file mode 100644 index 0000000000..240f3470e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AppSettingsPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { + + val currencyButton: KNode = child { + hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 69a1652732..7c7a8635cd 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { @@ -58,6 +59,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) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt index 7822f4b34e..b59290c76e 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt @@ -46,6 +46,10 @@ class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi useUnmergedTree = true } + val securityModeRow: KNode = child { + hasTestTag(DeviceSettingsScreenTestTags.SECURITY_MODE_ROW) + } + fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child { hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE) useUnmergedTree = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 87e72b5b05..93d6b19b64 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -25,6 +25,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(BaseDialogTestTags.TEXT) } + val inputField: KNode = child { + hasTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD) + useUnmergedTree = true + } + val cancelButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_cancel)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt new file mode 100644 index 0000000000..5d9e137e3d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt @@ -0,0 +1,19 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SecurityModeScreenTestTags +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 SecurityModePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(SecurityModeScreenTestTags.SCREEN_CONTAINER) + } +} + +internal fun BaseTestCase.onSecurityModeScreen(function: SecurityModePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 4996f7db80..6a94437811 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -85,6 +85,11 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE) } + val fiatBalance: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.BALANCE_FIAT) + useUnmergedTree = true + } + private val horizontalActionChips = KLazyListNode( semanticsProvider = semanticsProvider, viewBuilderAction = { hasTestTag(BaseActionButtonsBlockTestTags.HORIZONTAL_ACTION_CHIPS) }, diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index 5b84dda406..0babf5f29f 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -38,6 +38,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 { + withText(name) + useUnmergedTree = true + } + val accountsListContainer: KNode = walletSettingsItem.child { hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt new file mode 100644 index 0000000000..d1f2559169 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt @@ -0,0 +1,71 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.domain.models.scan.ProductType +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 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 = "Polygon" + + setupHooks( + additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) }, + ).run { + step("Set WireMock scenario '$currenciesScenario' to '$appSettingsState'") { + setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState) + } + step("Open 'Main Screen'") { + openMainScreen(productType = ProductType.Wallet2) + } + 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("Return to 'Main' screen") { + device.uiDevice.pressBack() + device.uiDevice.pressBack() + waitForIdle() + } + step("Assert total balance contains '$targetSymbol' on 'Main' screen") { + onMainScreen { totalBalanceText.assertTextContains(targetSymbol) } + } + step("Click on token '$token'") { + onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() } + } + step("Assert token fiat balance contains '$targetSymbol'") { + onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 7b980f10d1..3de6e2e1c6 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -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 { @@ -60,7 +63,8 @@ class DetailsTest : BaseTestCase() { } } - // @Test + @DisplayName("Details: (Wallet 2.0) fields") + @Test fun wallet2DetailsTest() = setupHooks().run { step("Open 'Main Screen'") { @@ -110,6 +114,8 @@ class DetailsTest : BaseTestCase() { } } + @AllureId("837") + @DisplayName("Details: (Note) fields") @Test fun noteDetailsTest() = setupHooks().run { @@ -154,6 +160,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 diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt new file mode 100644 index 0000000000..ac7d5fe013 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt @@ -0,0 +1,55 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.domain.models.scan.ProductType +import com.tangem.scenarios.openDeviceSettingsScreen +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 SecurityModeTest : BaseTestCase() { + + @AllureId("2267") + @DisplayName("Security Mode: Twin card opens the section") + @Test + fun twinSecurityModeOpensTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(productType = ProductType.Twins, isTwinsCard = true) + } + openDeviceSettingsScreen() + step("Click on 'Scan card or ring' button") { + onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } + } + step("Assert 'Security Mode' row is enabled") { + onDeviceSettingsScreen { securityModeRow.assertIsEnabled() } + } + step("Click on 'Security Mode' row") { + onDeviceSettingsScreen { securityModeRow.clickWithAssertion() } + } + step("Assert 'Security Mode' screen is displayed") { + onSecurityModeScreen { screenContainer.assertIsDisplayed() } + } + } + + @DisplayName("Security Mode: other cards cannot open the section") + @Test + fun walletSecurityModeDisabledTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + openDeviceSettingsScreen() + step("Click on 'Scan card or ring' button") { + onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } + } + step("Assert 'Security Mode' row is not clickable") { + onDeviceSettingsScreen { securityModeRow.assertIsNotEnabled() } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt new file mode 100644 index 0000000000..7fb5933c60 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt @@ -0,0 +1,56 @@ +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 { 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) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index 06eda3e12c..34627ab203 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 8ffb314316..83d745be03 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -10,6 +10,7 @@ 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 @@ -55,7 +56,9 @@ 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) + .testTag(item.id), item = item, ) is Item.Switch -> SettingsSwitchItem( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 7cffbf079d..7ab076f209 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -125,7 +125,7 @@ private fun ScanCardContent(onScanCardClick: () -> Unit) { } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @Composable private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return @@ -156,6 +156,13 @@ private fun CardSettings(state: CardSettingsScreenState) { Column( modifier = Modifier .fillMaxWidth() + .then( + if (cardInfo is CardInfo.SecurityMode) { + Modifier.testTag(DeviceSettingsScreenTestTags.SECURITY_MODE_ROW) + } else { + Modifier + }, + ) .clickable( enabled = cardInfo.isClickable, onClick = { state.onElementClick(cardInfo) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt index 981cd1c76a..c678c825da 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -5,9 +5,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.test.SecurityModeScreenTestTags import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement @@ -35,7 +37,8 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) { modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(bottom = 28.dp), + .padding(bottom = 28.dp) + .testTag(SecurityModeScreenTestTags.SCREEN_CONTAINER), verticalArrangement = Arrangement.SpaceBetween, ) { ScreenTitle(titleRes = R.string.card_settings_security_mode, Modifier.padding(bottom = 36.dp)) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index bf788f49a2..91da8dacd4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppCurrencySelectorScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppCurrencySelectorScreenTestTags.kt new file mode 100644 index 0000000000..06ced9103b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppCurrencySelectorScreenTestTags.kt @@ -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" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt new file mode 100644 index 0000000000..8dfc28d4b2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object AppSettingsScreenTestTags { + const val CURRENCY_BUTTON = "select_app_currency_button" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt index 9ed0a4fbd6..ebaeb46423 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseDialogTestTags.kt @@ -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" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt index a995632d4f..a891c43e85 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt @@ -5,4 +5,5 @@ object DeviceSettingsScreenTestTags { const val IMAGE_BLOCK = "DEVICE_SETTINGS_SCREEN_IMAGE_BLOCK" const val ITEM_TITLE = "DEVICE_SETTINGS_SCREEN_ITEM_TITLE" const val ITEM_SUBTITLE = "DEVICE_SETTINGS_SCREEN_ITEM_SUBTITLE" + const val SECURITY_MODE_ROW = "DEVICE_SETTINGS_SCREEN_SECURITY_MODE_ROW" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt new file mode 100644 index 0000000000..6c4c3753b6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object SecurityModeScreenTestTags { + const val SCREEN_CONTAINER = "SECURITY_MODE_SCREEN_CONTAINER" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index 9079c600f0..26e0a85af7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -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" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt index 15b8847280..d3f23d607e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt @@ -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" } \ No newline at end of file diff --git a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt index 17a322804b..b57dd0ca8c 100644 --- a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt +++ b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt @@ -13,7 +13,7 @@ import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository -import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.models.pay.TangemPayCardFrozenState import kotlinx.coroutines.flow.Flow import javax.inject.Inject import javax.inject.Singleton diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ad37bfa321..6592a5416a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -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, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 0b600daa13..9282b02b5b 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -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), From 2110b5f5413bb9800e828465d5769f4453a011db Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 09:12:45 +0200 Subject: [PATCH 2/8] Updated on 2026-08-14 --- .claude/skills/write-ui-test/SKILL.md | 10 ++++ .../com/tangem/scenarios/BaseScenarios.kt | 5 +- .../com/tangem/screens/DetailsPageObject.kt | 16 ++---- .../com/tangem/screens/DialogPageObject.kt | 5 +- .../screens/WalletSettingsPageObject.kt | 27 +++++++++ .../com/tangem/tests/AppCurrencyTest.kt | 31 ++++++++--- .../kotlin/com/tangem/tests/DetailsTest.kt | 55 ++----------------- .../com/tangem/tests/WalletRenameTest.kt | 5 +- .../core/ui/test/DetailsScreenTestTags.kt | 1 + .../details/ui/UserWalletListBlock.kt | 6 +- 10 files changed, 87 insertions(+), 74 deletions(-) diff --git a/.claude/skills/write-ui-test/SKILL.md b/.claude/skills/write-ui-test/SKILL.md index 77b244bafd..a0322d94b3 100644 --- a/.claude/skills/write-ui-test/SKILL.md +++ b/.claude/skills/write-ui-test/SKILL.md @@ -45,6 +45,16 @@ When the user asks to **port** an iOS test to Android: 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`). diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 911b7526d1..272f804ed8 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -175,7 +175,10 @@ fun BaseTestCase.openDeviceSettingsScreen() { onDetailsScreen { walletNameButton.performClick() } } step("Click on 'Device settings' button") { - onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() } + onWalletSettingsScreen { + scrollToDeviceSettings() + deviceSettingsButton.clickWithAssertion() + } } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 7c7a8635cd..4281531d5b 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -23,17 +23,11 @@ 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)) + // Match the wallet row by its own tag (not position-0 clickable, which races with the async-loading + // "Add Wallet" button and otherwise triggers a re-scan → "already saved" dialog). + val walletNameButton: KNode = child { + hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM) + useUnmergedTree = true } val buyTangemButton: KNode = child { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 93d6b19b64..00e3613039 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -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(semanticsProvider = semanticsProvider) { @@ -25,8 +26,10 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(BaseDialogTestTags.TEXT) } + // Tag is on the OutlineTextField wrapper; the editable node is its descendant with a SetText action. val inputField: KNode = child { - hasTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD) + hasSetTextAction() + hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD)) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index 0babf5f29f..5a014f01f9 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -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)) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt index d1f2559169..97730e72e3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt @@ -1,11 +1,13 @@ 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.extensions.isDisplayedSafely import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState -import com.tangem.domain.models.scan.ProductType 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 @@ -23,7 +25,7 @@ class AppCurrencyTest : BaseTestCase() { val appSettingsState = "AppSettings" val targetCurrency = "EUR" val targetSymbol = "€" - val token = "Polygon" + val token = "Bitcoin" setupHooks( additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) }, @@ -32,8 +34,9 @@ class AppCurrencyTest : BaseTestCase() { setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState) } step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Wallet2) + openMainScreen() } + synchronizeAddresses() step("Open wallet details") { onMainScreenTopBar { moreButton.clickWithAssertion() } } @@ -53,18 +56,30 @@ class AppCurrencyTest : BaseTestCase() { onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() } } step("Return to 'Main' screen") { - device.uiDevice.pressBack() - device.uiDevice.pressBack() - waitForIdle() + var displayed = false + var attempts = 0 + while (!displayed && attempts < 4) { + onMainScreen { displayed = screenContainer.isDisplayedSafely() } + if (!displayed) { + device.uiDevice.pressBack() + waitForIdle() + attempts++ + } + } } step("Assert total balance contains '$targetSymbol' on 'Main' screen") { - onMainScreen { totalBalanceText.assertTextContains(targetSymbol) } + // Balance re-loads in the new currency async after the switch — wait for the € equivalent. + composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { + runCatching { onMainScreen { totalBalanceText.assertTextContains(targetSymbol) } }.isSuccess + } } step("Click on token '$token'") { onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() } } step("Assert token fiat balance contains '$targetSymbol'") { - onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) } + composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { + runCatching { onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) } }.isSuccess + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 3de6e2e1c6..a4b8e21a22 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -49,66 +49,19 @@ 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") { - forgetWalletButton.assertIsDisplayed() - } - } - } - - @DisplayName("Details: (Wallet 2.0) fields") - @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") { + scrollToForgetWallet() forgetWalletButton.assertIsDisplayed() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt index 7fb5933c60..2c3e1d848b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletRenameTest.kt @@ -29,7 +29,10 @@ class WalletRenameTest : BaseTestCase() { onDetailsScreen { walletNameButton.clickWithAssertion() } } step("Click on 'Rename' button") { - onWalletSettingsScreen { renameWalletButton.clickWithAssertion() } + onWalletSettingsScreen { + scrollToRenameButton() + renameWalletButton.clickWithAssertion() + } } step("Enter new wallet name '$newWalletName'") { onDialog { inputField.performTextReplacement(newWalletName) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt index bbbeb91dd5..ec91e0b88a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt @@ -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" } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 2cff0e2d16..df93d20331 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -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") { From 53c0546b5c90284cb228d80cc9b31ba189816b9f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 09:47:32 +0200 Subject: [PATCH 3/8] Updated on 2026-08-14 --- .claude/skills/write-ui-test/SKILL.md | 7 +++ .../screens/DeviceSettingsPageObject.kt | 4 -- .../tangem/screens/SecurityModePageObject.kt | 19 ------- .../com/tangem/tests/SecurityModeTest.kt | 55 ------------------- .../ui/cardsettings/CardSettingsScreen.kt | 9 +-- .../ui/securitymode/SecurityModeScreen.kt | 5 +- .../ui/test/DeviceSettingsScreenTestTags.kt | 1 - .../ui/test/SecurityModeScreenTestTags.kt | 5 -- 8 files changed, 9 insertions(+), 96 deletions(-) delete mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt diff --git a/.claude/skills/write-ui-test/SKILL.md b/.claude/skills/write-ui-test/SKILL.md index a0322d94b3..11686c0e7c 100644 --- a/.claude/skills/write-ui-test/SKILL.md +++ b/.claude/skills/write-ui-test/SKILL.md @@ -93,6 +93,13 @@ Scenario files orchestrate flows; they must not define page objects or duplicate `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 diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt index b59290c76e..7822f4b34e 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt @@ -46,10 +46,6 @@ class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi useUnmergedTree = true } - val securityModeRow: KNode = child { - hasTestTag(DeviceSettingsScreenTestTags.SECURITY_MODE_ROW) - } - fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child { hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE) useUnmergedTree = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt deleted file mode 100644 index 5d9e137e3d..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import com.tangem.common.BaseTestCase -import com.tangem.core.ui.test.SecurityModeScreenTestTags -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 SecurityModePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen(semanticsProvider = semanticsProvider) { - - val screenContainer: KNode = child { - hasTestTag(SecurityModeScreenTestTags.SCREEN_CONTAINER) - } -} - -internal fun BaseTestCase.onSecurityModeScreen(function: SecurityModePageObject.() -> Unit) = - onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt deleted file mode 100644 index ac7d5fe013..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.tests - -import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.models.scan.ProductType -import com.tangem.scenarios.openDeviceSettingsScreen -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 SecurityModeTest : BaseTestCase() { - - @AllureId("2267") - @DisplayName("Security Mode: Twin card opens the section") - @Test - fun twinSecurityModeOpensTest() = - setupHooks().run { - step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Twins, isTwinsCard = true) - } - openDeviceSettingsScreen() - step("Click on 'Scan card or ring' button") { - onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } - } - step("Assert 'Security Mode' row is enabled") { - onDeviceSettingsScreen { securityModeRow.assertIsEnabled() } - } - step("Click on 'Security Mode' row") { - onDeviceSettingsScreen { securityModeRow.clickWithAssertion() } - } - step("Assert 'Security Mode' screen is displayed") { - onSecurityModeScreen { screenContainer.assertIsDisplayed() } - } - } - - @DisplayName("Security Mode: other cards cannot open the section") - @Test - fun walletSecurityModeDisabledTest() = - setupHooks().run { - step("Open 'Main Screen'") { - openMainScreen() - } - openDeviceSettingsScreen() - step("Click on 'Scan card or ring' button") { - onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } - } - step("Assert 'Security Mode' row is not clickable") { - onDeviceSettingsScreen { securityModeRow.assertIsNotEnabled() } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 7ab076f209..7cffbf079d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -125,7 +125,7 @@ private fun ScanCardContent(onScanCardClick: () -> Unit) { } } -@Suppress("ComplexMethod", "LongMethod") +@Suppress("ComplexMethod") @Composable private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return @@ -156,13 +156,6 @@ private fun CardSettings(state: CardSettingsScreenState) { Column( modifier = Modifier .fillMaxWidth() - .then( - if (cardInfo is CardInfo.SecurityMode) { - Modifier.testTag(DeviceSettingsScreenTestTags.SECURITY_MODE_ROW) - } else { - Modifier - }, - ) .clickable( enabled = cardInfo.isClickable, onClick = { state.onElementClick(cardInfo) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt index c678c825da..981cd1c76a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -5,11 +5,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.test.SecurityModeScreenTestTags import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement @@ -37,8 +35,7 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) { modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(bottom = 28.dp) - .testTag(SecurityModeScreenTestTags.SCREEN_CONTAINER), + .padding(bottom = 28.dp), verticalArrangement = Arrangement.SpaceBetween, ) { ScreenTitle(titleRes = R.string.card_settings_security_mode, Modifier.padding(bottom = 36.dp)) diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt index a891c43e85..a995632d4f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DeviceSettingsScreenTestTags.kt @@ -5,5 +5,4 @@ object DeviceSettingsScreenTestTags { const val IMAGE_BLOCK = "DEVICE_SETTINGS_SCREEN_IMAGE_BLOCK" const val ITEM_TITLE = "DEVICE_SETTINGS_SCREEN_ITEM_TITLE" const val ITEM_SUBTITLE = "DEVICE_SETTINGS_SCREEN_ITEM_SUBTITLE" - const val SECURITY_MODE_ROW = "DEVICE_SETTINGS_SCREEN_SECURITY_MODE_ROW" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt deleted file mode 100644 index 6c4c3753b6..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SecurityModeScreenTestTags.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.core.ui.test - -object SecurityModeScreenTestTags { - const val SCREEN_CONTAINER = "SECURITY_MODE_SCREEN_CONTAINER" -} \ No newline at end of file From 3f89491aff4a02ab29a2ddf4dd1d087cf88369c3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 09:55:53 +0200 Subject: [PATCH 4/8] Updated on 2026-08-14 --- .../androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 4281531d5b..53468891f0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -23,8 +23,7 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasText(getResourceString(R.string.wallet_connect_title)) } - // Match the wallet row by its own tag (not position-0 clickable, which races with the async-loading - // "Add Wallet" button and otherwise triggers a re-scan → "already saved" dialog). + // Own tag, not position-0 clickable — that races the async "Add Wallet" row and triggers a re-scan. val walletNameButton: KNode = child { hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM) useUnmergedTree = true From 846f6301d94cebc42e773ab5dd55ee8fd96f7d69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 11:12:02 +0200 Subject: [PATCH 5/8] Updated on 2026-08-14 --- .../write-ui-test/reference/compose-traps.md | 30 +++++++++++++++++++ .../screens/WalletSettingsPageObject.kt | 2 +- .../com/tangem/tests/AppCurrencyTest.kt | 8 +++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.claude/skills/write-ui-test/reference/compose-traps.md b/.claude/skills/write-ui-test/reference/compose-traps.md index e6e4beef19..03a06c524f 100644 --- a/.claude/skills/write-ui-test/reference/compose-traps.md +++ b/.claude/skills/write-ui-test/reference/compose-traps.md @@ -41,6 +41,36 @@ so the hold gesture is silently swallowed: the button looks fine, the user holds 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 diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index 5a014f01f9..d1df56061b 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -71,7 +71,7 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi } fun walletNameValue(name: String): KNode = walletSettingsItem.child { - withText(name) + hasText(name) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt index 97730e72e3..0cce0ace48 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt @@ -70,7 +70,9 @@ class AppCurrencyTest : BaseTestCase() { step("Assert total balance contains '$targetSymbol' on 'Main' screen") { // Balance re-loads in the new currency async after the switch — wait for the € equivalent. composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { - runCatching { onMainScreen { totalBalanceText.assertTextContains(targetSymbol) } }.isSuccess + runCatching { + onMainScreen { totalBalanceText.assertTextContains(targetSymbol, substring = true) } + }.isSuccess } } step("Click on token '$token'") { @@ -78,7 +80,9 @@ class AppCurrencyTest : BaseTestCase() { } step("Assert token fiat balance contains '$targetSymbol'") { composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { - runCatching { onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol) } }.isSuccess + runCatching { + onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol, substring = true) } + }.isSuccess } } } From 46ba35643c425aeefcbe22e8002a25835ad53811 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 11:17:43 +0200 Subject: [PATCH 6/8] Updated on 2026-08-14 --- .../androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt | 1 - .../androidTest/kotlin/com/tangem/screens/DialogPageObject.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 53468891f0..ff937e632d 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -23,7 +23,6 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasText(getResourceString(R.string.wallet_connect_title)) } - // Own tag, not position-0 clickable — that races the async "Add Wallet" row and triggers a re-scan. val walletNameButton: KNode = child { hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM) useUnmergedTree = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 00e3613039..b8a3422e11 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -26,7 +26,6 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(BaseDialogTestTags.TEXT) } - // Tag is on the OutlineTextField wrapper; the editable node is its descendant with a SetText action. val inputField: KNode = child { hasSetTextAction() hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD)) From 34a5bc859fdfe6679493f56f784d7c812530f068 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 12:36:46 +0200 Subject: [PATCH 7/8] Updated on 2026-08-14 --- .../reference/running-and-debugging.md | 4 +-- .../com/tangem/tests/AppCurrencyTest.kt | 31 +++++++------------ .../ui/appsettings/AppSettingsScreen.kt | 9 +++++- .../core/ui/test/AppSettingsScreenTestTags.kt | 2 +- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.claude/skills/write-ui-test/reference/running-and-debugging.md b/.claude/skills/write-ui-test/reference/running-and-debugging.md index c7e934192c..8cf1f04834 100644 --- a/.claude/skills/write-ui-test/reference/running-and-debugging.md +++ b/.claude/skills/write-ui-test/reference/running-and-debugging.md @@ -84,5 +84,5 @@ curl http://localhost:8081/__admin/scenarios | jq '.scenarios[] | {name, state}' 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 in `/Users/maxibello/dev/tangem-app-android`; `cd` into the mocks repo only when - needed and prefer absolute paths (the shell session resets cwd). \ No newline at end of file +- Path discipline: stay at the repo root; `cd` into the mocks repo only when needed and prefer absolute + paths (the shell session resets cwd). \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt index 0cce0ace48..78ffdaf70d 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt @@ -3,7 +3,6 @@ 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.extensions.isDisplayedSafely import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.openMainScreen @@ -55,34 +54,26 @@ class AppCurrencyTest : BaseTestCase() { step("Click on currency '$targetCurrency'") { onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() } } - step("Return to 'Main' screen") { - var displayed = false - var attempts = 0 - while (!displayed && attempts < 4) { - onMainScreen { displayed = screenContainer.isDisplayedSafely() } - if (!displayed) { - device.uiDevice.pressBack() - waitForIdle() - attempts++ - } - } + step("Press 'Back' button") { + waitForIdle() + device.uiDevice.pressBack() + } + step("Press 'Back' button") { + 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. - composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { - runCatching { - onMainScreen { totalBalanceText.assertTextContains(targetSymbol, substring = true) } - }.isSuccess + 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'") { - composeTestRule.waitUntil(WAIT_UNTIL_TIMEOUT_LONG) { - runCatching { - onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol, substring = true) } - }.isSuccess + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol, substring = true) } } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 83d745be03..a7d5a8890a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -16,6 +16,7 @@ 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.* @@ -58,7 +59,13 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { is Item.Button -> SettingsButtonItem( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing8) - .testTag(item.id), + .then( + if (item.id == AppSettingsItemsFactory.ID_SELECT_APP_CURRENCY_BUTTON) { + Modifier.testTag(AppSettingsScreenTestTags.CURRENCY_BUTTON) + } else { + Modifier + }, + ), item = item, ) is Item.Switch -> SettingsSwitchItem( diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt index 8dfc28d4b2..c8ed037e0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppSettingsScreenTestTags.kt @@ -1,5 +1,5 @@ package com.tangem.core.ui.test object AppSettingsScreenTestTags { - const val CURRENCY_BUTTON = "select_app_currency_button" + const val CURRENCY_BUTTON = "APP_SETTINGS_SCREEN_CURRENCY_BUTTON" } \ No newline at end of file From d9f3084090e5ab6c9925729078fc2442861fb7cc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 14:11:08 +0200 Subject: [PATCH 8/8] Updated on 2026-08-14 --- .../androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt index 78ffdaf70d..3d7345f883 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/AppCurrencyTest.kt @@ -54,11 +54,11 @@ class AppCurrencyTest : BaseTestCase() { step("Click on currency '$targetCurrency'") { onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() } } - step("Press 'Back' button") { + step("Press 'Back' button to return to 'Details' screen") { waitForIdle() device.uiDevice.pressBack() } - step("Press 'Back' button") { + step("Press 'Back' button to return to 'Main' screen") { waitForIdle() device.uiDevice.pressBack() }