tangem-app-android-audited/.claude/skills/write-ui-test/SKILL.md
2026-06-08 14:31:40 +03:00

9.4 KiB

name description allowed-tools argument-hint
write-ui-test 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", "автотест на экран"). Read, Grep, Glob, Bash, Edit, Write, Agent
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 testTags 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 testTags 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 productTypeProductType.Wallet). Do NOT port iOS .wallet2 to ProductType.Wallet2. Other cards map directly: iOS .twinProductType.Twins, .xrpNoteProductType.Note, .four12Firmware412MockContent (via the mockContent param). ProductType.Wallet2 exists but is a distinct Wallet-2.0-card case, not the iOS-wallet2 analog.
  • A wallet has no balance until you sync. The default Wallet mock starts with missing derivations ("Some addresses are missing"); the fiat balance shows until you call synchronizeAddresses() after openMainScreen() (mirror TotalBalanceUpdateTest). Any test asserting a balance/fiat-equivalent must sync first, then waitUntil the value loads — balances re-load asynchronously (e.g. after an app-currency change the equivalent repaints with a delay).
  • The WireMock scenarios are usually shared across platforms, but the branch may differ (see reference/running-and-debugging.md).

Conventions (must-follow)

Test class shape

  • Scenario state setup goes in the test body, not inside the open-the-feature helper. Each test starts with explicit step("Set WireMock scenario '$name' to '$state'") { setWireMockScenarioState(name, state) } calls, then calls a thin helper (e.g. openTangemPay()) that only opens the screen. Mirror the SendViaSwapTest pattern.
  • Open-the-feature helpers stay thin — no scenarios-as-parameters, no scenario juggling inside.
  • Every scenario name + state is a val at the top of the test method. Reviewers reject magic strings inside step(...).
  • Each click is its own step("Click on '$x' button"). Combining clicks into one step hides which click failed in the Allure report.
  • Every scenario call in the test body is wrapped in its own step("…"), even though the scenario itself contains inner step(...)s — the outer step names the flow in the Allure tree, the inner ones detail it (nested steps are expected). step(...) (Allure) is callable anywhere, including inside scenario extension functions; only flakySafely is restricted to the TestCase body. Caveat: don't wrap a mutating scenario (e.g. one that long-clicks to sign+send) in flakySafely — a retry would re-fire the action; rely on the assertion's own built-in retry instead.
  • Step naming: Click on 'X' button (not "Tap X"); Assert <thing> is displayed / is not displayed. Reviewers reject is visible, does not exist, Check X visible — the convention is is displayed / is not displayed even though older tests in the file may still use the old phrasing (don't copy it).
  • No conditional if (foo.isDisplayedSafely()) foo.performClick() for elements that are deterministically present after pm clear — the if is dead code. Use a straight performClick().

Locations

What Where
Page objects app/src/androidTest/kotlin/com/tangem/screens/…always
Common test helpers app/src/androidTest/kotlin/com/tangem/common/utils/
Feature scenarios app/src/androidTest/kotlin/com/tangem/scenarios/
Cross-feature helper (e.g. confirmSwapByHolding) the feature-of-origin scenarios file (e.g. SwapScenarios.kt), not the consumer's

Scenario files orchestrate flows; they must not define page objects or duplicate generic helpers.

Strings

  • No hardcoded UI text in matchers. Use getResourceString(R.string.foo) from com.tangem.core.res.R or com.tangem.core.ui.R. The Detekt rule UnsafeStringResourceUsage enforces this for production code; reviewers extend it to test code informally.

Allure IDs

  • Every test method gets its own unique @AllureId. Never reuse the same id across two test methods — not even for two variants of one manual case. If a manual case is split into multiple automated tests (e.g. a positive and a negative variant), each test must be linked to its own distinct Allure case/id. (Note: iOS sometimes shares one id across methods — do NOT mirror that here.)

Assertions

  • Never use Kotlin's built-in assert(...) — Android instrumentation runs don't enable JVM assertions, so assert(false) is a silent no-op. Use Truth / JUnit / Kaspresso / Kakao assertions.
  • Clipboard checks: assertClipboardTextEquals(expected, context) from common/utils/ClipboardUtils.kt. Read displayed text via KNode.extractText() first if you need to compare against UI state.
  • Every test ends with a meaningful assertion, not just an action. A test whose last step is "Click Submit" without verifying the result gets rejected.

Waits and synchronization

  • Manual polls are banned (onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty() in a loop). Use:
    • composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMillis) — wait for one thing to appear.
    • composeTestRule.waitUntil(timeout) { runCatching { someAssertion() }.isSuccess } — wait until an action no longer throws.
    • composeTestRule.waitUntil(timeout) { matcherA exists || matcherB exists } — the either/or case.
  • flakySafely(timeout) (Kaspresso) is reachable only from TestCase subclasses, NOT from extension functions on BaseTestCase. In extension code use the waitUntil variants above.

Comment hygiene

This repo enforces "no comments unless WHY is non-obvious", in test code too. One line max, WHY-only — encode a hidden constraint, not what the code does. Example that earns its keep: // Create+confirm screens share ACCESS_CODE_INPUT — gate on confirm-screen title. Delete anything explaining WHAT a step does.

Reference docs

  • reference/compose-traps.md — read when the screen uses PullToRefreshBox, TangemHoldToConfirmButton, a Decompose model that fetches in init {}, or a hot-wallet import with an access code. These have silent failure modes that look like passing tests.
  • reference/running-and-debugging.md — read when building, installing, running tests (orchestrator vs. raw am instrument), running against a local WireMock, interpreting CLI/Allure output, using @Ignore, or driving WireMock scenarios.