Updated on 2026-08-14
This commit is contained in:
parent
9b8ca9456d
commit
633666c3ce
8 changed files with 110 additions and 16 deletions
|
|
@ -139,8 +139,10 @@ 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.
|
||||
`TangemHoldToConfirmButton`, a Decompose model that fetches in `init {}`, a hot-wallet import with
|
||||
an access code, or a target inside a **LazyColumn/LazyRow that may be below the fold** (use a
|
||||
`KLazyListNode` matcher that auto-scrolls — never a manual swipe). 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.
|
||||
|
|
@ -95,6 +95,65 @@ fun walletNameValue(name: String) = child { withText(name); useUnmergedTree = tr
|
|||
fun walletNameValue(name: String) = child { hasText(name); useUnmergedTree = true }
|
||||
```
|
||||
|
||||
## LazyList item below the fold: plain `child { }` finds it but can't click it
|
||||
|
||||
A `child { hasTestTag(ITEM); hasAnyDescendant(withText(name)) }` matcher resolves the semantics node
|
||||
even when the item is composed **off-screen** (LazyColumn keeps a few items past the viewport). But the
|
||||
node isn't displayed, so `clickWithAssertion()` (`assertIsDisplayed()` first) fails, or `performClick()`
|
||||
taps nothing. Symptom: the test passes when the item happens to be near the top and fails for items
|
||||
lower in the list — and a manual swipe "fixes" it. Do **not** patch with a swipe (flaky, the
|
||||
`clickableSingle` 500ms debounce can also eat fast programmatic clicks).
|
||||
|
||||
**Whenever a target lives in a LazyColumn/LazyRow and might be below the fold, build a `KLazyListNode`
|
||||
matcher up front** — `childWith` scrolls the list to the item before returning it:
|
||||
|
||||
```kotlin
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||
|
||||
private val tokensList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider, // primary-ctor param is in scope in initializers
|
||||
viewBuilderAction = { hasTestTag(SomeScreenTestTags.LAZY_LIST) }, // the LazyColumn's OWN tag
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position -> SemanticsMatcher.expectValue(LazyListItemPositionSemantics, position) },
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun tokenWithTitle(title: String): LazyListItemNode =
|
||||
tokensList.childWith<LazyListItemNode> {
|
||||
hasTestTag(SomeScreenTestTags.LAZY_LIST_ITEM)
|
||||
hasText(title)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
```
|
||||
|
||||
Non-obvious points that bite:
|
||||
|
||||
- **`childWith` searches the MERGED tree** (it scopes via the list's `viewBuilderAction`, whose
|
||||
`useUnmergedTree` defaults to `false`). So match the item by `hasText(title)` — on a `MergeDescendants`
|
||||
item the child texts aggregate onto the item node. `hasAnyDescendant(withText(...))` does **not** match
|
||||
there. (`useUnmergedTree = true` on the item matcher is inert for the scroll/filter but harmless; keep
|
||||
it to mirror existing page objects.)
|
||||
- **The list needs its OWN `testTag` on the `LazyColumn`.** If production tags only the *items* (e.g.
|
||||
`MARKETS_TOKENS_LIST_ITEM`) and not the container, add a tag to the `LazyColumn` modifier in the
|
||||
production composable. Reuse the screen's existing `…TestTags.LAZY_LIST` constant when one fits.
|
||||
- **Scope to the right list when several coexist.** Multiple LazyColumns with the same *item* tag can be
|
||||
composed at once (e.g. the Add-Funds `ChooseTokenScreen` list AND the main-screen markets sheet, both
|
||||
using `MARKETS_TOKENS_LIST_ITEM`). A bare top-level `child { hasTestTag(ITEM); … }` is then ambiguous
|
||||
and may match the wrong screen. `childWith` (and `tokensList.child { … }`) scope through the container
|
||||
tag via `onNode(LAZY_LIST)` / `hasAnyAncestor(LAZY_LIST)`, so they pick the intended list. Prefer a
|
||||
unique container tag over hoping the item text is unique.
|
||||
- **`childWith` returns a `LazyListItemNode`, not a `KNode`.** `clickWithAssertion()` was a `KNode`
|
||||
extension; it's been generalized to `fun BaseNode<*>.clickWithAssertion()` (in
|
||||
`common/extensions/KNode.kt`) so it works on both. Both types extend `BaseNode`, and
|
||||
`assertIsDisplayed()`/`performClick()` live on `BaseNode`.
|
||||
- `positionMatcher` is only used by `childAt(index)` / `hasLazyListItemPosition`. For `childWith`
|
||||
(match-by-content) the items don't need to expose `LazyListItemPositionSemantics` — pass the matcher
|
||||
anyway since the constructor requires it.
|
||||
|
||||
Reference: `AddFundsBottomSheetPageObject.trendingTokenWithTitle` and `MainScreenPageObject` (`lazyList`).
|
||||
|
||||
## Decompose model lifecycle vs. data refresh
|
||||
|
||||
Models (e.g. `TangemPayDetailsModel`) call data fetches from `init {}`, NOT on `ON_RESUME`. Returning
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import android.os.SystemClock
|
|||
import androidx.compose.ui.test.ComposeTimeoutException
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import io.github.kakaocup.compose.node.core.BaseNode
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
||||
fun KNode.clickWithAssertion() {
|
||||
fun BaseNode<*>.clickWithAssertion() {
|
||||
assertIsDisplayed()
|
||||
performClick()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.test.TokenActionsTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
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.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
|
@ -53,9 +58,20 @@ class AddFundsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractions
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun trendingTokenWithTitle(tokenTitle: String): KNode = child {
|
||||
private val trendingTokensList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(BuyTokenScreenTestTags.LAZY_LIST) },
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position ->
|
||||
SemanticsMatcher.expectValue(LazyListItemPositionSemantics, position)
|
||||
},
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun trendingTokenWithTitle(tokenTitle: String): LazyListItemNode =
|
||||
trendingTokensList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||
hasAnyDescendant(withText(tokenTitle))
|
||||
hasText(tokenTitle)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,13 +54,22 @@ class AppCurrencyTest : BaseTestCase() {
|
|||
step("Click on currency '$targetCurrency'") {
|
||||
onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() }
|
||||
}
|
||||
step("Press 'Back' button to return to 'Details' screen") {
|
||||
step("Assert 'App settings' screen is open after currency selection") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onAppSettingsScreen { currencyButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Return to 'Details' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Press 'Back' button to return to 'Main' screen") {
|
||||
waitForIdle()
|
||||
device.uiDevice.pressBack()
|
||||
step("Return to 'Main' screen via 'Back' button") {
|
||||
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Main' screen is opened") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert total balance contains '$targetSymbol' on 'Main' screen") {
|
||||
// Balance re-loads in the new currency async after the switch — wait for the € equivalent.
|
||||
|
|
|
|||
|
|
@ -501,8 +501,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
onMainScreen { swapButton.performClick() }
|
||||
}
|
||||
step("Check 'Action is unavailable' dialog") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||
checkActionIsUnavailableDialog()
|
||||
}
|
||||
}
|
||||
step("Click on 'Ok' button") {
|
||||
onDialog { okButton.performClick() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.tangem.tests.send.warnings
|
|||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.XLM_ACTIVATED_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.constants.TestConstants.XLM_NON_ACTIVATED_RECIPIENT_ADDRESS
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.scenarios.checkSendWarning
|
||||
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||
import com.tangem.scenarios.openSendScreen
|
||||
import com.tangem.screens.onSendAddressScreen
|
||||
import com.tangem.screens.onSendConfirmScreen
|
||||
|
|
@ -110,8 +112,10 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
step("Type non activated address in input text field") {
|
||||
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
|
||||
}
|
||||
step("Click on 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
step("Open 'Send confirm' screen via 'Next' button") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
openSendConfirmScreenViaNextButton()
|
||||
}
|
||||
}
|
||||
step("Assert 'Invalid reserve amount warning' is not displayed") {
|
||||
checkSendWarning(
|
||||
|
|
|
|||
|
|
@ -126,7 +126,8 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
|
|||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.testTag(BuyTokenScreenTestTags.LAZY_LIST),
|
||||
state = lazyListState,
|
||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue