Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-24 23:39:09 +02:00
parent d0e8054535
commit 0ce9be5b08
4 changed files with 94 additions and 89 deletions

View file

@ -74,6 +74,10 @@ When the user asks to **port** an iOS test to Android:
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.
- **No reusable step-helpers as private functions in the test class.** A sequence reused across tests
(e.g. `enterAmount`, `assertReady`) goes in a `scenarios/` file as a `BaseTestCase` extension, not as a
private method on the test class — reviewers reject the latter. The test body then calls it wrapped in a
`step(...)` like any scenario.
- **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
@ -121,13 +125,20 @@ Scenario files orchestrate flows; they must not define page objects or duplicate
### 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.
- **Manual polls are banned** (`onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()` in a loop) — even
if a bot reviewer suggests one.
- **Default in the test body: `flakySafely(TIMEOUT) { assertion }`** — the codebase idiom (hundreds of
uses); reviewers prefer it over `composeTestRule.waitUntil { runCatching { … }.isSuccess }`.
- **`ComposeNotIdleException` / `AppNotIdleException` ("busy for ~60s") is usually a sick emulator, not
your test.** After many back-to-back local runs the emulator degrades (you may even see a "System UI
isn't responding" ANR), and idle-synced ops (`flakySafely`, `waitForIdle()`, Kakao actions) start
timing out *anywhere* data is loading — different test each run. Before concluding a test is flaky or
that a screen "never idles", **cold-boot a fresh emulator** (`emulator -avd … -no-snapshot -wipe-data
-memory 4096 -cores 2`) and re-run. A suite that flaked across runs on a tired emulator can be a clean
10/10 on a fresh one (verified on this exact suite). Don't rewrite waits to work around emulator rot.
- **In scenario / `BaseTestCase`-extension code, `flakySafely` is NOT available** regardless — use the
same `composeTestRule.waitUntil` fallback (or `waitUntilAtLeastOneExists(matcher, timeout)` to wait for
appearance, `{ a exists || b exists }` for either/or).
### Comment hygiene

View file

@ -170,16 +170,17 @@ expanding the sheet over the content, so the next click lands on the wrong eleme
Kakao/Compose-test actions block on Compose reaching *idle* first. A screen that animates forever — an
auto-advancing stories/onboarding carousel, a looping shimmer, a never-ending spinner — never idles, so
`clickWithAssertion()` / `assertIsDisplayed()` on it flake (`… is not displayed`, or
`ComposeNotIdleException`). **Remove the screen at its source rather than out-waiting it:** most are gated
by a feature toggle or a mock response — flip it off so the screen never renders. If it's server-driven,
set the toggle **before app launch** (config is fetched at startup), not mid-test. (Example: the swap
stories are disabled via their WireMock scenario, then opened with `storiesExist = false`.)
`ComposeNotIdleException`). **First rule out a degraded emulator** (see running-and-debugging) — a
slow-*loading* screen on a tired emulator throws the identical exception but is fixed by a cold-boot, not
by changing the test. Only treat it as a *truly* infinite animation if it reproduces on a fresh emulator.
**Polling the animated node does NOT rescue it** — two false fixes:
- `waitUntilAtLeastOneExists(hasTestTag(TAG))` polls the **merged** tree (no `useUnmergedTree` option); a
`clickable` node inside a `mergeDescendants` container exists only in the *unmerged* tree → never matches.
- `waitUntil { runCatching { node.assertIsDisplayed() }.isSuccess }` reads the unmerged tree but
`assertIsDisplayed` itself blocks on idle, which never comes → the outer wait times out too.
For a genuinely infinite animation, **remove the screen at its source rather than out-waiting it:** most
are gated by a feature toggle or a mock response — flip it off so the screen never renders. If it's
server-driven, set the toggle **before app launch** (config is fetched at startup), not mid-test.
(Example: the swap first-time stories are disabled via their WireMock scenario, then opened with
`storiesExist = false`.) Note that `waitUntilAtLeastOneExists(hasTestTag(TAG))` polls the **merged** tree
(no `useUnmergedTree` option), so a `clickable` node inside a `mergeDescendants` container — which exists
only in the *unmerged* tree — will never match it; poll through the page object instead.
## Decompose model lifecycle vs. data refresh