Updated on 2026-08-14
This commit is contained in:
commit
39b4d1add9
2179 changed files with 93617 additions and 13563 deletions
172
.claude/rules/codestyle/design-system.md
Normal file
172
.claude/rules/codestyle/design-system.md
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
---
|
||||||
|
description: Design-system generations (DS1/DS2/DS3), component pattern, KDoc & API conventions, storybook
|
||||||
|
paths:
|
||||||
|
- "core/ui/src/main/java/com/tangem/core/ui/ds2/**"
|
||||||
|
- "core/ui/src/main/java/com/tangem/core/ui/ds/**"
|
||||||
|
- "core/ui/src/main/java/com/tangem/core/ui/components/**"
|
||||||
|
- "features/tester/**"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Design System
|
||||||
|
|
||||||
|
The app currently hosts **three generations of the design system (DS)** side by side. They differ by
|
||||||
|
folder, token set (colors / typography / dimensions), and the `@Preview` wrapper. Knowing which
|
||||||
|
generation a component belongs to is essential so you don't mix tokens or pull the wrong building blocks.
|
||||||
|
|
||||||
|
## Three generations
|
||||||
|
|
||||||
|
| Generation | Folder | Colors | Typography | Dimensions | Preview wrapper |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **DS1** (legacy) | `core/ui/src/main/java/com/tangem/core/ui/components/` | `TangemTheme.colors` | `TangemTheme.typography` | `TangemTheme.dimens` | `TangemThemePreview` |
|
||||||
|
| **DS2** (redesign) | `core/ui/src/main/java/com/tangem/core/ui/ds/` | `TangemTheme.colors2` | `TangemTheme.typography2` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
|
||||||
|
| **DS3** (target) | `core/ui/src/main/java/com/tangem/core/ui/ds2/` | `TangemTheme.colors3` | `TangemTheme.typography3` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
|
||||||
|
|
||||||
|
> Mind the numbering mismatch: **folder `ds` is DS2**, **folder `ds2` is DS3**.
|
||||||
|
> The `colors2` / `typography2` tokens are `@Deprecated` (ReplaceWith `colors3` / `typography3`).
|
||||||
|
|
||||||
|
- **DS1** — the entire current app is built on it. Do **not** add new components here.
|
||||||
|
- **DS2** — redesign components. A transitional generation; don't write new components in it, only
|
||||||
|
maintain what already exists.
|
||||||
|
- **DS3** — the newest design system; **the whole app is being migrated to it**. Build new DS
|
||||||
|
components here.
|
||||||
|
|
||||||
|
## Using DS3 in features
|
||||||
|
|
||||||
|
**All DS3 components (folder `ds2`) may be used in features starting from app version 6.0.** Before
|
||||||
|
6.0 they must not be used on product screens.
|
||||||
|
|
||||||
|
If a needed component does not yet exist in DS3, **add it by analogy with the existing ones** (see the
|
||||||
|
pattern below).
|
||||||
|
|
||||||
|
## DS3 component pattern
|
||||||
|
|
||||||
|
Study the existing components as references:
|
||||||
|
- Simple: `ds2/checkbox/TangemCheckmark.kt` — single file, a public `@Composable` function + `@Preview`.
|
||||||
|
- Composite: `ds2/button/` — `TangemButton.kt` (public API), `TangemButtonInternal.kt` (private inner
|
||||||
|
layout), `TangemButtonExt.kt` (variant / size tokens).
|
||||||
|
|
||||||
|
Pattern rules:
|
||||||
|
|
||||||
|
1. **Package & location.** `com.tangem.core.ui.ds2.<component>`, folder
|
||||||
|
`core/ui/.../ds2/<component>/`. The component name is `Tangem<Name>`.
|
||||||
|
2. **DS3 tokens only.** Colors — `TangemTheme.colors3.*`, text — `TangemTheme.typography3.*`,
|
||||||
|
dimensions — `TangemTheme.dimens2.*`. No `colors` / `colors2` / hardcoded values (literal dp/colors
|
||||||
|
are acceptable only inside `@Preview`, where you add `@Suppress("MagicNumber")`).
|
||||||
|
3. **Signature.** `modifier: Modifier = Modifier` is mandatory (defaulting to `Modifier`, placed first
|
||||||
|
among the optional params or right after the required ones). Express variants/sizes via a nested
|
||||||
|
`enum` in `object Tangem<Name>` (like `TangemButton.Variant` / `TangemButton.Size`), not boolean flags.
|
||||||
|
4. **Accessibility.** Pass `contentDescription`, set the `Role`, mark `disabled()` in `semantics`, and
|
||||||
|
handle focus/press state via `interactionSource`.
|
||||||
|
5. **KDoc + Figma link.** Above the public function — KDoc describing behavior, every parameter, and a
|
||||||
|
link to the Figma node (see the KDoc requirements below).
|
||||||
|
6. **Previews.** Two `@Preview`s (Light + Dark via `UI_MODE_NIGHT_YES`), wrapped in
|
||||||
|
`TangemThemePreviewRedesign { ... }`, with `TangemTheme.colors3.bg.primary` as the background.
|
||||||
|
Preview helpers (`PreviewRow`, `Section`, etc.) are private in the same file.
|
||||||
|
7. **Composite components** (many variants / heavy layout) are split into 3 files like the button:
|
||||||
|
public `Tangem<Name>.kt`, private `Tangem<Name>Internal.kt`, tokens `Tangem<Name>Ext.kt`.
|
||||||
|
|
||||||
|
## API conventions
|
||||||
|
|
||||||
|
### Public properties live in the `object`
|
||||||
|
|
||||||
|
Any public type the component exposes — variant/size/role/align enums, status classes, constants —
|
||||||
|
is declared inside the namesake `object Tangem<Name>`, **not** as a top-level type. This keeps a single
|
||||||
|
`Tangem<Name>.Variant` / `Tangem<Name>.Size` / `Tangem<Name>.Role` namespace at the call site and
|
||||||
|
avoids polluting the package.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
object TangemTopNavigation {
|
||||||
|
/** Horizontal alignment of the center content slot. */
|
||||||
|
enum class ContentAlign { Start, Center }
|
||||||
|
}
|
||||||
|
// usage: TangemTopNavigation.ContentAlign.Center
|
||||||
|
```
|
||||||
|
|
||||||
|
References: `TangemTopNavigation.ContentAlign`, `TangemNavigationText.Role`, `TangemButton.Variant` /
|
||||||
|
`TangemButton.Size`.
|
||||||
|
|
||||||
|
### Provide convenient overloads
|
||||||
|
|
||||||
|
A component should ship ergonomic overloads so callers don't assemble boilerplate for the common case.
|
||||||
|
Two acceptable shapes:
|
||||||
|
|
||||||
|
1. **Additional `@Composable fun` overloads** with simpler parameters that delegate to the base one.
|
||||||
|
`TangemTopNavigation` has a low-level slot-based overload (`startButton`/`endButton`/`contentColumn`
|
||||||
|
lambdas) plus several high-level overloads taking `title` / `subtitle` / `onBack` / `onClose` that
|
||||||
|
wire the predefined buttons and the title/subtitle center for you.
|
||||||
|
2. **Extension functions on the `object`** for named presets — e.g. `@Composable fun TangemButton.Back(…)`
|
||||||
|
and `TangemButton.Close(…)` in `TangemButtonExt.kt` expose ready-made button presets while reading
|
||||||
|
as `TangemButton.Back { … }` at the call site.
|
||||||
|
|
||||||
|
Each overload keeps the same rules as the base component (`modifier` first among optionals, DS3 tokens,
|
||||||
|
its own KDoc — see below).
|
||||||
|
|
||||||
|
### Sub-components are first-class
|
||||||
|
|
||||||
|
Internal building blocks that are themselves public (e.g. `TangemNavigationText`, used for the
|
||||||
|
`TangemTopNavigation` title/subtitle slots) follow the **exact same rules** as a top-level component:
|
||||||
|
DS3 tokens only, `modifier: Modifier = Modifier`, public properties in their own `object`
|
||||||
|
(`TangemNavigationText.Role`), full KDoc, and their own Storybook entry where it makes sense. Don't
|
||||||
|
treat "helper" composables as second-class — if a feature can call it, it is a documented DS component.
|
||||||
|
|
||||||
|
## KDoc requirements for components
|
||||||
|
|
||||||
|
Every public DS component (and any non-trivial public composable) must carry a KDoc block. Use
|
||||||
|
`ds2/button/TangemButton.kt` and `ds2/checkbox/TangemCheckmark.kt` as the canonical examples.
|
||||||
|
|
||||||
|
A component KDoc must contain, in order:
|
||||||
|
|
||||||
|
1. **Summary line.** One sentence stating what the component is and which generation it belongs to —
|
||||||
|
start with `Design-system v2 …` for DS3 components (matches the existing wording).
|
||||||
|
2. **Figma link.** A markdown link to the exact Figma node:
|
||||||
|
`[Figma](https://www.figma.com/design/…?node-id=…)`. A component without a Figma reference is not
|
||||||
|
review-ready.
|
||||||
|
3. **Behavior notes** (when behavior is non-obvious). A short prose paragraph or a bulleted
|
||||||
|
`Behavior notes:` list covering state-dependent rendering — loading, disabled/enabled, icon-only
|
||||||
|
vs. labeled, focus ring, animations, what overrides what. Describe *observable behavior*, not the
|
||||||
|
implementation.
|
||||||
|
4. **`@param` for every parameter.** No parameter may be left undocumented — including `modifier`
|
||||||
|
when its effect is non-trivial (e.g. "Pass `Modifier.fillMaxWidth()` to switch to fixed-width
|
||||||
|
layout"). Each `@param` states the meaning **and** the consequences of notable values
|
||||||
|
(`null` → non-interactive, `false` → dimmed & clicks ignored, etc.).
|
||||||
|
5. **Accessibility guidance** where relevant — e.g. when `contentDescription` should be supplied
|
||||||
|
(icon-only buttons, loading state, disabled state) and what it announces.
|
||||||
|
|
||||||
|
Additional rules:
|
||||||
|
|
||||||
|
- Document the **nested `enum`s** (`Variant`, `Size`, `Status`, …) too: a short KDoc on the enum and,
|
||||||
|
where the options aren't self-explanatory, a one-line description per entry (see `TangemButton.Variant`).
|
||||||
|
- Keep KDoc about **contract and behavior**, not internals. Implementation comments explaining *why*
|
||||||
|
a specific approach was taken belong to inline `//` comments inside the body, not the KDoc.
|
||||||
|
- Reference other DS types with `[TangemSurface]` / `[TangemButton.Variant]` link syntax so they
|
||||||
|
resolve in the IDE.
|
||||||
|
- Detekt enforces missing-KDoc-on-public-API style checks on `core:ui`; run `./gradlew :core:ui:detektMain`.
|
||||||
|
|
||||||
|
## Storybook
|
||||||
|
|
||||||
|
Add every DS3 component to the **Storybook** (module `features/tester`) — a live on-device/emulator
|
||||||
|
component gallery (Tester → Storybook → DS Components).
|
||||||
|
|
||||||
|
Use the **`add-storybook-component`** skill — it wires the entity, the Build factory, the Composable
|
||||||
|
page, and registers it in the correct list. Run: `/add-storybook-component TangemCheckmark (DS)`.
|
||||||
|
Page layout guidelines live in
|
||||||
|
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md`.
|
||||||
|
|
||||||
|
## Checklist: adding a new DS3 component
|
||||||
|
|
||||||
|
- [ ] Component created under `core/ui/.../ds2/<component>/`, package `com.tangem.core.ui.ds2.<component>`.
|
||||||
|
- [ ] Named `Tangem<Name>`; first optional parameter is `modifier: Modifier = Modifier`.
|
||||||
|
- [ ] Uses **only** DS3 tokens: `colors3`, `typography3`, `dimens2`. No hardcoded values outside previews.
|
||||||
|
- [ ] Variants/sizes expressed as an `enum` inside `object Tangem<Name>` (not a set of boolean flags).
|
||||||
|
- [ ] All public types (enums, statuses, constants) declared inside the `object Tangem<Name>`.
|
||||||
|
- [ ] Convenient overloads provided (simpler `@Composable` overloads and/or `object` extension presets).
|
||||||
|
- [ ] Public sub-components (e.g. `TangemNavigationText`) follow the same rules + KDoc as a full component.
|
||||||
|
- [ ] States handled: enabled/disabled, press/focus (`interactionSource`), loading (if applicable).
|
||||||
|
- [ ] Accessibility: `contentDescription`, `Role`, `disabled()` in `semantics`.
|
||||||
|
- [ ] KDoc per the requirements above (summary + Figma link + behavior notes + every `@param` + a11y).
|
||||||
|
- [ ] Two `@Preview`s (Light/Dark) in `TangemThemePreviewRedesign`, background `colors3.bg.primary`.
|
||||||
|
- [ ] Heavy component split into `Tangem<Name>.kt` / `…Internal.kt` / `…Ext.kt`.
|
||||||
|
- [ ] Storybook page added (`add-storybook-component` skill).
|
||||||
|
- [ ] Detekt passes: `./gradlew :core:ui:detektMain` (plus
|
||||||
|
`./gradlew :features:tester:impl:assembleGoogleDebug` if you touched the Storybook).
|
||||||
|
- [ ] Use in product features only from app version **6.0** onward.
|
||||||
214
.claude/rules/unit-testing.md
Normal file
214
.claude/rules/unit-testing.md
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
# Unit Testing Rules
|
||||||
|
|
||||||
|
This document covers **unit tests** only — sources under `src/test`, running on the JVM via JUnit 5 (Jupiter). UI / instrumentation tests (`src/androidTest`, Kaspresso + Espresso on the JUnit 4 on-device runner) are a separate concern and out of scope here.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Purpose | Library | Version source |
|
||||||
|
|---|---|---|
|
||||||
|
| Test runner | JUnit 5 (Jupiter) | `deps.test.junit5` |
|
||||||
|
| Mocking | MockK | `deps.test.mockk` |
|
||||||
|
| Flow testing | Turbine | `deps.test.turbine` |
|
||||||
|
| Assertions | Google Truth | `deps.test.truth` |
|
||||||
|
| Coroutines | `kotlinx-coroutines-test` | `deps.test.coroutine` |
|
||||||
|
|
||||||
|
All unit tests run on JUnit 5. JUnit 4 (`deps.test.junit` = `junit:junit`) is **not** used in `src/test` at all — it survives only in `src/androidTest` instrumentation. Don't add new JUnit 4 unit tests.
|
||||||
|
|
||||||
|
Versions live in `gradle/dependencies.toml`. Do not hardcode library coordinates in module build scripts — always go through the catalog.
|
||||||
|
|
||||||
|
## Shared test modules
|
||||||
|
|
||||||
|
Depend on these via `testImplementation(projects.*)` — never copy their utilities inline.
|
||||||
|
|
||||||
|
Build test fixtures with **factory functions that default every argument** (`createXxx(id = 1, name = "Cat", … )`) rather than calling bloated constructors at each call site. A test then overrides only the fields relevant to it, so the intent stays visible and adding a model field doesn't churn every test. This is the idiom behind the `Mock*Factory` classes below — extend them instead of hand-rolling fixtures.
|
||||||
|
|
||||||
|
### `:test:core` (pure JVM)
|
||||||
|
`test/core/src/main/java/com/tangem/test/core/`. Re-exports as `api`: `test.coroutine`, `test.junit5`, `test.mockk`, `test.truth`, `test.turbine`. Use it as the one-line entry point to pull the whole unit-testing stack into a JVM module. Depends on `domain:core` and `arrow.core` (so its utilities can reference domain abstractions like `FlowProducer`).
|
||||||
|
|
||||||
|
Utilities:
|
||||||
|
- `TestCoroutineExt.getEmittedValues(flow)` — collect a `Flow` into a `List` from a `TestScope`.
|
||||||
|
- `TestFlowProducerTools(scope, dispatcher)` — test double for `FlowProducerTools` that mirrors production `DefaultFlowProducerTools` (retryWhen + fallback + `distinctUntilChanged` + `shareIn`) on a caller-provided test scope/dispatcher, without analytics/logging. Pass `TestScope.backgroundScope` + a dispatcher built from `testScheduler` so the 2s retry delay is virtual-time-controllable. Use it for `FlowProducer` tests instead of mocking `FlowProducerTools`.
|
||||||
|
- `@ProvideTestModels` — meta-annotation over JUnit 5 `@MethodSource("provideTestModels")` for parameterized tests.
|
||||||
|
- `TruthArrowExt` — `assertEither`, `assertEitherRight`, `assertEitherLeft`, `assertSome`, `assertNone` for Arrow types.
|
||||||
|
|
||||||
|
### `:common:test` (Android library — legacy, being retired)
|
||||||
|
`common/test/src/main/java/com/tangem/common/test/`. Factories and fakes for domain/data models. Being phased out in favour of `:test:core` (JVM mechanisms) and `:test:mock` (mock factories); don't add new utilities here.
|
||||||
|
|
||||||
|
- `TestAppCoroutineScope(testScope)` — test implementation of `AppCoroutineScope`.
|
||||||
|
- `MockStateDataStore` — in-memory `DataStore` for tests.
|
||||||
|
- `Mock*Factory` classes for `CryptoCurrency`, `UserWallet`, `NetworkStatus`, `ScanResponse`, `YieldDTO`, `QuoteResponse`, `UpdateWalletManagerResult` etc.
|
||||||
|
|
||||||
|
### `:test:mock`
|
||||||
|
`test/mock/`. Mock data for models not yet covered elsewhere (currently `MockAccounts`). Add to this module rather than creating new ad-hoc mock files.
|
||||||
|
|
||||||
|
## Dispatchers
|
||||||
|
|
||||||
|
Never use `Dispatchers.Main`/`IO`/`Default` directly in production code — always inject `CoroutineDispatcherProvider` from `core/utils`.
|
||||||
|
|
||||||
|
In tests, override with `TestingCoroutineDispatcherProvider` (defined in `core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt`). By default `main`/`mainImmediate`/`io`/`default` are `Dispatchers.Unconfined`, while `single` is a single-thread `Executors.newFixedThreadPool(1)` dispatcher.
|
||||||
|
|
||||||
|
For `Model`-layer tests inside features, construct it with a single `StandardTestDispatcher(testScheduler)` for all five roles (built from the enclosing `TestScope`) so `advanceUntilIdle()` controls execution. See any `features/*/impl` model test for the `TestScope.createTestingCoroutineDispatcherProvider()` helper.
|
||||||
|
|
||||||
|
## Naming & placement
|
||||||
|
|
||||||
|
- **Test class**: `FooTest` (singular noun). Not `FooSpec`, not `FooBehavior`, not `FooTests`.
|
||||||
|
- **Test method**: backtick-quoted sentence that **must** follow `GIVEN … WHEN … THEN …` (uppercase). The name states the behaviour under test — precondition, action, expected outcome — not the implementation.
|
||||||
|
```kotlin
|
||||||
|
@Test
|
||||||
|
fun `GIVEN currency status emitted WHEN model created THEN analytics sent`() = runTest { … }
|
||||||
|
```
|
||||||
|
A part may collapse when trivial (e.g. `GIVEN no wallets WHEN load THEN returns empty`), but all three keywords stay present.
|
||||||
|
- **Test body**: if the body is more than a one-liner (i.e. has distinct setup / action / check phases), it **must** be marked with `// Arrange`, `// Act`, `// Assert` comments. GWT names the behaviour from the outside; AAA structures the code inside.
|
||||||
|
- **Location**: mirrored packages under `src/test/kotlin/`. No `src/testFixtures/` — shared helpers go to the modules above.
|
||||||
|
|
||||||
|
## Unit-test skeleton (JUnit 5)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class FooTest {
|
||||||
|
|
||||||
|
private val barUseCase: BarUseCase = mockk()
|
||||||
|
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||||
|
|
||||||
|
private val foo = Foo(barUseCase, dispatchers)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun resetMocks() {
|
||||||
|
clearMocks(barUseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN bar returns right WHEN invoke THEN emits value`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { barUseCase(any()) } returns Either.Right(expected)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = foo.invoke(input)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(actual).isEqualTo(expected)
|
||||||
|
coVerify(exactly = 1) { barUseCase(input) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `@TestInstance(Lifecycle.PER_CLASS)` is **opt-in per class, not the project default.** Add it only where you need a non-static `@MethodSource`/`provideTestModels` provider or want to share expensive setup across methods (~half of test classes do). The JUnit default stays `PER_METHOD` (a fresh instance per test). Beware: `PER_CLASS` reuses one instance across all methods, so mutable fields leak between tests — reset them in `@BeforeEach`.
|
||||||
|
- **Group by method under test.** When a class/file exposes several functions and each accumulates many tests, don't keep one flat list — give each function its own `@Nested @TestInstance(Lifecycle.PER_CLASS) inner class`. The nesting maps the test structure onto the production API and keeps per-function setup local to its group.
|
||||||
|
```kotlin
|
||||||
|
internal class DesignControllerTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
inner class GetDesigns {
|
||||||
|
@Test fun `GIVEN … WHEN getDesigns THEN all fields included`() { … }
|
||||||
|
@Test fun `GIVEN limit WHEN getDesigns THEN list is capped`() { … }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
inner class DeleteDesign {
|
||||||
|
@Test fun `GIVEN existing id WHEN deleteDesign THEN removed from db`() { … }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- You do **not** declare `useJUnitPlatform()` per module — the `configuration` convention plugin applies it (and the JUnit 5 engine) to every module. See "Gradle wiring" below.
|
||||||
|
|
||||||
|
## MockK conventions
|
||||||
|
|
||||||
|
- Field-level init: `private val x: T = mockk()`; use `mockk(relaxed = true)` only when stubs are not the subject of the test.
|
||||||
|
- Stub coroutines with `coEvery { … } returns …` / `returnsMany(...)`; verify with `coVerify { … }`, `coVerify(exactly = n) { … }`, `coVerifyOrder { … }`.
|
||||||
|
- Create mocks once as `val` fields and reset them with `clearMocks(...)` in `@BeforeEach` — recreating mocks (`x = mockk()` inside `@BeforeEach`) every test is measurably expensive (MockK instantiation dominates the runtime of small tests). Only recreate a field when the subject-under-test itself holds mutable state that must be fresh per test.
|
||||||
|
- For companion/top-level objects use `mockkObject(Obj)` and pair with `unmockkObject(Obj)` in teardown.
|
||||||
|
|
||||||
|
## Flow testing
|
||||||
|
|
||||||
|
- Default to `TestScope.getEmittedValues(flow)` (from `:test:core`) when you just want the list of values produced during the test scope — this is the most common approach in the codebase.
|
||||||
|
- Use **Turbine** (`flow.test { … }`) when you specifically need to assert on the emission *sequence* (ordering, intermediate items, completion/error timing), or for hot `SharedFlow`s where you must control collection start/stop.
|
||||||
|
- Drive hot sources via `MutableSharedFlow` / `MutableStateFlow` and `advanceUntilIdle()` between emission and assertion.
|
||||||
|
- For `FlowProducer` tests (retry/fallback/shareIn semantics), inject `TestFlowProducerTools` from `:test:core` and use Turbine + `advanceTimeBy(2001); runCurrent()` to step over the 2s retry window deterministically.
|
||||||
|
|
||||||
|
## Parameterized tests
|
||||||
|
|
||||||
|
When the same behaviour is exercised over a set of inputs, write **one parameterized test** — not several near-identical methods, and not one method with a stack of `assertThat(...)` calls over different inputs. Repeated asserts hide *which* input failed and stop at the first failure; a parameterized test reports each case separately. Add a new case = add a row to the provider.
|
||||||
|
|
||||||
|
Use the project's `@ProvideTestModels` annotation — it wires `@MethodSource("provideTestModels")` for you.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@ParameterizedTest
|
||||||
|
@ProvideTestModels
|
||||||
|
fun create(model: CreateModel) = runTest { … }
|
||||||
|
|
||||||
|
private data class CreateModel(val input: Input, val expected: Either<Error, Value>)
|
||||||
|
|
||||||
|
private fun provideTestModels() = listOf(
|
||||||
|
CreateModel(input = …, expected = Either.Right(…)),
|
||||||
|
CreateModel(input = …, expected = Either.Left(Error.Foo)),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`provideTestModels` is a non-static instance method, so the class needs `@TestInstance(Lifecycle.PER_CLASS)` (or a `@JvmStatic` provider in a companion).
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
- Default to Truth: `assertThat(actual).isEqualTo(expected)`, `.isInstanceOf(T::class.java)`, `.hasMessageThat().isEqualTo(…)`, `.isNull()`.
|
||||||
|
- **Assert whole objects, not field-by-field.** When the type is a `data class`, build the expected instance and compare with one `isEqualTo(expected)` — the structural `equals`/`toString` gives a self-explanatory diff. For collections use `.containsExactly(…)` (add `.inOrder()` when order matters). Prefer this over a series of `assertThat(actual.id)…`, `assertThat(actual.name)…` checks, which produce opaque failures and miss unexpected fields.
|
||||||
|
- For Arrow `Either`/`Option`, prefer `assertEither`, `assertEitherLeft`, `assertEitherRight`, `assertSome`, `assertNone` from `:test:core`.
|
||||||
|
- Exception testing: `runCatching { … }.exceptionOrNull()` + Truth, not `assertThrows`.
|
||||||
|
|
||||||
|
## Feature model tests
|
||||||
|
|
||||||
|
`features/*/impl` Decompose models share a heavy dependency graph — extract a `XxxModelTestBase` with pre-built mocks/fixtures and inherit per-scenario test classes from it (see `features/staking/impl/.../presentation/model/StakingModelTestBase` as reference).
|
||||||
|
|
||||||
|
Lifecycle:
|
||||||
|
```kotlin
|
||||||
|
val model = createModel(testScope = this)
|
||||||
|
advanceUntilIdle()
|
||||||
|
// assertions…
|
||||||
|
model.onDestroy()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew unitTest # all JVM + debug/googleDebug unit tests (root aggregator)
|
||||||
|
./gradlew :<module>:testDebugUnitTest # single Android library module
|
||||||
|
./gradlew :app:testGoogleDebugUnitTest # app module
|
||||||
|
./gradlew :<jvm-module>:test # pure JVM module
|
||||||
|
./gradlew :<module>:testDebugUnitTest --tests "com.tangem.<Fqn>Test" # single class
|
||||||
|
```
|
||||||
|
|
||||||
|
The `unitTest` aggregator lives in the root `build.gradle.kts`; it is wired automatically for every `com.android.application`, `com.android.library`, and pure `org.jetbrains.kotlin.jvm` subproject — no need to touch it when adding a new module.
|
||||||
|
|
||||||
|
## Gradle wiring for a new test-bearing module
|
||||||
|
|
||||||
|
The `configuration` convention plugin (`configureUnitTests` in `plugins/configuration/.../TestConfigurations.kt`) centralizes the JUnit 5 setup for **every** module:
|
||||||
|
|
||||||
|
1. `useJUnitPlatform()` on all `Test` tasks — so Jupiter tests are discovered (without it the default JUnit 4 runner runs zero Jupiter tests).
|
||||||
|
2. `testRuntimeOnly(<test-junit5-engine>)` — the Jupiter runtime engine. The platform without the engine silently runs **zero** tests, so these two are paired in one place.
|
||||||
|
3. Test logging (full exception format, standard streams, PASSED/SKIPPED/FAILED events, per-task summary).
|
||||||
|
|
||||||
|
So a test module must **not** re-declare `useJUnitPlatform()`, the engine, or `testLogging { … }`. It only needs the Jupiter **API** (provided transitively by `:test:core`, or declared explicitly):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Any module (JVM or Android library) — plugin already supplies platform + engine + logging
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.kotlin.jvm) // or the android-library convention
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(projects.test.core) // junit5 (api) + mockk + turbine + truth + coroutine-test
|
||||||
|
testImplementation(projects.common.test) // add only if the tests need legacy model factories / fakes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If a module doesn't want the full `:test:core` bundle, declare the Jupiter API directly with `testImplementation(deps.test.junit5)` — the engine still comes from the plugin, so never add `testRuntimeOnly(deps.test.junit5.engine)` per module.
|
||||||
|
|
||||||
|
## Module type vs. layer
|
||||||
|
|
||||||
|
The domain layer is **not** uniformly pure-JVM: domain modules are split roughly evenly between `org.jetbrains.kotlin.jvm` (pure JVM) and `com.android.library` modules. Don't assume the layer dictates the module type — check the `plugins { }` block to pick the right test task:
|
||||||
|
|
||||||
|
- `kotlin.jvm` (pure JVM) → `./gradlew :<module>:test`
|
||||||
|
- `com.android.library` / `com.android.application` → `./gradlew :<module>:testDebugUnitTest` (`:app` → `testGoogleDebugUnitTest`)
|
||||||
|
|
||||||
|
`./gradlew unitTest` runs the right task for every module regardless of type.
|
||||||
150
.claude/skills/add-storybook-component/SKILL.md
Normal file
150
.claude/skills/add-storybook-component/SKILL.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
---
|
||||||
|
name: add-storybook-component
|
||||||
|
description: Add a component showcase page to the Tangem storybook (in features/tester). Wires the entity, Build factory, Composable page, and registers it either in the "DS Components" sub-list (first/default target — for design-system components under core.ui.ds2.*) or in the root storybook list (second target — for any other component). Use when asked to "add a storybook page/story", "add <Component> to the storybook", "сделай сторибук для <компонент>", "добавь стори/историю в storybook", or to showcase a DS component in the tester.
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write
|
||||||
|
argument-hint: [component to add, e.g. "TangemCheckbox (DS)" or "MyLegacyCard"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a new component page to the Tangem storybook. The storybook lives in
|
||||||
|
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/`
|
||||||
|
and renders interactive DS/component showcases on a device or emulator.
|
||||||
|
|
||||||
|
This is an **interactive** skill: read the real production component first to get its actual
|
||||||
|
parameters, enums, and package — never guess the API. Then mirror the closest existing story.
|
||||||
|
|
||||||
|
## Two placement targets — pick one
|
||||||
|
|
||||||
|
| Target | Use for | List screen | Page dir | Entity supertype |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **1. DS Components (default)** | Design-system components under `com.tangem.core.ui.ds2.*` (the newest "DS3"/redesign components: `TangemButton`, `TangemBadge`, `TangemRow`, `TangemLoader`, …) | `page/ds/DsComponentsListScreen.kt` → `buildDsStories()` | `page/ds/<component>/` | `DsStoryBookPage` |
|
||||||
|
| **2. Other components** | Anything else (legacy/cross-cutting components, backgrounds, effects, typography demos) | `ui/StoryBookListScreen.kt` → `buildStories()` | `page/<component>/` | `StoryBookPage` |
|
||||||
|
|
||||||
|
**Default to Target 1 (DS Components)** when the component lives under `core.ui.ds2.*` or the user
|
||||||
|
mentions "DS"/"ds3"/"design system". Only the **list screen** and **page directory** differ between
|
||||||
|
the two targets — everything else (entity declaration file, `StoryBookScreen.kt` routing, factory
|
||||||
|
pattern) is identical.
|
||||||
|
|
||||||
|
> The ONLY behavioral difference of `DsStoryBookPage` vs `StoryBookPage`: `StoryBookViewModel.onBackClick`
|
||||||
|
> routes a `DsStoryBookPage` back to the DS sub-list, while a plain `StoryBookPage` routes back to the
|
||||||
|
> root list. That's it.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md` is the canonical doc — read it for the **design guidelines** (mandatory
|
||||||
|
page layout: single live preview pinned at top + one control per parameter below, chip-selector pattern,
|
||||||
|
colors, realistic text). This skill covers the *wiring*; STORYBOOK.md covers the *look*.
|
||||||
|
|
||||||
|
Best reference implementations to mirror:
|
||||||
|
- **Stateful DS page with many controls:** `page/ds/button/` (TangemButton — variant/size/background
|
||||||
|
selectors, toggles, text-scale slider, blur backdrop). Read all three files: `Build.kt`,
|
||||||
|
`TangemButtonStory.kt`, and the `TangemButtonStory` entity in `entity/StoryBookPage.kt`.
|
||||||
|
- **Simple stateful page:** `page/ds/loader/` (TangemLoader — single size selector).
|
||||||
|
- **Stateless page (no params):** a `data object` sibling such as `ButtonsStory`.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Read the production component.** Grep `core/ui/src/main/java/com/tangem/core/ui/ds2/<name>/`
|
||||||
|
(or wherever it lives) for the composable signature, its `enum`s (Variant/Size/Status/…), and
|
||||||
|
required vs optional params. The set of parameters becomes the set of controls.
|
||||||
|
2. **Decide stateless vs stateful:**
|
||||||
|
- **Stateless** (`data object`) — ONLY if the component has no configurable parameters at all.
|
||||||
|
- **Stateful** (`data class`) — the normal case: one field per parameter the user can change, each
|
||||||
|
paired with an `onXxxChange`/`onXxxToggle` lambda.
|
||||||
|
3. **Pick the target** (see table above) and **mirror the closest sibling**.
|
||||||
|
4. **Do the 4 edits + 1 new dir** (Steps A–E below).
|
||||||
|
5. **Verify it compiles** (see Build).
|
||||||
|
|
||||||
|
## The edits
|
||||||
|
|
||||||
|
Assume component `Foo` rendered by `com.tangem.core.ui.ds2.foo.TangemFoo` with a `Variant` enum and an
|
||||||
|
`isEnabled` flag. Adjust names to the real component. `<page-dir>` =
|
||||||
|
`page/ds/foo/` for Target 1, or `page/foo/` for Target 2.
|
||||||
|
|
||||||
|
### A. Declare the entity in `entity/StoryBookPage.kt`
|
||||||
|
|
||||||
|
Stateful (normal):
|
||||||
|
```kotlin
|
||||||
|
internal data class TangemFooStory(
|
||||||
|
val variant: TangemFoo.Variant,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val onVariantChange: (TangemFoo.Variant) -> Unit,
|
||||||
|
val onEnabledToggle: () -> Unit,
|
||||||
|
) : DsStoryBookPage // <- StoryBookPage for Target 2
|
||||||
|
```
|
||||||
|
Stateless: `internal data object TangemFooStory : DsStoryBookPage` (or `StoryBookPage`).
|
||||||
|
|
||||||
|
Add the matching import for the production type at the top of the file.
|
||||||
|
|
||||||
|
### B. Create `<page-dir>/Build.kt`
|
||||||
|
|
||||||
|
Stateful — uses `storyPageFactory` + `StateUpdater`:
|
||||||
|
```kotlin
|
||||||
|
internal fun StateUpdater<TangemFooStory>.build(): TangemFooStory {
|
||||||
|
return TangemFooStory(
|
||||||
|
variant = TangemFoo.Variant.Primary,
|
||||||
|
isEnabled = true,
|
||||||
|
onVariantChange = { v -> updateStory { it.copy(variant = v) } },
|
||||||
|
onEnabledToggle = { updateStory { it.copy(isEnabled = !it.isEnabled) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val tangemFooStoryFactory
|
||||||
|
get() = storyPageFactory(StateUpdater<TangemFooStory>::build)
|
||||||
|
```
|
||||||
|
Stateless: `internal val tangemFooStoryFactory: StoryPageFactory = StoryPageFactory { TangemFooStory }`
|
||||||
|
|
||||||
|
### C. Create `<page-dir>/TangemFooStory.kt`
|
||||||
|
|
||||||
|
`@Composable internal fun TangemFooStory(state: TangemFooStory, modifier: Modifier = Modifier)`
|
||||||
|
(drop `state` for stateless). Follow STORYBOOK.md design guidelines: live preview pinned at the top
|
||||||
|
in a `Column`, controls scrolling below. Reuse the chip-selector / toggle-row patterns from
|
||||||
|
`page/ds/button/TangemButtonStory.kt` (its `Section`, `ChipGrid`, `Chip`, `ToggleRow` are private —
|
||||||
|
copy the ones you need into the new file). Use representative text, not "Btn".
|
||||||
|
|
||||||
|
### D. Register routing in `ui/StoryBookScreen.kt`
|
||||||
|
|
||||||
|
Add both imports (entity + page composable share the simple name — Kotlin resolves them by position):
|
||||||
|
```kotlin
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemFooStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.foo.TangemFooStory
|
||||||
|
```
|
||||||
|
Add a branch to the `when (storyState)`:
|
||||||
|
```kotlin
|
||||||
|
is TangemFooStory -> TangemFooStory(state = storyState) // stateless: TangemFooStory -> TangemFooStory()
|
||||||
|
```
|
||||||
|
|
||||||
|
### E. Register in the list screen (target-specific)
|
||||||
|
|
||||||
|
- **Target 1 (DS):** in `page/ds/DsComponentsListScreen.kt` add the factory import and a row to
|
||||||
|
`buildDsStories()`:
|
||||||
|
```kotlin
|
||||||
|
DsStoryItem(title = "🔘 TangemFoo", factory = tangemFooStoryFactory),
|
||||||
|
```
|
||||||
|
- **Target 2 (other):** in `ui/StoryBookListScreen.kt` add the factory import and a row to
|
||||||
|
`buildStories()`:
|
||||||
|
```kotlin
|
||||||
|
StoryItem(title = "🔘 Foo", factory = tangemFooStoryFactory),
|
||||||
|
```
|
||||||
|
|
||||||
|
**Every title must start with an emoji** matching the component category (🔘 buttons, 🏷️ badge,
|
||||||
|
📋 row, ⏳ loader, 🔤 typography, 🔍 search, 🧭 navigation, 💀 placeholder, ✨ effects, 🪙 token…).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :features:tester:impl:assembleGoogleDebug
|
||||||
|
```
|
||||||
|
Detekt runs via the convention plugin; keep `@file:Suppress("MagicNumber")` on showcase files that use
|
||||||
|
literal dp/colors (the button story does this). Then run the app, open Tester → Storybook → (DS
|
||||||
|
Components →) your entry, and confirm the preview + every control works.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Read the real component; every meaningful parameter has a control.
|
||||||
|
- [ ] Entity in `StoryBookPage.kt` extends the correct supertype (`DsStoryBookPage` for DS, else `StoryBookPage`).
|
||||||
|
- [ ] `Build.kt` factory name is `<camelCaseName>StoryFactory`.
|
||||||
|
- [ ] Page composable shares the entity's simple name; both imported in `StoryBookScreen.kt`.
|
||||||
|
- [ ] `when` branch added in `StoryBookScreen.kt` (`is` prefix for stateful, bare for stateless).
|
||||||
|
- [ ] Registered in the correct list screen with an emoji-prefixed title.
|
||||||
|
- [ ] Live preview pinned at top, controls below (STORYBOOK.md layout rule).
|
||||||
|
- [ ] `:features:tester:impl:assembleGoogleDebug` passes.
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
name: analyze-logs
|
name: analyze-logs
|
||||||
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
|
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
|
||||||
allowed-tools: Read, Grep
|
allowed-tools: Read, Grep
|
||||||
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf]
|
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] [--no-secrets-audit]
|
||||||
---
|
---
|
||||||
|
|
||||||
Analyze the Tangem app user log file at path: `$ARGUMENTS`
|
Analyze the Tangem app user log file at path: `$ARGUMENTS`
|
||||||
|
|
@ -108,12 +108,37 @@ Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (dev
|
||||||
- `MainActivity.*onNewIntent` — deep link or push notification
|
- `MainActivity.*onNewIntent` — deep link or push notification
|
||||||
- `CardSDK_Session.*start card session` — NFC session starts
|
- `CardSDK_Session.*start card session` — NFC session starts
|
||||||
|
|
||||||
|
**Secrets & PII Audit (full file, head_limit: 20 each, -n: true):**
|
||||||
|
|
||||||
|
Skip this entire group if `--no-secrets-audit` is in arguments.
|
||||||
|
|
||||||
|
- API key leak in URL: `[?&](api[_-]?key|apiKey|access_token|token|secret)=(?!\*+)[^&\s]{8,}`
|
||||||
|
- Bearer token: `Bearer\s+[A-Za-z0-9._\-]{20,}`
|
||||||
|
- JWT: `eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+`
|
||||||
|
- Authorization header: `(?i)authorization:\s*\S+`
|
||||||
|
- Critical PII in JSON: `"(privateKey|mnemonic|seedPhrase|private_key|seed_phrase)"\s*:\s*"[^"]+"`
|
||||||
|
- card_public_key in JSON: `"card_public_key"\s*:\s*"[^"]{40,}"`
|
||||||
|
- FCM push token: `:APA91[A-Za-z0-9_\-]{100,}`
|
||||||
|
- xprv/tprv extended private key: `\b[xytzuv]prv[A-Za-z0-9]{100,}`
|
||||||
|
- Suspicious long hex in URL path: `https?://[^?\s]+/[A-Fa-f0-9]{32,}\b`
|
||||||
|
- Masking health check: count of `\*{6,}` — if 0 in a build that should mask, flag pipeline broken
|
||||||
|
|
||||||
**Error filtering:** When processing error results, skip these noisy matches:
|
**Error filtering:** When processing error results, skip these noisy matches:
|
||||||
- `java.io.IOException: Canceled` — normal request cancellation
|
- `java.io.IOException: Canceled` — normal request cancellation
|
||||||
- `HttpException(code=304` — HTTP "Not Modified"
|
- `HttpException(code=304` — HTTP "Not Modified"
|
||||||
- Bare stacktrace lines starting with `\tat`
|
- Bare stacktrace lines starting with `\tat`
|
||||||
- `<-- HTTP FAILED: java.io.IOException: Canceled`
|
- `<-- HTTP FAILED: java.io.IOException: Canceled`
|
||||||
|
|
||||||
|
### Step 6.5: Masking Consistency Check
|
||||||
|
|
||||||
|
Skip if `--no-secrets-audit` in arguments. Run sequentially after the parallel batch (needs results from the masked-endpoint grep).
|
||||||
|
|
||||||
|
1. Grep `https?://[^/\s]+/[^\s*]*\*{6,}` (full file, head_limit: 50) — collect all URLs where a path segment is masked
|
||||||
|
2. For each unique `host + path-prefix-before-mask`, derive the prefix string
|
||||||
|
3. For each prefix, Grep the prefix followed by a non-`*` character (`<prefix>[^*\s]`, head_limit: 20)
|
||||||
|
- If hits found → masking inconsistency: same endpoint has both masked and unmasked variants
|
||||||
|
- Record the prefix, count of masked hits, count of unmasked hits, first unmasked line number
|
||||||
|
|
||||||
### Step 7: Deep Dive
|
### Step 7: Deep Dive
|
||||||
|
|
||||||
For each significant error found above:
|
For each significant error found above:
|
||||||
|
|
@ -207,6 +232,37 @@ Structure your report EXACTLY as follows:
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
(chronological: app starts, card sessions, navigation, errors, notable API calls)
|
(chronological: app starts, card sessions, navigation, errors, notable API calls)
|
||||||
|
|
||||||
|
## Secrets & PII Audit
|
||||||
|
|
||||||
|
Omit this section entirely if `--no-secrets-audit` was passed.
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
- Total masked tokens (`******`) in log: **N**
|
||||||
|
- If N = 0 in a build expected to mask, flag: "masking pipeline may be broken"
|
||||||
|
|
||||||
|
### Confirmed Leaks (CRITICAL / HIGH)
|
||||||
|
| Line | Severity | Type | Matched (first 16 chars + `…`) | Context |
|
||||||
|
|------|----------|------|--------------------------------|---------|
|
||||||
|
|
||||||
|
### Masking Inconsistencies
|
||||||
|
| Endpoint Prefix | Masked Hits | Unmasked Hits | First Unmasked Line |
|
||||||
|
|-----------------|-------------|---------------|---------------------|
|
||||||
|
|
||||||
|
### Suspected Leaks (MEDIUM / LOW)
|
||||||
|
| Line | Severity | Type | Pattern Matched | Why Suspect |
|
||||||
|
|------|----------|------|-----------------|-------------|
|
||||||
|
|
||||||
|
**Severity legend:**
|
||||||
|
- **CRITICAL** — private key / mnemonic / xprv in clear text
|
||||||
|
- **HIGH** — API key / bearer / JWT / card_public_key visible
|
||||||
|
- **MEDIUM** — push token, card_id, persistent identifiers
|
||||||
|
- **LOW** — heuristic patterns that may be false positives (tx hash, content hash)
|
||||||
|
|
||||||
|
**Output rules:**
|
||||||
|
- Never include the full matched value — always truncate to 16 chars + `…`
|
||||||
|
- For LOW severity, add a "Why Suspect" column explaining typical false positives
|
||||||
|
- Skip matches from these known-public Tangem endpoints: `/v1/coins/settings`, `/v1/geo`, `/v1/currencies`, `/v1/hot_crypto`
|
||||||
|
|
||||||
## Analysis Summary
|
## Analysis Summary
|
||||||
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
|
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
|
||||||
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
|
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
|
||||||
|
|
|
||||||
160
.claude/skills/write-ui-test/SKILL.md
Normal file
160
.claude/skills/write-ui-test/SKILL.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
---
|
||||||
|
name: write-ui-test
|
||||||
|
description: Write a Kaspresso/Compose instrumentation UI test for the Tangem Android app following project conventions — test class shape, page-object locations, Allure step naming, WireMock scenario setup, synchronization, and meaningful assertions. Covers known Compose traps (PullToRefreshBox swipe, TangemHoldToConfirmButton, Decompose lifecycle, hot-wallet access code) and the build/run/debug flow. Use when the user asks to write, add, port, or fix an instrumentation / androidTest / UI test, a page object, or a test scenario ("напиши UI-тест", "добавь инструментальный тест", "напиши тест в androidTest", "page object", "автотест на экран").
|
||||||
|
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent
|
||||||
|
argument-hint: [screen/flow or TC# to cover, e.g. "TangemPay freeze card"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Write an instrumentation (androidTest) UI test for the Tangem Android app. These conventions are
|
||||||
|
enforced by reviewers (tnagmetulla, dpodoynikov) — applying them up front skips a review round.
|
||||||
|
|
||||||
|
This is an **interactive** skill: if scope is ambiguous (which screen, which flow, what the final
|
||||||
|
assertion should verify), ask before writing. Do not invent UI text or test tags — read the real
|
||||||
|
production source and reuse existing patterns.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
Use for instrumented UI tests under `app/src/androidTest/` (Kaspresso + Kakao-Compose), page objects,
|
||||||
|
and test scenarios. **Not** for JVM/Robolectric unit tests (`testDebugUnitTest`) — those follow a
|
||||||
|
different setup.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Clarify scope.** Which screen/flow, which Allure TC#, and what the *final assertion* verifies.
|
||||||
|
Ask if any of these is unclear.
|
||||||
|
2. **Find an existing sibling test to mirror.** Grep `app/src/androidTest/` for a test on a similar
|
||||||
|
screen (e.g. `SendViaSwapTest`). Match its structure rather than inventing one. Read the real
|
||||||
|
production composable to get the actual `testTag`s and string resources — never guess UI text.
|
||||||
|
3. **Locate / extend page objects** in `com/tangem/screens/` (see Locations). Add new ones there,
|
||||||
|
never inside the scenario or test file.
|
||||||
|
4. **Set up WireMock scenarios** in the *test body* if the flow depends on backend state
|
||||||
|
(see `reference/running-and-debugging.md`).
|
||||||
|
5. **Write the test** per Conventions below.
|
||||||
|
6. **Build BOTH APKs, install, run, and classify the result** correctly — Allure post-run hook
|
||||||
|
failures are not test failures (see `reference/running-and-debugging.md`).
|
||||||
|
|
||||||
|
## Porting a test from iOS
|
||||||
|
|
||||||
|
When the user asks to **port** an iOS test to Android:
|
||||||
|
|
||||||
|
- **Default to the sibling iOS repo `../tangem-app-ios/`** (next to `tangem-app-android`). If that path
|
||||||
|
doesn't exist, **ask the user** where the iOS repo is — don't guess.
|
||||||
|
- iOS UI tests live under `TangemUITests/`; look there for the source test, its page objects
|
||||||
|
(`*Screen`), and accessibility identifiers (`*AccessibilityIdentifiers`).
|
||||||
|
- Port the *intent and steps*, not the API. Map the iOS stack to the Android one:
|
||||||
|
XCUITest/accessibility identifiers → Compose `testTag`; iOS `*Screen` page objects → Kotlin page
|
||||||
|
objects in `com/tangem/screens/`; XCTest assertions → Kaspresso/Truth assertions. Re-derive the real
|
||||||
|
Android `testTag`s and string resources from production source — never reuse iOS identifier strings.
|
||||||
|
- **Card/wallet mock mapping — where iOS uses `wallet2`, Android uses the default `Wallet`**
|
||||||
|
(`openMainScreen()` with no `productType` → `ProductType.Wallet`). Do NOT port iOS `.wallet2` to
|
||||||
|
`ProductType.Wallet2`. Other cards map directly: iOS `.twin` → `ProductType.Twins`, `.xrpNote` →
|
||||||
|
`ProductType.Note`, `.four12` → `Firmware412MockContent` (via the `mockContent` param). `ProductType.Wallet2`
|
||||||
|
exists but is a distinct Wallet-2.0-card case, not the iOS-`wallet2` analog.
|
||||||
|
- **A wallet has no balance until you sync.** The default `Wallet` mock starts with missing derivations
|
||||||
|
("Some addresses are missing"); the fiat balance shows `—` until you call `synchronizeAddresses()` after
|
||||||
|
`openMainScreen()` (mirror `TotalBalanceUpdateTest`). Any test asserting a balance/fiat-equivalent must
|
||||||
|
sync first, then `waitUntil` the value loads — balances re-load asynchronously (e.g. after an app-currency
|
||||||
|
change the equivalent repaints with a delay).
|
||||||
|
- The WireMock scenarios are usually shared across platforms, but the branch may differ
|
||||||
|
(see `reference/running-and-debugging.md`).
|
||||||
|
|
||||||
|
## Conventions (must-follow)
|
||||||
|
|
||||||
|
### Test class shape
|
||||||
|
|
||||||
|
- **Test method names are camelCase and always end with `Test`** (e.g. `groupTokensTest()`,
|
||||||
|
`renameWalletTest()`). Never use `snake_case` and never the unit-test `GIVEN … WHEN … THEN …`
|
||||||
|
backtick phrasing — that GWT convention is for JVM unit tests only, not instrumentation tests.
|
||||||
|
- **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.
|
||||||
|
- **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
|
||||||
|
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) — 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
|
||||||
|
|
||||||
|
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 {}`, 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. Includes how to find app-side root causes when the UI fails
|
||||||
|
silently (the app log in `files/log.txt`, and the WireMock journal).
|
||||||
246
.claude/skills/write-ui-test/reference/compose-traps.md
Normal file
246
.claude/skills/write-ui-test/reference/compose-traps.md
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
# 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`.
|
||||||
|
|
||||||
|
## Asserting enabled/disabled on a `Modifier.clickable` row
|
||||||
|
|
||||||
|
When a settings/list row puts `Modifier.clickable(enabled = isClickable, ...)` on the row **container**
|
||||||
|
(not the title `Text`), the enabled/disabled state lives on that container; the child Texts only carry
|
||||||
|
`testTag`/text. So `assertIsEnabled()` / `assertIsNotEnabled()` must target the container, matched by a
|
||||||
|
descendant text — not the title node itself.
|
||||||
|
|
||||||
|
Match the container in BOTH states with **click-action OR disabled-semantics**. Do NOT rely on
|
||||||
|
`hasClickAction()` alone: depending on the Compose version a `clickable(enabled = false)` row may not
|
||||||
|
expose an onClick action, so a `hasClickAction()`-only matcher finds no node and `assertIsNotEnabled()`
|
||||||
|
fails with "No node found".
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import androidx.compose.ui.test.hasClickAction as withClickAction
|
||||||
|
import androidx.compose.ui.test.isNotEnabled as withDisabled
|
||||||
|
|
||||||
|
val row: KNode = child {
|
||||||
|
addSemanticsMatcher(withClickAction() or withDisabled()) // matches enabled AND disabled rows
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.row_title))) // narrows to the specific row
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
// enabled card: row.assertIsEnabled() ; disabled card: row.assertIsNotEnabled()
|
||||||
|
```
|
||||||
|
|
||||||
|
## `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 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`).
|
||||||
|
|
||||||
|
## Touch auto-scroll gets hijacked by a nested-scroll container (e.g. a bottom sheet)
|
||||||
|
|
||||||
|
When a screen hosts a nested-scroll container (a Material3 bottom sheet, `PullToRefreshBox`), Kakao's
|
||||||
|
**touch-based** auto-scroll toward a below-the-fold target can be consumed by that container instead —
|
||||||
|
expanding the sheet over the content, so the next click lands on the wrong element.
|
||||||
|
|
||||||
|
- **Scroll with semantics, not touch:** `onNode(CONTAINER).performScrollToNode(matcher)` issues a
|
||||||
|
`ScrollToIndex` action that does NOT engage nested scroll.
|
||||||
|
- **Don't `device.pressBack()` to collapse the sheet** on a root screen — its `BackHandler` only fires
|
||||||
|
when already expanded, races the press, and back often falls through and quits the app.
|
||||||
|
|
||||||
|
## A perpetually animating screen keeps Compose non-idle → idle-synced actions flake
|
||||||
|
|
||||||
|
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`). **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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Terminal screen never reaches Compose-idle: self-feeding `StateFlow` loop
|
||||||
|
|
||||||
|
A screen whose model writes a fresh state back into the same `StateFlow` it observes will recompose
|
||||||
|
forever, so **any** Compose/Espresso assertion on it times out with `ComposeNotIdleException`
|
||||||
|
(`autoAdvance=true`) or `AppNotIdleException` "last message = DispatchedContinuation target=Handler"
|
||||||
|
(`autoAdvance=false`). The classic shape (hit on the send-v2 `ConfirmSuccess` screen, [REDACTED_TASK_KEY]):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
combine(uiState, currentRoute)
|
||||||
|
.onEach { (state, _) -> callback.onResult(state.copy(navigationUM = NavigationUM.Content(onClick = { … }))) }
|
||||||
|
// callback writes back into uiState → emits again → onEach again → ∞
|
||||||
|
```
|
||||||
|
|
||||||
|
`NavigationUM.Content` is a `data class` whose fields are **lambdas**, recreated every pass → `equals`
|
||||||
|
is always false → `StateFlow` never dedups → unthrottled loop. No test-side workaround helps (it's an
|
||||||
|
app loop): not `flakySafely`, not longer timeouts, not mocking external sources, not UiAutomator
|
||||||
|
(touching the window mid-async-signing aborts the send). **Fix is app-side** — emit once (guard the
|
||||||
|
`filter`/`distinctUntilChanged` so the self-induced field is ignored). If you see `ComposeNotIdle` on a
|
||||||
|
*static-looking* success/result screen, suspect this before blaming background polling.
|
||||||
|
|
||||||
|
## Animation-gated content via `delay()` never appears under the test clock
|
||||||
|
|
||||||
|
Compose UI tests run inside `runTest` — **virtual time**. A `LaunchedEffect { delay(600); visible = true }`
|
||||||
|
that gates the screen body behind `AnimatedVisibility(visible)` will *never* reveal it once the
|
||||||
|
composition is otherwise idle: `waitForIdle` sees no pending frame-clock awaiters, so it stops without
|
||||||
|
advancing the virtual clock to the delay's deadline. The body stays empty (you see only the parent
|
||||||
|
chrome, e.g. a top-bar close icon), the `testTag` is absent, and `assertIsDisplayed` fails as
|
||||||
|
"not displayed" — **after** burning the full wall-clock timeout. `flakySafely(LONG)` does NOT help:
|
||||||
|
it retries in wall-clock time while virtual time stays frozen.
|
||||||
|
|
||||||
|
Distinguish from the loop trap above: a `delay`-gate gives a clean `AssertionError: … not displayed`
|
||||||
|
(idle is reached, node just isn't there); the loop gives a `ComposeNotIdle`/`AppNotIdle` timeout.
|
||||||
|
|
||||||
|
Fixes: (a) app-side — drop the pre-`delay`, let the enter transition (`slideIn`/`fadeIn`) play on the
|
||||||
|
frame clock (which `autoAdvance` *does* pump); or (b) put the asserted `testTag` on a node **outside**
|
||||||
|
the `AnimatedVisibility` so the container exists from frame 0. A plain coroutine `delay` is not a
|
||||||
|
frame-clock awaiter, so advancing frames won't fire it — only `advanceTimeBy` (with `autoAdvance=false`)
|
||||||
|
would, which is fragile. Prefer the app-side fix.
|
||||||
|
|
||||||
|
## 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.
|
||||||
183
.claude/skills/write-ui-test/reference/running-and-debugging.md
Normal file
183
.claude/skills/write-ui-test/reference/running-and-debugging.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
# Building, running, and debugging instrumentation tests
|
||||||
|
|
||||||
|
## Both APKs matter
|
||||||
|
|
||||||
|
Instrumentation tests need TWO APKs:
|
||||||
|
|
||||||
|
- `:app:assembleGoogleMocked` → `app-google-mocked.apk` — production code under test
|
||||||
|
- `:app:assembleGoogleMockedAndroidTest` → `app-google-mocked-androidTest.apk` — the test code
|
||||||
|
|
||||||
|
If you change production code and rebuild only the test APK, **the installed main APK stays old** and
|
||||||
|
your fix doesn't take effect. Symptom: "the fix doesn't help" — except it does, you just ran the
|
||||||
|
unfixed build.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build both
|
||||||
|
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
|
||||||
|
# Install each
|
||||||
|
adb install -r -t <path-to-app-google-mocked.apk>
|
||||||
|
adb install -r -t <path-to-app-google-mocked-androidTest.apk>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run a single test (manual)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell pm clear com.tangem.wallet.mocked
|
||||||
|
curl -X POST http://localhost:8081/__admin/scenarios/reset
|
||||||
|
adb shell am instrument -w \
|
||||||
|
-e class "com.tangem.tests.tangempay.TangemPayTest#freezeUnfreezeCard_TogglesCardState" \
|
||||||
|
com.tangem.wallet.mocked.test/com.tangem.common.HiltTestRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Harness: orchestrator vs. raw `am instrument`
|
||||||
|
|
||||||
|
The app is configured `execution = "ANDROIDX_TEST_ORCHESTRATOR"` (`app/build.gradle.kts`). The orchestrator
|
||||||
|
runs **each test method in its own process** (and can clear app data between them). It is still 100%
|
||||||
|
local — it runs on the same emulator; nothing remote about it.
|
||||||
|
|
||||||
|
Raw `adb shell am instrument` runs **all selected tests in one shared process**, which has two failure
|
||||||
|
modes that look like test bugs but aren't:
|
||||||
|
|
||||||
|
- Running several tests in one invocation → `IllegalStateException: There are multiple DataStores active
|
||||||
|
for the same file` mid-run. Run them one at a time (with `pm clear` between) if you must use raw
|
||||||
|
`am instrument`.
|
||||||
|
- Tests that re-scan the card inside **Card/Device Settings** (the "Scan card or ring" gate) →
|
||||||
|
`IllegalStateException: Tangem SDK is null after re-registering with foreground activity`. The existing
|
||||||
|
`ResetCardTest` crashes identically under raw `am instrument`. These only pass via the orchestrator.
|
||||||
|
|
||||||
|
**Prefer the orchestrator** (it's what CI/Marathon use). Run a class or method through Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :app:connectedGoogleMockedAndroidTest \
|
||||||
|
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest
|
||||||
|
# or a single method: ...class=com.tangem.tests.DetailsTest#someTest
|
||||||
|
# or several classes: ...class=com.tangem.tests.DetailsTest,com.tangem.tests.SecurityModeTest
|
||||||
|
```
|
||||||
|
|
||||||
|
Gradle installs both APKs, runs via the orchestrator, then **uninstalls them** — so a following raw
|
||||||
|
`am instrument` reports `Unable to find instrumentation info`; reinstall both APKs first. Read results
|
||||||
|
from the JUnit XML (authoritative pass/fail counts), not just stdout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -t app/build/outputs/androidTest-results/connected/mocked/flavors/google/*.xml | head -1
|
||||||
|
# inspect tests="…" failures="…" errors="…" skipped="…" and the <testcase>/<failure> nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running against local WireMock
|
||||||
|
|
||||||
|
Every instrumentation test runs with `ApiEnvironment.MOCK` (forced in `BaseTestCase.setupHooks`), so the
|
||||||
|
app's API base URLs point at `wiremock.tests-d.com` — i.e. tests **always** talk to WireMock, never the
|
||||||
|
real backend. By default that's the **remote** WireMock at `wiremock.tests-d.com`. To use a **local**
|
||||||
|
WireMock instead, pass `wiremockBaseUrl`: `WireMockRedirectInterceptor` then rewrites every
|
||||||
|
`wiremock.tests-d.com` request to your local instance.
|
||||||
|
|
||||||
|
Emulator addressing matters — `localhost` inside an emulator is the **emulator itself**, not your host:
|
||||||
|
|
||||||
|
- Use the host alias **`http://10.0.2.2:8081`** (no extra setup), **or**
|
||||||
|
- `http://localhost:8081` **with** `adb reverse tcp:8081 tcp:8081` run first.
|
||||||
|
|
||||||
|
Pass it through the orchestrator (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8081/__admin/scenarios/reset # start clean
|
||||||
|
./gradlew :app:connectedGoogleMockedAndroidTest \
|
||||||
|
-Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest \
|
||||||
|
-Pandroid.testInstrumentationRunnerArguments.wiremockBaseUrl=http://10.0.2.2:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
(Raw `am instrument` equivalent: `-e wiremockBaseUrl http://10.0.2.2:8081` — subject to the harness
|
||||||
|
caveats above.)
|
||||||
|
|
||||||
|
**If a screen hangs / you get `ComposeNotIdleException` (infinite recomposition):** that usually means a
|
||||||
|
request the app made wasn't served (endless retry/loading), *not* a test bug. Ask WireMock what it
|
||||||
|
didn't match — this is the smoking gun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8081/__admin/requests/unmatched | jq '.requests[] | "\(.method) \(.url)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
`unmatched: 0` means the URL plumbing is correct and local WireMock served everything — look elsewhere
|
||||||
|
(harness/emulator) for the hang. A non-empty list names exactly which mapping (or scenario state) the
|
||||||
|
local instance is missing.
|
||||||
|
|
||||||
|
## When the UI fails silently, the cause is usually app-side — two places to look
|
||||||
|
|
||||||
|
A screen failing silently with correct locators (fee shows "—", a banner never appears, a button stays
|
||||||
|
disabled) is usually missing mock *data* or an app-side gate, not a test bug. Two diagnostics find it:
|
||||||
|
|
||||||
|
- **The app's own log is in `files/log.txt`, not logcat** — the mocked build routes `TangemLogger` to a
|
||||||
|
file, so `adb logcat` shows nothing. Fastest path to a root cause (e.g. it surfaced
|
||||||
|
`IllegalStateException: No native currency found` → a native coin missing from the mock):
|
||||||
|
```bash
|
||||||
|
adb exec-out run-as <pkg> cat files/log.txt | grep -iE "Error|Exception|<feature>"
|
||||||
|
```
|
||||||
|
- **The WireMock journal separates "mock missing" from "app never asked"** —
|
||||||
|
`/__admin/requests/unmatched` finds missing mappings, but if `unmatched=0` *and* the expected request
|
||||||
|
is also absent from the full log (`/__admin/requests`), the app never issued it (a data/state gate) →
|
||||||
|
fix the mock data or the app, not the mappings.
|
||||||
|
|
||||||
|
## "UiAutomationService already registered" — retry, it's not a failure
|
||||||
|
|
||||||
|
Back-to-back `am instrument` runs sometimes fail instantly with `UiAutomationService … already
|
||||||
|
registered!` — a teardown race between runs, not a test failure. Retry. (The orchestrator avoids it by
|
||||||
|
spacing runs — another reason to confirm a flaky-looking suite via the orchestrator, not raw
|
||||||
|
`am instrument`.)
|
||||||
|
|
||||||
|
## Classify the result — Allure noise vs. real failure
|
||||||
|
|
||||||
|
After `pm clear`, `/data/user/0/<pkg>/files/original_screenshots` doesn't exist →
|
||||||
|
`AllureResultsHack.testRunFinished` throws `NoSuchFileException` → reported as
|
||||||
|
`Tests run: 1, Failures: 1` with a stack trace starting at `AllureResultsHack`. **This is a post-run
|
||||||
|
hook failure, NOT a test logic failure.**
|
||||||
|
|
||||||
|
Distinguish:
|
||||||
|
- First stack frame is `AllureResultsHack.testRunFinished` → infra hook noise; ignore it.
|
||||||
|
- Kaspresso step logs show all `SUCCEED` for steps 1..N → the test passed.
|
||||||
|
- A REAL failure shows `java.lang.AssertionError` inside the test's own classes
|
||||||
|
(e.g. `at com.tangem.tests.X.foo$lambda…`). When auto-classifying CLI output, key off the presence
|
||||||
|
of `java.lang.AssertionError` vs. only `original_screenshots`.
|
||||||
|
|
||||||
|
## `@Ignore` on instrumentation tests
|
||||||
|
|
||||||
|
- Pattern: `@Ignore("https://tangem.atlassian.net/browse/AND-XXXXX")` above `@Test`.
|
||||||
|
- When ignored, `am instrument -e class …` reports `OK (0 tests)` with `Tests run: 0`
|
||||||
|
(NOT `Skipped: 1`). Auto-detection should match the zero-test count.
|
||||||
|
|
||||||
|
## WireMock cheatsheet
|
||||||
|
|
||||||
|
Without a `wiremockBaseUrl` arg the app hits the **remote** WireMock (`wiremock.tests-d.com`); pass the
|
||||||
|
arg to redirect to a local instance (see "Running against local WireMock"). Default local port: `8081`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set a scenario state — PUT, not POST
|
||||||
|
curl -X PUT http://localhost:8081/__admin/scenarios/<name>/state \
|
||||||
|
-H "Content-Type: application/json" -d '{"state":"<state>"}'
|
||||||
|
|
||||||
|
# Reset all scenarios
|
||||||
|
curl -X POST http://localhost:8081/__admin/scenarios/reset
|
||||||
|
|
||||||
|
# Inspect
|
||||||
|
curl http://localhost:8081/__admin/mappings | jq
|
||||||
|
curl http://localhost:8081/__admin/scenarios | jq '.scenarios[] | {name, state}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mocks repo: default to the sibling directory `../tangem-api-mocks/` (i.e. next to
|
||||||
|
`tangem-app-android`). If that path doesn't exist, **ask the user** where the mocks repo is rather
|
||||||
|
than guessing.
|
||||||
|
- The repo is **branch-per-suite** — dozens of feature branches (e.g. `send-via-swap-p1`,
|
||||||
|
`account-creation`, `swap-express-mocks`, `android-tangem-pay-mocks`). There is no universal
|
||||||
|
default branch; check out the one the suite under test expects. If it's unclear which branch holds
|
||||||
|
the mappings for your flow, ask the user. Mappings live under `mocks/mappings/`, response bodies
|
||||||
|
under `mocks/__files/`.
|
||||||
|
- **State transitions are atomic per `requiredScenarioState`.** If a scenario defines an `AfterDeposit`
|
||||||
|
mapping for `/customer/balance` but not `/customer/me`, a request to `/customer/me` after switching
|
||||||
|
to `AfterDeposit` falls through. Check *both* endpoints when an "after" assertion fails.
|
||||||
|
|
||||||
|
## Misc
|
||||||
|
|
||||||
|
- `./gradlew unitTest` aggregates all debug/googleDebug + JVM-module tests — faster than per-module
|
||||||
|
tasks for verifying a broad change (but it's for *unit* tests, not instrumentation).
|
||||||
|
- Detekt config lives in the `tangem-android-tools` git submodule — look there before assuming a local
|
||||||
|
`.detekt.yml`.
|
||||||
|
- Path discipline: stay at the repo root; `cd` into the mocks repo only when needed and prefer absolute
|
||||||
|
paths (the shell session resets cwd).
|
||||||
|
|
@ -9,6 +9,11 @@
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
||||||
|
},
|
||||||
|
"notion": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,16 +107,13 @@ configurations.all {
|
||||||
configurations.androidTestImplementation {
|
configurations.androidTestImplementation {
|
||||||
exclude(module = "protobuf-lite")
|
exclude(module = "protobuf-lite")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.domain.legacy)
|
implementation(projects.domain.legacy)
|
||||||
implementation(projects.libs.blockchainSdk)
|
implementation(projects.libs.blockchainSdk)
|
||||||
implementation(projects.domain.account)
|
implementation(projects.domain.account)
|
||||||
implementation(projects.domain.account.status)
|
implementation(projects.domain.account.status)
|
||||||
|
implementation(projects.domain.addressBook)
|
||||||
|
implementation(projects.domain.appsflyer)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
implementation(projects.domain.core)
|
implementation(projects.domain.core)
|
||||||
api(projects.domain.common)
|
api(projects.domain.common)
|
||||||
|
|
@ -236,6 +233,8 @@ dependencies {
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common.ui)
|
||||||
|
|
||||||
/** Features */
|
/** Features */
|
||||||
|
implementation(projects.features.addressBook.api)
|
||||||
|
implementation(projects.features.addressBook.impl)
|
||||||
implementation(projects.features.rating.impl)
|
implementation(projects.features.rating.impl)
|
||||||
implementation(projects.features.referral.impl)
|
implementation(projects.features.referral.impl)
|
||||||
implementation(projects.features.referral.domain)
|
implementation(projects.features.referral.domain)
|
||||||
|
|
@ -255,8 +254,8 @@ dependencies {
|
||||||
implementation(projects.features.tokendetails.impl)
|
implementation(projects.features.tokendetails.impl)
|
||||||
implementation(projects.features.manageTokens.api)
|
implementation(projects.features.manageTokens.api)
|
||||||
implementation(projects.features.manageTokens.impl)
|
implementation(projects.features.manageTokens.impl)
|
||||||
implementation(projects.features.sendV2.api)
|
implementation(projects.features.send.api)
|
||||||
implementation(projects.features.sendV2.impl)
|
implementation(projects.features.send.impl)
|
||||||
implementation(projects.features.qrScanning.api)
|
implementation(projects.features.qrScanning.api)
|
||||||
implementation(projects.features.qrScanning.impl)
|
implementation(projects.features.qrScanning.impl)
|
||||||
implementation(projects.features.staking.api)
|
implementation(projects.features.staking.api)
|
||||||
|
|
@ -283,6 +282,8 @@ dependencies {
|
||||||
implementation(projects.features.onboardingV2.impl)
|
implementation(projects.features.onboardingV2.impl)
|
||||||
implementation(projects.features.stories.api)
|
implementation(projects.features.stories.api)
|
||||||
implementation(projects.features.stories.impl)
|
implementation(projects.features.stories.impl)
|
||||||
|
implementation(projects.features.survey.api)
|
||||||
|
implementation(projects.features.survey.impl)
|
||||||
implementation(projects.features.txhistory.api)
|
implementation(projects.features.txhistory.api)
|
||||||
implementation(projects.features.txhistory.impl)
|
implementation(projects.features.txhistory.impl)
|
||||||
implementation(projects.features.biometry.api)
|
implementation(projects.features.biometry.api)
|
||||||
|
|
@ -319,6 +320,12 @@ dependencies {
|
||||||
implementation(projects.features.tangempay.main.impl)
|
implementation(projects.features.tangempay.main.impl)
|
||||||
implementation(projects.features.tangempay.onboarding.api)
|
implementation(projects.features.tangempay.onboarding.api)
|
||||||
implementation(projects.features.tangempay.onboarding.impl)
|
implementation(projects.features.tangempay.onboarding.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.onboarding.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.onboarding.api)
|
||||||
|
implementation(projects.features.virtualAccounts.main.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.main.api)
|
||||||
|
implementation(projects.features.virtualAccounts.details.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.details.api)
|
||||||
implementation(projects.features.tokenRecieve.api)
|
implementation(projects.features.tokenRecieve.api)
|
||||||
implementation(projects.features.tokenRecieve.impl)
|
implementation(projects.features.tokenRecieve.impl)
|
||||||
implementation(projects.features.yieldSupply.api)
|
implementation(projects.features.yieldSupply.api)
|
||||||
|
|
@ -422,8 +429,6 @@ dependencies {
|
||||||
/** Testing libraries */
|
/** Testing libraries */
|
||||||
testImplementation(projects.test.core)
|
testImplementation(projects.test.core)
|
||||||
testImplementation(projects.common.test)
|
testImplementation(projects.common.test)
|
||||||
testImplementation(deps.test.junit)
|
|
||||||
testRuntimeOnly(deps.test.junit5.engine)
|
|
||||||
androidTestImplementation(deps.test.junit.android)
|
androidTestImplementation(deps.test.junit.android)
|
||||||
androidTestImplementation(deps.test.espresso)
|
androidTestImplementation(deps.test.espresso)
|
||||||
androidTestImplementation(deps.test.espresso.intents)
|
androidTestImplementation(deps.test.espresso.intents)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||||
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
|
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||||
import com.tangem.tap.MainActivity
|
import com.tangem.tap.MainActivity
|
||||||
|
|
@ -63,6 +64,9 @@ abstract class BaseTestCase : TestCase(
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
|
lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var singleAccountListSupplier: SingleAccountListSupplier
|
||||||
|
|
||||||
private val hiltRule = HiltAndroidRule(this)
|
private val hiltRule = HiltAndroidRule(this)
|
||||||
private val apiEnvironmentRule = ApiEnvironmentRule()
|
private val apiEnvironmentRule = ApiEnvironmentRule()
|
||||||
private val permissionRule = GrantPermissionRule.grant(
|
private val permissionRule = GrantPermissionRule.grant(
|
||||||
|
|
@ -183,9 +187,42 @@ abstract class BaseTestCase : TestCase(
|
||||||
"GASLESS_APPROVAL_ENABLED" to true,
|
"GASLESS_APPROVAL_ENABLED" to true,
|
||||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||||
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
||||||
|
"ASSETS_DISCOVERY_ENABLED" to true,
|
||||||
"VISA_ONBOARDING_ENABLED" to true,
|
"VISA_ONBOARDING_ENABLED" to true,
|
||||||
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
// Version-gated toggles released in versions <= 6.0 — forced on so tests run against the actual
|
||||||
|
// build even when the app version resolves to 1.0.0-SNAPSHOT on CI (then 1.0.0 < x.xx would
|
||||||
|
// disable them). On the releases/6.0 branch every toggle with version <= 6.0 ships enabled.
|
||||||
|
// 5.37
|
||||||
|
"HEDERA_ERC20_ENABLED" to true,
|
||||||
|
// 5.39
|
||||||
|
"STAKING_ETH_ENABLED" to true,
|
||||||
|
"DYNAMIC_ADDRESSES_ENABLED" to true,
|
||||||
|
"SOLANA_TX_HISTORY_ENABLED" to true,
|
||||||
|
"SOLANA_SCALED_UI_AMOUNT_ENABLED" to true,
|
||||||
|
"SWAP_AB_ENABLED" to true,
|
||||||
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
"AND_15310_ADD_FUNDS_STAGE1" to true,
|
||||||
|
"AND_15009_SWAP_PROVIDER_FILTER_ENABLED" to true,
|
||||||
|
"AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING" to true,
|
||||||
|
"AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED" to true,
|
||||||
|
"AND_15103_SWAP_RATE_EXPERIENCE_ENABLED" to true,
|
||||||
|
"AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED" to true,
|
||||||
|
"TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED" to true,
|
||||||
|
// 5.39.2
|
||||||
|
"AND_15154_YIELD_PROMO_ENABLED" to true,
|
||||||
|
// 5.40
|
||||||
|
"TWI_1377_MANAGE_FUNDS" to true,
|
||||||
|
// 6.0
|
||||||
|
"APP_REDESIGN_ENABLED" to true,
|
||||||
|
"TWI_1326_YIELD_MODE_SWAP_ENABLED" to true,
|
||||||
|
"AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED" to true,
|
||||||
|
"AND_15120_SWAP_INTEGRATED_APPROVE" to true,
|
||||||
|
"AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED" to true,
|
||||||
|
"AND_15258_QUICK_TOP_UP_ENABLED" to true,
|
||||||
|
"AND_15368_VISA_PAY_REDESIGN" to true,
|
||||||
|
"AND_15364_VISA_PAY_CARD_CLOSE" to true,
|
||||||
|
"AND_15489_EXPRESS_SHARE_BUTTON_ENABLED" to true,
|
||||||
|
"AND_15235_VISA_MULTIPLE_CARDS" to true,
|
||||||
|
"AND_15715_SWAP_BEST_DEX_RATE_ENABLED" to true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ object TestConstants {
|
||||||
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
|
||||||
const val REFERRAL_API_SCENARIO = "referral_api"
|
const val REFERRAL_API_SCENARIO = "referral_api"
|
||||||
const val QUOTES_API_SCENARIO = "quotes_api"
|
const val QUOTES_API_SCENARIO = "quotes_api"
|
||||||
|
const val CREATE_USER_WALLET_API_SCENARIO = "create_user_wallet_api"
|
||||||
|
const val WALLET_TOKENS_API_SCENARIO = "wallet_tokens_api"
|
||||||
|
const val MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO = "moralis_evm_token_balances_api"
|
||||||
|
const val PROVIDERS_API_SCENARIO = "networks_providers"
|
||||||
|
|
||||||
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
|
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
|
||||||
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
|
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
|
||||||
|
|
@ -60,6 +64,9 @@ object TestConstants {
|
||||||
"bread much nature basic fun iron benefit egg error prosper"
|
"bread much nature basic fun iron benefit egg error prosper"
|
||||||
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
|
const val SVS_SEED_PHRASE_12 = "diagram thunder merit soup muscle amused refuse usual ring couch popular wash"
|
||||||
|
|
||||||
|
const val SEED_PHRASE_HAPPY_PATH =
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
|
||||||
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
const val TANGEM_PAY_ELIGIBILITY_SCENARIO = "tangem_pay_eligibility"
|
||||||
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
const val TANGEM_PAY_ACCESS_CODE = "517384"
|
||||||
}
|
}
|
||||||
|
|
@ -115,5 +115,13 @@ private fun extractText(node: SemanticsNode): String? {
|
||||||
|
|
||||||
private fun parseVolume(node: SemanticsNode): Double? {
|
private fun parseVolume(node: SemanticsNode): Double? {
|
||||||
val text = extractText(node) ?: return null
|
val text = extractText(node) ?: return null
|
||||||
return text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull()
|
val multiplier = when {
|
||||||
|
text.contains('T', ignoreCase = true) -> 1_000_000_000_000.0
|
||||||
|
text.contains('B', ignoreCase = true) -> 1_000_000_000.0
|
||||||
|
text.contains('M', ignoreCase = true) -> 1_000_000.0
|
||||||
|
text.contains('K', ignoreCase = true) -> 1_000.0
|
||||||
|
else -> 1.0
|
||||||
|
}
|
||||||
|
val number = text.replace("[^0-9.]".toRegex(), "").toDoubleOrNull() ?: return null
|
||||||
|
return number * multiplier
|
||||||
}
|
}
|
||||||
|
|
@ -4,13 +4,19 @@ import android.os.SystemClock
|
||||||
import androidx.compose.ui.test.ComposeTimeoutException
|
import androidx.compose.ui.test.ComposeTimeoutException
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||||
|
import io.github.kakaocup.compose.node.core.BaseNode
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
||||||
fun KNode.clickWithAssertion() {
|
fun BaseNode<*>.clickWithAssertion() {
|
||||||
assertIsDisplayed()
|
assertIsDisplayed()
|
||||||
performClick()
|
performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun KNode.clickWhenEnabled() {
|
||||||
|
assertIsEnabled()
|
||||||
|
performClick()
|
||||||
|
}
|
||||||
|
|
||||||
fun KNode.assertTextContainsSafe(
|
fun KNode.assertTextContainsSafe(
|
||||||
text: String,
|
text: String,
|
||||||
substring: Boolean = false,
|
substring: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import androidx.test.uiautomator.By
|
||||||
import androidx.test.uiautomator.Until
|
import androidx.test.uiautomator.Until
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.wallet.R
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_SHORT
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
|
||||||
|
|
||||||
fun BaseTestCase.swipeVertical(
|
fun BaseTestCase.swipeVertical(
|
||||||
direction: SwipeDirection,
|
direction: SwipeDirection,
|
||||||
|
|
@ -31,21 +31,6 @@ fun BaseTestCase.pullToRefresh(steps: Int = 1000) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.swipeMarketsBlock(direction: SwipeDirection) {
|
|
||||||
val searchBarText = device.uiDevice
|
|
||||||
.findObject(By.textContains(getResourceString(R.string.markets_search_header_title)))
|
|
||||||
val bounds = searchBarText.visibleBounds
|
|
||||||
|
|
||||||
val centerX = bounds.centerX()
|
|
||||||
val startY = bounds.centerY()
|
|
||||||
val endY = when (direction) {
|
|
||||||
SwipeDirection.UP -> 50
|
|
||||||
SwipeDirection.DOWN -> device.uiDevice.displayHeight - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
device.uiDevice.swipe(centerX, startY, centerX, endY, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BaseTestCase.openTheAppFromRecents() {
|
fun BaseTestCase.openTheAppFromRecents() {
|
||||||
device.uiDevice.waitForIdle()
|
device.uiDevice.waitForIdle()
|
||||||
|
|
||||||
|
|
@ -113,6 +98,12 @@ fun BaseTestCase.restartApp(packageName: String) {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.clickOnSystemButton(buttonName: String) {
|
||||||
|
device.uiDevice.wait(Until.hasObject(By.text(buttonName)), WAIT_UNTIL_TIMEOUT_SHORT)
|
||||||
|
device.uiDevice.findObject(By.text(buttonName))?.click()
|
||||||
|
?: throw AssertionError("System '$buttonName' button not found")
|
||||||
|
}
|
||||||
|
|
||||||
enum class SwipeDirection {
|
enum class SwipeDirection {
|
||||||
UP, DOWN
|
UP, DOWN
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.common.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for inspecting individual nodes of a BIP-44-style derivation path string
|
||||||
|
* (e.g. one read from `Network.derivationPath` of a token in the domain account model).
|
||||||
|
*/
|
||||||
|
object DerivationPathHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [index1Based]-th node of a derivation path, ignoring the leading `m`.
|
||||||
|
* For "m/44'/0'/1'/0/0": node 1 = "44'", node 3 = "1'", node 5 = "0".
|
||||||
|
*/
|
||||||
|
fun nodeAt(derivationPath: String, index1Based: Int): String {
|
||||||
|
val nodes = derivationPath.removePrefix("m/").split("/")
|
||||||
|
require(index1Based in 1..nodes.size) {
|
||||||
|
"Node #$index1Based is out of range for path '$derivationPath' (${nodes.size} nodes)"
|
||||||
|
}
|
||||||
|
return nodes[index1Based - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
package com.tangem.scenarios
|
package com.tangem.scenarios
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.R
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.screens.accounts.onAccountDetailsScreen
|
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||||
|
import com.tangem.screens.accounts.onAccountInfoEditorScreen
|
||||||
import com.tangem.screens.accounts.onArchivedAccountsScreen
|
import com.tangem.screens.accounts.onArchivedAccountsScreen
|
||||||
import com.tangem.screens.onDetailsScreen
|
import com.tangem.screens.onDetailsScreen
|
||||||
import com.tangem.screens.onDialog
|
import com.tangem.screens.onDialog
|
||||||
import com.tangem.screens.onMainScreenTopBar
|
import com.tangem.screens.onMainScreenTopBar
|
||||||
import com.tangem.screens.onWalletSettingsScreen
|
import com.tangem.screens.onWalletSettingsScreen
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
|
||||||
|
private const val ACCOUNT_POLL_INTERVAL_MS = 500L
|
||||||
|
|
||||||
fun BaseTestCase.openWalletSettingsScreen() {
|
fun BaseTestCase.openWalletSettingsScreen() {
|
||||||
step("Open 'Wallet details' screen") {
|
step("Open 'Wallet details' screen") {
|
||||||
|
|
@ -19,6 +30,15 @@ fun BaseTestCase.openWalletSettingsScreen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.startAccountCreation() {
|
||||||
|
step("Click on 'Add account' button") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Account info editor' screen is displayed") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.openAccountDetails(accountName: String) {
|
fun BaseTestCase.openAccountDetails(accountName: String) {
|
||||||
step("Click on account: '$accountName'") {
|
step("Click on account: '$accountName'") {
|
||||||
onWalletSettingsScreen { accountItem(accountName).clickWithAssertion() }
|
onWalletSettingsScreen { accountItem(accountName).clickWithAssertion() }
|
||||||
|
|
@ -28,6 +48,46 @@ fun BaseTestCase.openAccountDetails(accountName: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.checkUnsavedChangesCreationModal() {
|
||||||
|
step("Assert 'Unsaved changes' alert is displayed") {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Unsaved changes' alert has proper title") {
|
||||||
|
onDialog { title.assertTextContains(getResourceString(R.string.account_unsaved_dialog_title)) }
|
||||||
|
}
|
||||||
|
step("Assert 'Unsaved changes' alert has proper description for account creation") {
|
||||||
|
onDialog {
|
||||||
|
text.assertTextContains(getResourceString(R.string.account_unsaved_dialog_message_create))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Keep editing' button is displayed in alert with proper text") {
|
||||||
|
onDialog { keepEditButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Discard' button is displayed in alert") {
|
||||||
|
onDialog { discardButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.assertUnsavedChangesEditionModal() {
|
||||||
|
step("Assert 'Unsaved changes' alert is displayed") {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Unsaved changes' alert has proper title") {
|
||||||
|
onDialog { title.assertTextContains(getResourceString(R.string.account_unsaved_dialog_title)) }
|
||||||
|
}
|
||||||
|
step("Assert 'Unsaved changes' alert has proper description for account creation") {
|
||||||
|
onDialog {
|
||||||
|
text.assertTextContains(getResourceString(R.string.account_unsaved_dialog_message_edit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Keep editing' button is displayed in alert with proper text") {
|
||||||
|
onDialog { keepEditButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Discard' button is displayed in alert") {
|
||||||
|
onDialog { discardButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.archiveAccount() {
|
fun BaseTestCase.archiveAccount() {
|
||||||
step("Assert 'Archive' button is displayed") {
|
step("Assert 'Archive' button is displayed") {
|
||||||
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
|
onAccountDetailsScreen { archiveAccountButton.assertIsDisplayed() }
|
||||||
|
|
@ -99,4 +159,53 @@ fun BaseTestCase.restoreArchivedAccount(accountName: String) {
|
||||||
.restoreButton.clickWithAssertion()
|
.restoreButton.clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls [singleAccountListSupplier] for the selected wallet until a [Account.CryptoPortfolio] with the given
|
||||||
|
* [derivationIndex] appears with a non-empty token list, then returns it.
|
||||||
|
*
|
||||||
|
* Per-account token derivation paths live in the domain account model
|
||||||
|
* ([Account.CryptoPortfolio.cryptoCurrencies] → [com.tangem.domain.models.network.Network.derivationPath]),
|
||||||
|
* not in the tester-menu "Addresses info" (which reads from the account-agnostic wallet managers store and
|
||||||
|
* only ever shows main/base derivations). Reading the model directly is the reliable source for asserting
|
||||||
|
* per-account derivations.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.awaitCryptoPortfolioAccount(derivationIndex: Int): Account.CryptoPortfolio {
|
||||||
|
val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||||
|
?: error("No selected wallet found")
|
||||||
|
|
||||||
|
var account: Account.CryptoPortfolio? = null
|
||||||
|
runBlocking {
|
||||||
|
withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||||
|
while (true) {
|
||||||
|
val candidate = singleAccountListSupplier.getSyncOrNull(walletId)
|
||||||
|
?.accounts
|
||||||
|
?.filterIsInstance<Account.CryptoPortfolio>()
|
||||||
|
?.firstOrNull { it.derivationIndex.value == derivationIndex }
|
||||||
|
|
||||||
|
if (candidate != null && candidate.cryptoCurrencies.isNotEmpty()) {
|
||||||
|
TangemLogger.i(
|
||||||
|
"Account with derivation index $derivationIndex resolved: " +
|
||||||
|
"${candidate.cryptoCurrencies.size} token(s)",
|
||||||
|
)
|
||||||
|
account = candidate
|
||||||
|
return@withTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(ACCOUNT_POLL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requireNotNull(account) {
|
||||||
|
"Account with derivation index $derivationIndex was not found for wallet $walletId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all derivation paths of tokens whose name equals [tokenName] (case-insensitive) within this account.
|
||||||
|
*/
|
||||||
|
fun Account.CryptoPortfolio.derivationPathsForToken(tokenName: String): List<String> = cryptoCurrencies
|
||||||
|
.filter { it.name.equals(tokenName, ignoreCase = true) }
|
||||||
|
.mapNotNull { it.network.derivationPath.value }
|
||||||
|
|
@ -2,8 +2,6 @@ package com.tangem.scenarios
|
||||||
|
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.performTextInput
|
|
||||||
import androidx.compose.ui.test.waitUntilAtLeastOneExists
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
|
@ -142,12 +140,15 @@ fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String, accessC
|
||||||
|
|
||||||
fun BaseTestCase.synchronizeAddresses(
|
fun BaseTestCase.synchronizeAddresses(
|
||||||
balance: String? = null,
|
balance: String? = null,
|
||||||
isBalanceAvailable: Boolean = true
|
isBalanceAvailable: Boolean = true,
|
||||||
|
assertBalance: Boolean = true,
|
||||||
) {
|
) {
|
||||||
step("Click on 'Synchronize addresses' button") {
|
step("Click on 'Synchronize addresses' button") {
|
||||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!assertBalance) return
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!isBalanceAvailable -> step("Assert wallet balance = '$DASH_SIGN'") {
|
!isBalanceAvailable -> step("Assert wallet balance = '$DASH_SIGN'") {
|
||||||
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
|
onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) }
|
||||||
|
|
@ -175,7 +176,10 @@ fun BaseTestCase.openDeviceSettingsScreen() {
|
||||||
onDetailsScreen { walletNameButton.performClick() }
|
onDetailsScreen { walletNameButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Device settings' button") {
|
step("Click on 'Device settings' button") {
|
||||||
onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() }
|
onWalletSettingsScreen {
|
||||||
|
scrollToDeviceSettings()
|
||||||
|
deviceSettingsButton.clickWithAssertion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,105 +6,41 @@ import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.checkSingleCurrencyMainScreen(
|
fun BaseTestCase.checkSingleCurrencyMainScreen(cardTitle: String) {
|
||||||
cardBlockchain: String,
|
|
||||||
cardTitle: String,
|
|
||||||
withTransactions: Boolean = false,
|
|
||||||
withWalletImage: Boolean = true
|
|
||||||
) {
|
|
||||||
step("Assert card title equal '$cardTitle'") {
|
step("Assert card title equal '$cardTitle'") {
|
||||||
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
||||||
}
|
}
|
||||||
if (withWalletImage) {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
step("Assert card image is displayed") { //TODO: create assertion method for checking images
|
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||||
onMainScreen { walletImage.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert card image is not displayed") {
|
|
||||||
onMainScreen { walletImage.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onMainScreen { receiveButton.assertIsDisplayed() }
|
onMainScreen { transferButton.assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Assert 'Buy' button is displayed") {
|
|
||||||
onMainScreen { buyButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Send' button is displayed") {
|
|
||||||
onMainScreen { sendButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Sell' button is displayed") {
|
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is not displayed") {
|
step("Assert 'Swap' button is not displayed") {
|
||||||
onMainScreen { swapButton.assertIsNotDisplayed() }
|
onMainScreen { swapButton.assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Market Price' on single card main screen is displayed") {
|
|
||||||
onMainScreen { marketPriceBlock().assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Market Price' title equals $cardBlockchain Market Price") {
|
|
||||||
onMainScreen { marketPriceText.assertTextContains("$cardBlockchain Market Price") }
|
|
||||||
}
|
|
||||||
step("Swipe up") {
|
step("Swipe up") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
swipeVertical(SwipeDirection.UP)
|
||||||
}
|
}
|
||||||
if (withTransactions) {
|
|
||||||
step("Assert 'Transactions' block is displayed") {
|
|
||||||
onMainScreen { transactionsExplorerText.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Transactions' title is displayed") {
|
|
||||||
onMainScreen { transactionsTitle.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Explorer' icon is displayed") {
|
|
||||||
onMainScreen { transactionsExplorerIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert empty 'Transactions' block is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlock.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block icon is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block text is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockText.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert empty 'Transactions' block 'Explore' button is displayed") {
|
|
||||||
onMainScreen { emptyTransactionBlockExploreButton.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert 'Add & Manage' button is not displayed") {
|
step("Assert 'Add & Manage' button is not displayed") {
|
||||||
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
|
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.checkMultiCurrencyMainScreen(
|
fun BaseTestCase.checkMultiCurrencyMainScreen(
|
||||||
devicesCount: String,
|
|
||||||
cardTitle: String,
|
cardTitle: String,
|
||||||
withWalletImage: Boolean = true
|
|
||||||
) {
|
) {
|
||||||
step("Assert card title equal '$cardTitle'") {
|
step("Assert card title equal '$cardTitle'") {
|
||||||
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
onMainScreen { walletNameText.assertTextEquals(cardTitle) }
|
||||||
}
|
}
|
||||||
if (withWalletImage) {
|
|
||||||
step("Assert card image is displayed") {
|
|
||||||
onMainScreen { walletImage.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step("Assert card image is not displayed") {
|
|
||||||
onMainScreen { walletImage.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert devices count equal to '$devicesCount'") {
|
|
||||||
onMainScreen { walletDevicesCount.assertTextContains(devicesCount) }
|
|
||||||
}
|
|
||||||
step("Assert 'Add funds' button is displayed") {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
onMainScreen { addFundsButton.assertIsDisplayed() }
|
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is displayed") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
onMainScreen { swapButton.assertIsDisplayed() }
|
onMainScreen { swapButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
onMainScreen { transferButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not displayed") {
|
step("Assert 'Send' button is not displayed") {
|
||||||
onMainScreen { sendButton.assertIsNotDisplayed() }
|
onMainScreen { sendButton.assertIsNotDisplayed() }
|
||||||
|
|
@ -125,8 +61,8 @@ fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean =
|
||||||
step("Assert 'Swap' button is enabled") {
|
step("Assert 'Swap' button is enabled") {
|
||||||
onMainScreen { swapButton.assertIsEnabled() }
|
onMainScreen { swapButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is enabled") {
|
step("Assert 'Transfer' button is enabled") {
|
||||||
onMainScreen { sellButton.assertIsEnabled() }
|
onMainScreen { transferButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
step("Assert 'Add funds' button is not enabled") {
|
step("Assert 'Add funds' button is not enabled") {
|
||||||
|
|
@ -135,8 +71,8 @@ fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean =
|
||||||
step("Assert 'Swap' button is not enabled") {
|
step("Assert 'Swap' button is not enabled") {
|
||||||
onMainScreen { swapButton.assertIsNotEnabled() }
|
onMainScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is not enabled") {
|
step("Assert 'Transfer' button is not enabled") {
|
||||||
onMainScreen { sellButton.assertIsNotEnabled() }
|
onMainScreen { transferButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.screens.onDeviceSettingsScreen
|
import com.tangem.screens.onDeviceSettingsScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
|
fun BaseTestCase.scanCardInDeviceSettings() {
|
||||||
|
step("Click on 'Scan card or ring' button") {
|
||||||
|
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) {
|
fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) {
|
||||||
step("Click on 'Scan card or ring' button") {
|
step("Click on 'Scan card or ring' button") {
|
||||||
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
|
onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
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.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the recipient step: fill the address and advance to the 'Send confirm' screen.
|
||||||
|
* Uses `composeTestRule.waitUntil` because `flakySafely` is unavailable in extensions on [BaseTestCase].
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.enterRecipientAndOpenSendConfirm(recipientAddress: String) {
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button until 'Send confirm' screen opens") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { openSendConfirmScreenViaNextButton() }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enter the send amount, then fill the recipient and open the 'Send confirm' screen. */
|
||||||
|
fun BaseTestCase.enterAmountAndOpenSendConfirm(amount: String, recipientAddress: String) {
|
||||||
|
step("Type '$amount' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
enterRecipientAndOpenSendConfirm(recipientAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On the 'Send confirm' screen, open the network-fee selector and switch the fee token from the
|
||||||
|
* native coin to the given (stablecoin) token — the core gasless action repeated across the suite.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.selectStablecoinAsFeeToken(coinName: String, tokenName: String) {
|
||||||
|
step("Click on 'Network fee' block") {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeSelectorBlock.assertIsDisplayed()
|
||||||
|
feeSelectorBlock.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on '$coinName' fee token to open 'Choose token'") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSendFeeSelectorBottomSheet { feeTokenItem(coinName).performClick() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an existing hot wallet (gasless signing needs a hot wallet, not the mock card), set the
|
||||||
|
* portfolio and quotes mocks, and reach the send amount input for the given token.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.openGaslessSendScreenWithHotWallet(
|
||||||
|
seedPhrase: String,
|
||||||
|
tokenName: String,
|
||||||
|
userTokensState: String,
|
||||||
|
quotesState: String,
|
||||||
|
) {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Open 'Main' screen with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an existing hot wallet, select the token to send and choose the swap target token/network —
|
||||||
|
* the shared entry into the gasless send-via-swap flow. Scenario states stay in the test body.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.openSendViaSwapScreenWithHotWallet(
|
||||||
|
seedPhrase: String,
|
||||||
|
tokenName: String,
|
||||||
|
swapTokenName: String,
|
||||||
|
networkName: String,
|
||||||
|
networkType: String? = null,
|
||||||
|
) {
|
||||||
|
step("Open 'Main' screen with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Select '$swapTokenName' as the token to receive via swap") {
|
||||||
|
selectTokenToSendViaSwap(
|
||||||
|
swapTokenName = swapTokenName,
|
||||||
|
networkName = networkName,
|
||||||
|
networkType = networkType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send-via-swap amount entry: type the amount, advance past the quote-gated 'Next' button (waiting
|
||||||
|
* until it becomes enabled once the swap quote loads), then fill the recipient and open the
|
||||||
|
* 'Send confirm' screen. Uses `composeTestRule.waitUntil` because `flakySafely` is unavailable in
|
||||||
|
* extensions on [BaseTestCase].
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.enterSwapAmountAndOpenSendConfirm(amount: String, recipientAddress: String) {
|
||||||
|
step("Type amount '$amount' in input field") {
|
||||||
|
onSendScreen { amountInputTextField.performTextReplacement(amount) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching {
|
||||||
|
onSendScreen {
|
||||||
|
nextButton.assertIsEnabled()
|
||||||
|
nextButton.performClick()
|
||||||
|
}
|
||||||
|
}.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enterRecipientAndOpenSendConfirm(recipientAddress)
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
package com.tangem.scenarios
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.extensions.SwipeDirection
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.swipeVertical
|
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.screens.onMarketsExchangesScreen
|
import com.tangem.screens.onMarketsExchangesScreen
|
||||||
import com.tangem.screens.onMarketsScreen
|
import com.tangem.screens.onMarketsScreen
|
||||||
import com.tangem.screens.onMarketsTokenDetailsScreen
|
import com.tangem.screens.onMarketsTokenDetailsScreen
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName: String) {
|
fun BaseTestCase.openTokenDetailsFromMarketsScreen(blockchainName: String, tokenName: String) {
|
||||||
step("Open 'Markets' screen") {
|
step("Open 'Markets' screen") {
|
||||||
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
onMainScreen { marketsSheetDragHandle.clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Click on 'Search' placeholder") {
|
|
||||||
onMarketsScreen { searchThroughMarketPlaceholder.performClick() }
|
|
||||||
}
|
|
||||||
step("Click on $blockchainName blockchain") {
|
step("Click on $blockchainName blockchain") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on $tokenName token") {
|
step("Click on 'In your portfolio' block") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsTokenDetailsScreen { inYourPortfolioBlock.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on $tokenName token in 'Your portfolio' bottom sheet") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
|
|
@ -54,11 +54,12 @@ fun BaseTestCase.openMarketsScreen() {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' screen") {
|
step("Open 'Markets' screen") {
|
||||||
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
onMainScreen { marketsSheetDragHandle.clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAllButton: Boolean = false) {
|
||||||
openMarketsScreen()
|
openMarketsScreen()
|
||||||
if (shouldClickSeeAllButton)
|
if (shouldClickSeeAllButton)
|
||||||
|
|
@ -69,9 +70,8 @@ fun BaseTestCase.openMarketsExchangesScreen(tokenName: String, shouldClickSeeAll
|
||||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Scroll down") {
|
step("Scroll to 'Listed on exchanges' block") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
onMarketsScreen { scrollToListedOnBlock() }
|
||||||
swipeVertical(SwipeDirection.UP)
|
|
||||||
}
|
}
|
||||||
step("Click on 'Listed on exchanges' block") {
|
step("Click on 'Listed on exchanges' block") {
|
||||||
onMarketsScreen { listedOnBlockContainer.performClick() }
|
onMarketsScreen { listedOnBlockContainer.performClick() }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.isDisplayedSafely
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||||
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
|
/** 'Add Wallet' scans a card immediately (no type chooser), so [mockContent] must be set before the click. */
|
||||||
|
fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
|
||||||
|
step("Click 'More' button on TopBar") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
MockProvider.setMocks(mockContent)
|
||||||
|
step("Click on 'Add Wallet' button (scans a new hardware wallet)") {
|
||||||
|
onDetailsScreen { addWalletButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
// Gate on the top-bar More button, not the container — the bottom Markets sheet can leave the container un-"displayed".
|
||||||
|
step("Assert 'Main' screen is displayed with the new wallet") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||||
|
runCatching { onMainScreenTopBar { moreButton.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The added card is the newest pager page; its "Synchronize addresses" prompt is off-screen until swiped to.
|
||||||
|
step("Synchronize the new card wallet's addresses") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
var shown = false
|
||||||
|
onMainScreen { shown = synchronizeAddressesButton.isDisplayedSafely() }
|
||||||
|
if (!shown) onMainScreen { swipeToAdjacentWallet(toPrevious = false) }
|
||||||
|
shown
|
||||||
|
}
|
||||||
|
// Let the pager fling settle — a click mid-animation is eaten by the button's clickableSingle debounce.
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||||
|
// The prompt clears once the card's addresses are derived (re-scan + reload over many, some failing, RPCs).
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||||
|
var generated = false
|
||||||
|
onMainScreen { generated = !synchronizeAddressesButton.isDisplayedSafely() }
|
||||||
|
generated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
|
||||||
|
step("Click on token '$tokenName' on the visible wallet") {
|
||||||
|
onMainScreen { clickDisplayedToken(tokenName) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.switchToPreviousWallet() {
|
||||||
|
step("Swipe wallet card to the previous wallet") {
|
||||||
|
onMainScreen { swipeToAdjacentWallet(toPrevious = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Picks a [token] the recipient [walletName] already holds, via the wallet tab. */
|
||||||
|
fun BaseTestCase.selectReceiveTokenOnWallet(token: String, walletName: String) {
|
||||||
|
step("Click on 'Choose token' button") {
|
||||||
|
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Select wallet tab '$walletName'") {
|
||||||
|
onBuyTokenScreen { walletTab(walletName).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on token with name '$token'") {
|
||||||
|
onBuyTokenScreen { tokenWithTitle(token).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds [token] to [recipientWalletName] which lacks it, via market search. */
|
||||||
|
fun BaseTestCase.addMissingReceiveTokenToWallet(token: String, recipientWalletName: String) {
|
||||||
|
step("Click on 'Choose token' button") {
|
||||||
|
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Type '$token' in search field") {
|
||||||
|
onSwapSelectTokenScreen {
|
||||||
|
searchBarBlock.performClick()
|
||||||
|
searchBarBlock.performTextInput(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on market token '$token'") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSwapSelectTokenScreen { marketsTokenWithName(token).performClick() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The 'Add token' sheet pre-selects the recipient (the only wallet missing the token, since the source already holds it).
|
||||||
|
step("Assert recipient wallet '$recipientWalletName' is selected") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onAddToPortfolioScreen { walletName(recipientWalletName).assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Add' button") {
|
||||||
|
onAddToPortfolioScreen { addButton.performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
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.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.assertIsDimmed
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.extractText
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
|
||||||
import com.tangem.screens.*
|
import com.tangem.screens.*
|
||||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
fun BaseTestCase.openSendScreen(
|
fun BaseTestCase.openSendScreen(
|
||||||
|
|
@ -34,8 +37,11 @@ fun BaseTestCase.openSendScreen(
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,11 +97,11 @@ fun BaseTestCase.openSendAddressScreen(
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not dimmed") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
@ -109,6 +115,13 @@ fun BaseTestCase.openSendAddressScreen(
|
||||||
step("Assert 'Send Address' container is displayed") {
|
step("Assert 'Send Address' container is displayed") {
|
||||||
onSendAddressScreen { container.assertIsDisplayed() }
|
onSendAddressScreen { container.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
|
step("Wait for recipient list to load") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching {
|
||||||
|
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
|
||||||
|
}.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) {
|
fun BaseTestCase.checkScanQrScreen(emptyClipboard: Boolean = true) {
|
||||||
|
|
@ -193,6 +206,20 @@ fun BaseTestCase.openSendConfirmScreenViaNextButton() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.openSendConfirmScreenViaContinueButton() {
|
||||||
|
step("Click on 'Continue' button") {
|
||||||
|
onSendAddressScreen {
|
||||||
|
addressesShimmer.assertIsNotDisplayed()
|
||||||
|
continueButton.assertIsDisplayed()
|
||||||
|
continueButton.assertIsEnabled()
|
||||||
|
continueButton.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button on 'Send confirm' screen is displayed") {
|
||||||
|
onSendConfirmScreen { sendButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.openSendSuccessScreenViaLongClickOnSendButton() {
|
fun BaseTestCase.openSendSuccessScreenViaLongClickOnSendButton() {
|
||||||
step("Long click on 'Send' button") {
|
step("Long click on 'Send' button") {
|
||||||
onSendConfirmScreen {
|
onSendConfirmScreen {
|
||||||
|
|
@ -239,13 +266,100 @@ fun BaseTestCase.checkSendViaSwapSuccessScreen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** From the token details screen, open the transfer bottom sheet and reach the send amount input. */
|
||||||
|
fun BaseTestCase.openSendFromTokenDetails() {
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open an existing hot wallet and reach the send amount input for [tokenName]. */
|
||||||
|
fun BaseTestCase.openSendScreenWithHotWallet(seedPhrase: String, tokenName: String) {
|
||||||
|
step("Open 'Main' screen with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
openSendFromTokenDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.getNetworkFeeAmount(): String {
|
||||||
|
var fee = ""
|
||||||
|
step("Read current network fee amount") {
|
||||||
|
onSendConfirmScreen { fee = feeAmount.extractText() }
|
||||||
|
}
|
||||||
|
return fee
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.switchFeeToFastAndApply() {
|
||||||
|
val fastOption = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
step("Click on fee selector icon") {
|
||||||
|
onSendConfirmScreen { feeSelectorIcon.performClick() }
|
||||||
|
}
|
||||||
|
// Selecting a non-custom speed auto-applies and closes the fee selector — no 'Done' step.
|
||||||
|
step("Click on '$fastOption' fee option") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastOption).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.assertNetworkFeeChanged(previousFee: String) {
|
||||||
|
step("Assert network fee changed from '$previousFee'") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching {
|
||||||
|
var current = previousFee
|
||||||
|
onSendConfirmScreen { current = feeAmount.extractText() }
|
||||||
|
current != previousFee
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the network fee amount on the 'Send confirm' screen (empty while it shows a loading shimmer). */
|
||||||
|
fun BaseTestCase.readNetworkFeeAmount(): String {
|
||||||
|
var fee = ""
|
||||||
|
onSendConfirmScreen { fee = feeAmount.extractText() }
|
||||||
|
return fee
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until the network fee value stops changing across two checks — the send button stays disabled
|
||||||
|
* (and the hold-to-confirm gesture is swallowed) until the fee re-fetch settles. The hold button has
|
||||||
|
* no enabled/disabled semantics, so waiting on the fee value is the only reliable readiness signal.
|
||||||
|
*/
|
||||||
|
fun TestContext<Unit>.waitUntilNetworkFeeIsStable(readFee: () -> String) {
|
||||||
|
step("Wait for the network fee to finish loading") {
|
||||||
|
var previousFee: String? = null
|
||||||
|
flakySafely(timeoutMs = WAIT_UNTIL_TIMEOUT_LONG, intervalMs = FEE_STABILITY_INTERVAL_MS) {
|
||||||
|
val currentFee = readFee()
|
||||||
|
val isStable = currentFee.isNotEmpty() && currentFee == previousFee
|
||||||
|
previousFee = currentFee
|
||||||
|
if (!isStable) throw AssertionError("Network fee is still settling (current='$currentFee')")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val FEE_STABILITY_INTERVAL_MS = 750L
|
||||||
|
|
||||||
|
fun BaseTestCase.assertNetworkFeeContains(currencySymbol: String) {
|
||||||
|
step("Assert network fee contains '$currencySymbol'") {
|
||||||
|
onSendConfirmScreen { feeAmount.assertTextContains(currencySymbol, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.selectTokenToSendViaSwap(
|
fun BaseTestCase.selectTokenToSendViaSwap(
|
||||||
swapTokenName: String,
|
swapTokenName: String,
|
||||||
networkName: String,
|
networkName: String,
|
||||||
networkType: String? = null,
|
networkType: String? = null,
|
||||||
) {
|
) {
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap to another token' button") {
|
step("Click on 'Swap to another token' button") {
|
||||||
onSendScreen { swapToAnotherTokenButton.performClick() }
|
onSendScreen { swapToAnotherTokenButton.performClick() }
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,18 @@ import androidx.compose.ui.test.performTouchInput
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
import com.tangem.common.constants.TestConstants.HOLD_DURATION_MS
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||||
import com.tangem.common.extensions.assertVisibility
|
import com.tangem.common.extensions.assertVisibility
|
||||||
|
import com.tangem.common.extensions.clickAndWaitFor
|
||||||
|
import com.tangem.common.extensions.clickWhenEnabled
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.extractText
|
||||||
import com.tangem.common.extensions.isDisplayedSafely
|
import com.tangem.common.extensions.isDisplayedSafely
|
||||||
import com.tangem.core.ui.R as CoreUiR
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
|
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
|
||||||
import com.tangem.screens.*
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
import com.tangem.common.ui.R as CommonUiR
|
import com.tangem.common.ui.R as CommonUiR
|
||||||
|
|
@ -43,8 +48,8 @@ fun BaseTestCase.openSwapScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") {
|
SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.clickWhenEnabled() }
|
||||||
}
|
}
|
||||||
|
|
||||||
SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") {
|
SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
||||||
|
|
@ -167,7 +172,7 @@ fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
|
||||||
when (feeType) {
|
when (feeType) {
|
||||||
FeeType.Market -> {
|
FeeType.Market -> {
|
||||||
step("Click on 'Market' item") {
|
step("Click on 'Market' item") {
|
||||||
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() }
|
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
|
step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
|
||||||
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
||||||
|
|
@ -175,7 +180,7 @@ fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
|
||||||
}
|
}
|
||||||
FeeType.Fast -> {
|
FeeType.Fast -> {
|
||||||
step("Click on 'Fast' item") {
|
step("Click on 'Fast' item") {
|
||||||
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() }
|
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
|
step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
|
||||||
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
||||||
|
|
@ -184,49 +189,23 @@ fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.selectFeeTypeWithGasless(feeType: FeeType, selectedFeeAmount: String) {
|
fun BaseTestCase.selectFeeTypeAndReadFee(feeType: FeeType): String {
|
||||||
step("Click on 'Select fee' icon") {
|
step("Click on 'Select fee' icon") {
|
||||||
onSwapTokenScreen { selectFeeIcon.performClick() }
|
onSwapTokenScreen { selectFeeIcon.performClick() }
|
||||||
}
|
}
|
||||||
|
step("Click on '$feeType' item") {
|
||||||
when (feeType) {
|
|
||||||
FeeType.Market -> selectMarketFee(selectedFeeAmount)
|
|
||||||
FeeType.Fast -> selectFastFee(selectedFeeAmount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseTestCase.selectMarketFee(selectedFeeAmount: String) {
|
|
||||||
step("Deselect current fee and select 'Market'") {
|
|
||||||
onSwapSelectNetworkFeeBottomSheet {
|
onSwapSelectNetworkFeeBottomSheet {
|
||||||
if (fastSelectorItem.isDisplayedSafely()) {
|
when (feeType) {
|
||||||
fastSelectorItem.performClick()
|
FeeType.Market -> marketSelectorItem.clickWithAssertion()
|
||||||
|
FeeType.Fast -> fastSelectorItem.clickWithAssertion()
|
||||||
}
|
}
|
||||||
marketSelectorItem.performClick()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Click on 'Apply' button") {
|
var fee = ""
|
||||||
onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() }
|
step("Read displayed '$feeType' fee amount") {
|
||||||
}
|
onSwapTokenScreen { fee = feeAmount.extractText() }
|
||||||
step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
|
|
||||||
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseTestCase.selectFastFee(selectedFeeAmount: String) {
|
|
||||||
step("Deselect current fee and select 'Fast'") {
|
|
||||||
onSwapSelectNetworkFeeBottomSheet {
|
|
||||||
if (marketSelectorItem.isDisplayedSafely()) {
|
|
||||||
marketSelectorItem.performClick()
|
|
||||||
}
|
|
||||||
fastSelectorItem.performClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Click on 'Apply' button") {
|
|
||||||
onSwapSelectNetworkFeeBottomSheet { applyButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
|
|
||||||
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
|
|
||||||
}
|
}
|
||||||
|
return fee
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) {
|
fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) {
|
||||||
|
|
@ -284,6 +263,112 @@ fun BaseTestCase.checkSwapWarning(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scans a card wallet and opens Swap for [tokenName] in [fromAccountName] without choosing the receive token yet. */
|
||||||
|
fun BaseTestCase.openSwapForTokenInAccount(
|
||||||
|
tokenName: String,
|
||||||
|
fromAccountName: String = "Account 1",
|
||||||
|
mockContent: MockContent? = null,
|
||||||
|
) {
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen(mockContent = mockContent)
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses(assertBalance = false)
|
||||||
|
}
|
||||||
|
step("Wait for addresses to be generated") {
|
||||||
|
waitForAddressesGenerated()
|
||||||
|
}
|
||||||
|
navigateToSwapForToken(tokenName, fromAccountName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens Swap for [tokenName] in [fromAccountName] and picks it again in [toAccountName] to enter Transfer mode; needs a two-accounts-same-token mock. */
|
||||||
|
fun BaseTestCase.openSwapInTransferMode(
|
||||||
|
tokenName: String,
|
||||||
|
fromAccountName: String = "Account 1",
|
||||||
|
toAccountName: String = "Account 2",
|
||||||
|
mockContent: MockContent? = null,
|
||||||
|
) {
|
||||||
|
openSwapForTokenInAccount(tokenName, fromAccountName, mockContent)
|
||||||
|
step("Choose identical receive token '$tokenName' from '$toAccountName'") {
|
||||||
|
chooseIdenticalReceiveToken(tokenName = tokenName, receiveAccountName = toAccountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like [openSwapInTransferMode] but imports a hot wallet first — required for broadcasting flows (the mock card can't sign). */
|
||||||
|
fun BaseTestCase.openSwapInTransferModeWithHotWallet(
|
||||||
|
tokenName: String,
|
||||||
|
seedPhrase: String,
|
||||||
|
fromAccountName: String = "Account 1",
|
||||||
|
toAccountName: String = "Account 2",
|
||||||
|
) {
|
||||||
|
step("Open 'Main' screen with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||||
|
}
|
||||||
|
step("Generate missing addresses") {
|
||||||
|
generateMissingHotWalletAddresses()
|
||||||
|
}
|
||||||
|
step("Wait for addresses to be generated") {
|
||||||
|
waitForAddressesGenerated()
|
||||||
|
}
|
||||||
|
navigateToSwapForToken(tokenName, fromAccountName)
|
||||||
|
step("Choose identical receive token '$tokenName' from '$toAccountName'") {
|
||||||
|
chooseIdenticalReceiveToken(tokenName = tokenName, receiveAccountName = toAccountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BaseTestCase.navigateToSwapForToken(tokenName: String, fromAccountName: String) {
|
||||||
|
step("Scroll '$fromAccountName' into view (semantics, not touch — avoids the Markets sheet)") {
|
||||||
|
onMainScreen { scrollToAccount(fromAccountName) }
|
||||||
|
}
|
||||||
|
step("Expand account '$fromAccountName' and reveal token '$tokenName'") {
|
||||||
|
onMainScreen {
|
||||||
|
findAccountSectionByName(fromAccountName).clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onMainScreen { findTokenInAnyAccountByName(tokenName).assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { findTokenInAnyAccountByName(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hot wallets derive locally, so the second account's missing addresses are generated without a card scan when prompted.
|
||||||
|
fun BaseTestCase.generateMissingHotWalletAddresses() {
|
||||||
|
var notificationShown = false
|
||||||
|
onMainScreen { notificationShown = synchronizeAddressesButton.isDisplayedSafely() }
|
||||||
|
if (notificationShown) {
|
||||||
|
onMainScreen { synchronizeAddressesButton.performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The receive selector shows "No address" until the second account's derivation lands; the prompt disappears when it does.
|
||||||
|
fun BaseTestCase.waitForAddressesGenerated() {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
var generated = false
|
||||||
|
onMainScreen { generated = !synchronizeAddressesButton.isDisplayedSafely() }
|
||||||
|
generated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Picks the identical [tokenName] in [receiveAccountName]; the receive list collapses the other account, so its header is expanded first. */
|
||||||
|
fun BaseTestCase.chooseIdenticalReceiveToken(tokenName: String, receiveAccountName: String) {
|
||||||
|
step("Click on 'Choose token' button") {
|
||||||
|
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Expand account '$receiveAccountName' in receive selector") {
|
||||||
|
onSwapSelectTokenScreen { tokenWithName(receiveAccountName).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on token with name '$tokenName'") {
|
||||||
|
onSwapSelectTokenScreen { tokenWithName(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun BaseTestCase.chooseReceiveToken(tokenName: String) {
|
fun BaseTestCase.chooseReceiveToken(tokenName: String) {
|
||||||
step("Click on 'Choose token' button") {
|
step("Click on 'Choose token' button") {
|
||||||
onSwapTokenScreen { chooseTokenButton.performClick() }
|
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||||
|
|
@ -293,6 +378,95 @@ fun BaseTestCase.chooseReceiveToken(tokenName: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reopens the receive selector via the receive-card icon and picks [tokenName] directly — the reopened selector keeps the account expanded. */
|
||||||
|
fun BaseTestCase.changeReceiveToken(tokenName: String) {
|
||||||
|
step("Open receive token selector") {
|
||||||
|
onSwapTokenScreen { receiveSelectTokenIcon.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on token with name '$tokenName'") {
|
||||||
|
onSwapSelectTokenScreen { tokenWithName(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From a clean start: open the main screen (cold by default, or an existing hot wallet when
|
||||||
|
* [seedPhrase] is given), open Swap for [fromTokenName], choose [receiveTokenName] to receive and
|
||||||
|
* enter [amount]. Scenario states stay in the test body.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.openSwapAmountScreen(
|
||||||
|
fromTokenName: String,
|
||||||
|
receiveTokenName: String,
|
||||||
|
amount: String,
|
||||||
|
seedPhrase: String? = null,
|
||||||
|
) {
|
||||||
|
if (seedPhrase == null) {
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step("Open 'Main' screen with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(seedPhrase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$fromTokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(fromTokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
||||||
|
}
|
||||||
|
step("Choose receive token '$receiveTokenName'") {
|
||||||
|
chooseReceiveToken(receiveTokenName)
|
||||||
|
}
|
||||||
|
step("Input swap amount '$amount'") {
|
||||||
|
waitForIdle()
|
||||||
|
onSwapTokenScreen {
|
||||||
|
textInput.clickWithAssertion()
|
||||||
|
textInput.performTextReplacement(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Wait for the receive amount to load") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSwapTokenScreen { receiveAmount.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the swap 'Network fee' bottom sheet, retrying the click until the fee selector shows.
|
||||||
|
* Single action without its own step — wrap the call in a `step(...)`.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.openSwapNetworkFeeSelector() {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||||
|
runCatching { onSwapTokenScreen { networkFeeBlock.performClick() } }
|
||||||
|
runCatching { onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the fee selector and switches the fee-paying token from [currentFeeToken] to [newFeeToken],
|
||||||
|
* then applies. Works both ways — coin -> stablecoin and back.
|
||||||
|
*/
|
||||||
|
fun BaseTestCase.switchFeeTokenAndApply(currentFeeToken: String, newFeeToken: String) {
|
||||||
|
step("Open the 'Network fee' bottom sheet") {
|
||||||
|
openSwapNetworkFeeSelector()
|
||||||
|
}
|
||||||
|
step("Click on '$currentFeeToken' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(currentFeeToken).performClick() }
|
||||||
|
}
|
||||||
|
step("Select '$newFeeToken' as the fee-paying token") {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSendFeeSelectorBottomSheet { feeTokenItem(newFeeToken).performClick() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
waitForIdle()
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Holds the last BASE_BUTTON; enters [accessCode] if a hot wallet prompts for it. */
|
/** Holds the last BASE_BUTTON; enters [accessCode] if a hot wallet prompts for it. */
|
||||||
fun BaseTestCase.confirmSwapByHolding(accessCode: String? = null) {
|
fun BaseTestCase.confirmSwapByHolding(accessCode: String? = null) {
|
||||||
val buttonMatcher = hasTestTag(BaseButtonTestTags.BUTTON)
|
val buttonMatcher = hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
|
@ -316,6 +490,15 @@ fun BaseTestCase.confirmSwapByHolding(accessCode: String? = null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caller asserts the outcome — transfer mode has no in-progress marker to wait on.
|
||||||
|
fun BaseTestCase.holdToConfirmTransfer() {
|
||||||
|
composeTestRule.onNode(
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON) and
|
||||||
|
hasText(getResourceString(CoreUiR.string.swapping_transfer_action)),
|
||||||
|
).performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
|
||||||
sealed class SwapEntryPoint {
|
sealed class SwapEntryPoint {
|
||||||
object MainScreen : SwapEntryPoint()
|
object MainScreen : SwapEntryPoint()
|
||||||
object TokenDetails : SwapEntryPoint()
|
object TokenDetails : SwapEntryPoint()
|
||||||
|
|
@ -328,4 +511,35 @@ enum class FeeType {
|
||||||
Fast
|
Fast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.inputAmount(amount: String) {
|
||||||
|
// No waitForIdle(): the transfer screen recalculates the fee continuously and never reaches idle.
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSwapTokenScreen { textInput.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
onSwapTokenScreen {
|
||||||
|
textInput.clickWithAssertion()
|
||||||
|
textInput.performTextReplacement(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// composeTestRule.waitUntil rather than flakySafely — the latter is unavailable in extensions on BaseTestCase.
|
||||||
|
fun BaseTestCase.assertTransferReady() {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSwapTokenScreen { transferButton.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
onSwapTokenScreen { providersBlock.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.waitForFeeDisplayed() {
|
||||||
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { onSwapTokenScreen { feeAmount.assertIsDisplayed() } }.isSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.swapFeeDiffersFrom(previousFee: String): Boolean {
|
||||||
|
var current = ""
|
||||||
|
onSwapTokenScreen { current = feeAmount.extractText() }
|
||||||
|
return current.isNotEmpty() && current != previousFee
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
class AddFundsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AddFundsBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val buyButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_buy)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val swapButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiveButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_receive)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val buyTokenButton: KNode = child {
|
||||||
|
hasTestTag(TokenActionsTestTags.BUY_ACTION)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun userTokenWithTitle(tokenTitle: String): KNode = child {
|
||||||
|
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||||
|
hasAnyDescendant(withTestTag(TokenElementsTestTags.TOKEN_TITLE))
|
||||||
|
hasAnyDescendant(withText(tokenTitle))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
hasText(tokenTitle)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAddFundsBottomSheet(function: AddFundsBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import androidx.compose.ui.test.hasClickAction
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
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
|
||||||
|
|
||||||
|
class AddToPortfolioPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AddToPortfolioPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
fun walletName(walletName: String): KNode = child {
|
||||||
|
hasText(walletName)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val addButton: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.common_add))
|
||||||
|
hasClickAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAddToPortfolioScreen(function: AddToPortfolioPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -11,18 +11,32 @@ import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
|
||||||
class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class AddTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<AddTokenBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<AddTokenBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||||
hasText(getResourceString(R.string.common_add_token))
|
hasText(getResourceString(R.string.common_add_token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasTestTag(BaseBottomSheetTestTags.CLOSE_BUTTON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val addButton: KNode = child {
|
val addButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
hasText(getResourceString(R.string.common_add))
|
hasText(getResourceString(R.string.common_add))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val laterButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
|
hasText(getResourceString(R.string.common_later))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
internal fun BaseTestCase.onAddTokenBottomSheet(function: AddTokenBottomSheetPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.AppCurrencySelectorScreenTestTags
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class AppCurrencySelectorPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AppCurrencySelectorPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val searchActionButton: KNode = child {
|
||||||
|
hasTestTag(AppCurrencySelectorScreenTestTags.TOP_BAR_ACTION_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchField: KNode = child {
|
||||||
|
hasTestTag(AppCurrencySelectorScreenTestTags.SEARCH_FIELD)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currencyItem(code: String): KNode = child {
|
||||||
|
hasTestTag(AppCurrencySelectorScreenTestTags.CURRENCY_ITEM)
|
||||||
|
hasAnyDescendant(withText(code, substring = true))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAppCurrencySelectorScreen(function: AppCurrencySelectorPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.AppSettingsScreenTestTags
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
||||||
|
class AppSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AppSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val currencyButton: KNode = child {
|
||||||
|
hasTestTag(AppSettingsScreenTestTags.CURRENCY_BUTTON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAppSettingsScreen(function: AppSettingsPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -15,6 +15,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -48,6 +49,19 @@ class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun walletTab(walletName: String): KNode = child {
|
||||||
|
hasTestTag(BuyTokenScreenTestTags.WALLET_TAB)
|
||||||
|
hasAnyDescendant(withText(walletName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun tokenWithTitle(tokenTitle: String): LazyListItemNode = lazyList.childWith<LazyListItemNode> {
|
||||||
|
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||||
|
hasText(tokenTitle)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =
|
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,38 @@ package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
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.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
import com.tangem.core.res.R as CoreResR
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "You receive" token chooser opened from the main-screen "Add funds" button.
|
* Token chooser bottom sheet opened from the main-screen "Add funds" button.
|
||||||
|
*
|
||||||
|
* After the onramp redesign this is a [BaseBottomSheetTestTags.CONTAINER] bottom sheet
|
||||||
|
* (centered title + close icon), not a full screen with a top app bar.
|
||||||
*/
|
*/
|
||||||
class ChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class ChooseTokenBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<ChooseTokenPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<ChooseTokenBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
val topAppBarTitle: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.TITLE)
|
hasText(getResourceString(CoreResR.string.common_choose_token))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchBar: KNode = child {
|
val searchBar: KNode = child {
|
||||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tokenWithTitle(tokenTitle: String): KNode = child {
|
fun tokenWithTitle(tokenTitle: String): KNode = child {
|
||||||
|
|
@ -35,5 +44,5 @@ class ChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onChooseTokenScreen(function: ChooseTokenPageObject.() -> Unit) =
|
internal fun BaseTestCase.onChooseTokenBottomSheet(function: ChooseTokenBottomSheetPageObject.() -> Unit) =
|
||||||
onComposeScreen(composeTestRule, function)
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -9,6 +9,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -22,17 +23,13 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasText(getResourceString(R.string.wallet_connect_title))
|
hasText(getResourceString(R.string.wallet_connect_title))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val walletBlock: KNode = child {
|
val walletNameButton: KNode = child {
|
||||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
hasTestTag(DetailsScreenTestTags.USER_WALLET_ITEM)
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val walletNameButton: KNode = walletBlock.child {
|
val addWalletButton: KNode = child {
|
||||||
hasClickAction()
|
hasTestTag(DetailsScreenTestTags.ADD_WALLET_BUTTON)
|
||||||
hasPosition(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val scanCardButton: KNode = walletBlock.child {
|
|
||||||
hasText(getResourceString(R.string.scan_card_settings_button))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val buyTangemButton: KNode = child {
|
val buyTangemButton: KNode = child {
|
||||||
|
|
@ -58,6 +55,12 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasTestTag(DetailsScreenTestTags.VERSION_NAME)
|
hasTestTag(DetailsScreenTestTags.VERSION_NAME)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun walletNameValue(name: String): KNode = child {
|
||||||
|
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||||
|
hasAnyDescendant(withText(name))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =
|
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasClickAction as withClickAction
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
import androidx.compose.ui.test.isNotEnabled as withDisabled
|
||||||
|
|
||||||
class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<DeviceSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<DeviceSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -46,6 +49,19 @@ class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val securityModeRowTitle: KNode = child {
|
||||||
|
hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE)
|
||||||
|
hasText(getResourceString(R.string.card_settings_security_mode))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the row container (not the title Text): enabled exposes a click action, disabled exposes disabled semantics.
|
||||||
|
val securityModeRow: KNode = child {
|
||||||
|
addSemanticsMatcher(withClickAction() or withDisabled())
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.card_settings_security_mode)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child {
|
fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child {
|
||||||
hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE)
|
hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
|
||||||
|
|
@ -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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
|
|
||||||
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<DialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -25,6 +26,17 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasTestTag(BaseDialogTestTags.TEXT)
|
hasTestTag(BaseDialogTestTags.TEXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val inputField: KNode = child {
|
||||||
|
hasSetTextAction()
|
||||||
|
hasAnyAncestor(withTestTag(BaseDialogTestTags.TEXT_INPUT_FIELD))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val gotItButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.common_got_it))
|
||||||
|
}
|
||||||
|
|
||||||
val cancelButton: KNode = child {
|
val cancelButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
hasText(getResourceString(R.string.common_cancel))
|
hasText(getResourceString(R.string.common_cancel))
|
||||||
|
|
@ -45,6 +57,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasText(getResourceString(R.string.account_details_archive_action))
|
hasText(getResourceString(R.string.account_details_archive_action))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val discardButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.account_unsaved_dialog_action_second))
|
||||||
|
}
|
||||||
|
|
||||||
|
val keepEditButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.account_unsaved_dialog_action_first))
|
||||||
|
}
|
||||||
|
|
||||||
val continueButton: KNode = child {
|
val continueButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
hasText(getResourceString(R.string.common_continue))
|
hasText(getResourceString(R.string.common_continue))
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.semantics.SemanticsProperties
|
import androidx.compose.ui.semantics.SemanticsProperties
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.*
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
|
||||||
import androidx.compose.ui.test.hasAnyAncestor
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.extensions.getQuantityString
|
import com.tangem.common.extensions.getQuantityString
|
||||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||||
|
|
@ -22,7 +19,7 @@ import androidx.compose.ui.test.hasText as withText
|
||||||
import com.tangem.core.res.R as CoreResR
|
import com.tangem.core.res.R as CoreResR
|
||||||
import com.tangem.core.ui.R as CoreUiR
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MainScreenPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
private val lazyList = KLazyListNode(
|
private val lazyList = KLazyListNode(
|
||||||
|
|
@ -49,32 +46,38 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
val buyButton: KNode = child {
|
val buyButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_buy))
|
hasAnyDescendant(withText(getResourceString(R.string.common_buy)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val addFundsButton: KNode = child {
|
val addFundsButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_add_funds))
|
hasAnyDescendant(withText(getResourceString(R.string.common_add_funds)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val sendButton: KNode = child {
|
val sendButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_send))
|
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val receiveButton: KNode = child {
|
val receiveButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_receive))
|
hasAnyDescendant(withText(getResourceString(R.string.common_receive)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val sellButton: KNode = child {
|
val transferButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_sell))
|
hasAnyDescendant(withText(getResourceString(R.string.common_transfer)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val swapButton: KNode = child {
|
val swapButton: KNode = child {
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasAnyDescendant(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val walletNameText: KNode = child {
|
val walletNameText: KNode = child {
|
||||||
|
|
@ -87,13 +90,68 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val walletDevicesCount: KNode = child {
|
/**
|
||||||
hasTestTag(MainScreenTestTags.DEVICES_COUNT)
|
* Collapses the collapsing header via a touch-based swipe so that items near the bottom
|
||||||
|
* of the lazy list fall within screen bounds before programmatic childWith scroll.
|
||||||
|
* Required because TangemCollapsingTopBar places the body at y=collapsingHeight, which
|
||||||
|
* pushes lower list items off-screen when the header is expanded.
|
||||||
|
*/
|
||||||
|
private fun collapseHeader() {
|
||||||
|
screenContainer {
|
||||||
|
performTouchInput { swipeUp(startY = visibleSize.height * 0.6f, endY = visibleSize.height * 0.1f) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scrolls to [accountName] via ScrollToIndex semantics, not a touch swipe — a bottom-edge drag is stolen by the Markets sheet's nested scroll. */
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun scrollToAccount(accountName: String) {
|
||||||
|
semanticsProvider.onNode(withTestTag(MainScreenTestTags.SCREEN_CONTAINER))
|
||||||
|
.performScrollToNode(
|
||||||
|
withTestTag(MainScreenTestTags.ACCOUNT_LIST_ITEM) and hasAnyDescendant(withText(accountName)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet pager keeps the adjacent page composed (beyondViewportPageCount=1), so the token is mounted on two pages — click the displayed copy.
|
||||||
|
fun clickDisplayedToken(tokenName: String) {
|
||||||
|
val matcher = withTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(withText(tokenName))
|
||||||
|
val nodes = semanticsProvider.onAllNodes(matcher, useUnmergedTree = true)
|
||||||
|
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
||||||
|
if (runCatching { nodes[i].assertIsDisplayed(); nodes[i].performClick() }.isSuccess) return
|
||||||
|
}
|
||||||
|
error("Token '$tokenName' is not displayed on the current wallet page")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjacent pager pages stay mounted; swipe the wallet card that's actually on-screen.
|
||||||
|
fun swipeToAdjacentWallet(toPrevious: Boolean) {
|
||||||
|
val nodes = semanticsProvider.onAllNodes(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
|
||||||
|
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
||||||
|
val swiped = runCatching {
|
||||||
|
nodes[i].assertIsDisplayed()
|
||||||
|
nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
||||||
|
}.isSuccess
|
||||||
|
if (swiped) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val restoringProgressText: KNode = child {
|
||||||
|
hasTestTag(MainScreenTestTags.SYNC_PROGRESS_TEXT)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val walletImportedBanner: KNode = child {
|
||||||
|
hasTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val walletImportedBannerCheckHereButton: KNode = child {
|
||||||
|
hasAnyAncestor(withTestTag(WalletNotificationTestTags.ASSETS_DISCOVERY_BANNER))
|
||||||
|
hasText(getResourceString(CoreResR.string.main_manage_tokens))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun marketPriceBlock(): LazyListItemNode {
|
fun marketPriceBlock(): LazyListItemNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MarketPriceBlockTestTags.BLOCK)
|
hasTestTag(MarketPriceBlockTestTags.BLOCK)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
@ -225,6 +283,22 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty-tokens placeholder shown under an expanded account that has no tokens.
|
||||||
|
*/
|
||||||
|
val emptyAccountTokensPlaceholder: KNode = child {
|
||||||
|
hasTestTag(MainScreenTestTags.EMPTY_TOKENS_PLACEHOLDER)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'Add tokens' button inside the empty-account placeholder. Click opens manage tokens for that account.
|
||||||
|
*/
|
||||||
|
val emptyAccountAddTokensButton: KNode = child {
|
||||||
|
hasTestTag(MainScreenTestTags.EMPTY_TOKENS_ADD_BUTTON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main account header on the main screen. Click to expand/collapse its tokens list.
|
* Main account header on the main screen. Click to expand/collapse its tokens list.
|
||||||
*/
|
*/
|
||||||
|
|
@ -236,6 +310,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun accountWithName(name: String): LazyListItemNode {
|
fun accountWithName(name: String): LazyListItemNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasAnyDescendant(withText(name))
|
hasAnyDescendant(withText(name))
|
||||||
|
|
@ -243,11 +318,21 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun tokenRowWithTitle(tokenTitle: String): LazyListItemNode {
|
||||||
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
|
hasText(tokenTitle)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find token list item with title and address
|
* Find token list item with title and address
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
|
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -260,6 +345,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -272,6 +358,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun addAndManageButton(): KNode {
|
fun addAndManageButton(): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
||||||
}.child<KNode> {
|
}.child<KNode> {
|
||||||
|
|
@ -287,11 +374,17 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchThroughMarketPlaceholder: KNode = child {
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_search_header_title))
|
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val marketsSheetDragHandle: KNode = child {
|
||||||
|
hasTestTag(MainScreenTestTags.MARKETS_SHEET_DRAG_HANDLE)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasAnyChild(withText(tokenNetwork))
|
hasAnyChild(withText(tokenNetwork))
|
||||||
|
|
@ -301,6 +394,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
@OptIn(ExperimentalTestApi::class)
|
||||||
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
|
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
|
||||||
|
collapseHeader()
|
||||||
return lazyList.childWith<LazyListItemNode> {
|
return lazyList.childWith<LazyListItemNode> {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
hasText(tokenTitle)
|
hasText(tokenTitle)
|
||||||
|
|
@ -312,6 +406,46 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account row on the main screen. Tappable — click to expand/collapse its tokens.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun findAccountSectionByName(accountName: String): KNode {
|
||||||
|
return lazyList.child {
|
||||||
|
hasTestTag(MainScreenTestTags.ACCOUNT_LIST_ITEM)
|
||||||
|
hasAnyDescendant(withText(accountName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls the account row into view and collapses the top bar so the account's tokens (or the
|
||||||
|
* empty placeholder) land within screen bounds after expansion. Click via [findAccountSectionByName].
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun scrollToAccountSection(accountName: String) {
|
||||||
|
collapseHeader()
|
||||||
|
lazyList.childWith<LazyListItemNode> {
|
||||||
|
hasTestTag(MainScreenTestTags.ACCOUNT_LIST_ITEM)
|
||||||
|
hasAnyDescendant(withText(accountName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a token row on the main screen by token name. Tokens belonging to collapsed accounts
|
||||||
|
* are hidden from the semantics tree, so expanding a single account before calling this
|
||||||
|
* effectively scopes the lookup to that account's tokens.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
|
fun findTokenInAnyAccountByName(tokenName: String): KNode {
|
||||||
|
return lazyList.child {
|
||||||
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
|
hasAnyDescendant(withText(tokenName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun KNode.assertIsUnreachable() {
|
fun KNode.assertIsUnreachable() {
|
||||||
this {
|
this {
|
||||||
hasAnyAncestor(withText(getResourceString(R.string.common_unreachable)))
|
hasAnyAncestor(withText(getResourceString(R.string.common_unreachable)))
|
||||||
|
|
@ -324,16 +458,25 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
* Tests will fail if assertIsNotDisplayed() or assertDoesNotExist() are used instead.
|
* Tests will fail if assertIsNotDisplayed() or assertDoesNotExist() are used instead.
|
||||||
*/
|
*/
|
||||||
fun assertTokenDoesNotExist(tokenTitle: String) {
|
fun assertTokenDoesNotExist(tokenTitle: String) {
|
||||||
try {
|
lazyList.child<KNode> {
|
||||||
tokenWithTitleAndAddress(tokenTitle).assertExists()
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
throw AssertionError("Token with title '$tokenTitle' should not exist but was found")
|
hasAnyDescendant(withText(tokenTitle))
|
||||||
} catch (e: AssertionError) {
|
useUnmergedTree = true
|
||||||
if (e.message?.contains("No node found") == true) {
|
}.assertDoesNotExist()
|
||||||
return
|
}
|
||||||
} else {
|
|
||||||
throw e
|
fun assertTokensCount(expectedCount: Int) {
|
||||||
}
|
semanticsProvider
|
||||||
}
|
.onAllNodes(withTestTag(MainScreenTestTags.TOKEN_LIST_ITEM), useUnmergedTree = true)
|
||||||
|
.assertCountEquals(expectedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertTokenExists(tokenTitle: String) {
|
||||||
|
lazyList.child<KNode> {
|
||||||
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
|
hasAnyDescendant(withText(tokenTitle))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||||
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
||||||
import com.tangem.core.ui.test.SwitchTestTags
|
import com.tangem.core.ui.test.SwitchTestTags
|
||||||
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
@ -20,6 +21,16 @@ import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
|
||||||
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val topAppBarBackButton: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
val topAppBarTitle: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.TITLE)
|
||||||
|
hasText(getResourceString(com.tangem.core.ui.R.string.add_tokens_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val searchField: KNode = child {
|
val searchField: KNode = child {
|
||||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.semantics.SemanticsNode
|
import androidx.compose.ui.semantics.SemanticsNode
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import androidx.compose.ui.test.hasParent
|
|
||||||
import androidx.compose.ui.test.hasTestTag
|
import androidx.compose.ui.test.hasTestTag
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
|
||||||
import com.tangem.features.onramp.impl.R
|
import com.tangem.features.onramp.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
|
|
@ -23,16 +21,15 @@ class MarketsExchangesPageObject(private val provider: SemanticsNodeInteractions
|
||||||
|
|
||||||
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
fun allExchangeTypeNodes(): List<SemanticsNode> =
|
||||||
provider
|
provider
|
||||||
.onAllNodes(hasParent(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))))
|
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_PRICE))
|
||||||
.fetchSemanticsNodes()
|
.fetchSemanticsNodes()
|
||||||
|
|
||||||
fun allTrustScoreNodes(): List<SemanticsNode> =
|
fun allTrustScoreNodes(): List<SemanticsNode> =
|
||||||
provider
|
provider
|
||||||
.onAllNodes(hasParent(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT)))
|
.onAllNodes(hasTestTag(TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT))
|
||||||
.fetchSemanticsNodes()
|
.fetchSemanticsNodes()
|
||||||
|
|
||||||
val exchangesTitle: KNode = child {
|
val exchangesTitle: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.TITLE)
|
|
||||||
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
hasText(getResourceString(R.string.markets_token_details_exchanges_title))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import androidx.compose.ui.test.hasTestTag
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
|
|
@ -15,9 +17,9 @@ import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
val addToPortfolioButton: KNode = child {
|
val addButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
hasText(getResourceString(R.string.common_add_to_portfolio))
|
hasText(getResourceString(R.string.common_add))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +33,12 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchThroughMarketPlaceholder: KNode = child {
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_search_header_title))
|
hasText(getResourceString(R.string.markets_search_title_placeholder))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val tokenDetailsContent: KNode = child {
|
||||||
|
hasTestTag(MarketsTestTags.TOKEN_DETAILS_CONTENT)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +48,8 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val listedOnBlockContainer: KNode = child {
|
val listedOnBlockContainer: KNode = child {
|
||||||
hasText(getResourceString(R.string.markets_token_details_listed_on), substring = true)
|
hasTestTag(MarketsTestTags.LISTED_ON_BLOCK)
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val listedOnEmptyText: KNode = child {
|
val listedOnEmptyText: KNode = child {
|
||||||
|
|
@ -60,6 +68,13 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasText(title)
|
hasText(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalTestApi
|
||||||
|
fun scrollToListedOnBlock() {
|
||||||
|
tokenDetailsContent {
|
||||||
|
performScrollToNode(hasTestTag(MarketsTestTags.LISTED_ON_BLOCK))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> Unit) =
|
internal fun BaseTestCase.onMarketsScreen(function: MarketsPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package com.tangem.screens
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
||||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
|
||||||
import com.tangem.features.onramp.impl.R
|
import com.tangem.features.onramp.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MarketsTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MarketsTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
@ -20,11 +19,14 @@ class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractions
|
||||||
hasText(getResourceString(R.string.common_swap), substring = true)
|
hasText(getResourceString(R.string.common_swap), substring = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val inYourPortfolioBlock: KNode = child {
|
||||||
|
hasText(getResourceString(CoreUiR.string.markets_portfolio_block_subtitle), substring = true)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun tokenWithTitle(title: String): KNode = child {
|
fun tokenWithTitle(title: String): KNode = child {
|
||||||
hasAnyAncestor(withTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM))
|
|
||||||
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
|
||||||
hasAnySibling(withTestTag(TokenElementsTestTags.TOKEN_ICON))
|
|
||||||
hasAnyChild(withText(title))
|
hasAnyChild(withText(title))
|
||||||
|
hasClickAction()
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,18 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val topBarGroupButton: KNode = child {
|
val organizeMenuButton: KNode = child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.GROUP_BUTTON)
|
hasTestTag(OrganizeTokensScreenTestTags.MENU_BUTTON)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupButton: KNode = topBarGroupButton.child {
|
val groupButton: KNode = child {
|
||||||
hasText(getResourceString(R.string.organize_tokens_group))
|
hasText(getResourceString(R.string.organize_tokens_group))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val ungroupButton: KNode = topBarGroupButton.child {
|
|
||||||
hasText(getResourceString(R.string.organize_tokens_ungroup))
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val sortByBalanceButton: KNode = child {
|
val sortByBalanceButton: KNode = child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.SORT_BY_BALANCE_BUTTON)
|
hasText(getResourceString(R.string.organize_tokens_sort_by_balance))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
// endregion TopBar
|
// endregion TopBar
|
||||||
|
|
@ -84,7 +79,7 @@ class OrganizeTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM)
|
hasTestTag(OrganizeTokensScreenTestTags.GROUP_TITLE_ITEM)
|
||||||
hasAnyChild(withText(tokenNetwork))
|
hasAnyDescendant(withText(tokenNetwork))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
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
|
||||||
|
|
||||||
|
class SecurityModePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<SecurityModePageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
// Description only appears on the Security Mode screen — unambiguous "screen opened" signal.
|
||||||
|
val longTapOptionDescription: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.details_manage_security_long_tap_description))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveChangesButton: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.common_save_changes))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onSecurityModeScreen(function: SecurityModePageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.FooterTestTags
|
import com.tangem.core.ui.test.FooterTestTags
|
||||||
import com.tangem.core.ui.test.SendAddressScreenTestTags
|
import com.tangem.core.ui.test.SendAddressScreenTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
import com.tangem.features.send.v2.impl.R
|
import com.tangem.features.send.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
@ -97,7 +97,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
): KNode = child {
|
): KNode = child {
|
||||||
hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM)
|
hasTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ITEM)
|
||||||
hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON))
|
hasAnyChild(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_ICON))
|
||||||
hasAnyDescendant(withText(recipientAddress))
|
hasAnyDescendant(withText(recipientAddress, substring = true))
|
||||||
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
|
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TEXT))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,18 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun warningMessageContaining(textPart: String): KNode = child {
|
||||||
|
hasTestTag(NotificationTestTags.MESSAGE)
|
||||||
|
hasText(textPart, substring = true)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun warningTitleContaining(textPart: String): KNode = child {
|
||||||
|
hasTestTag(NotificationTestTags.TITLE)
|
||||||
|
hasText(textPart, substring = true)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun warningIcon(message: String): KNode = child {
|
fun warningIcon(message: String): KNode = child {
|
||||||
hasTestTag(NotificationTestTags.ICON)
|
hasTestTag(NotificationTestTags.ICON)
|
||||||
hasAnySibling(withText(message))
|
hasAnySibling(withText(message))
|
||||||
|
|
@ -96,6 +108,16 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val recipientBlock: KNode = child {
|
||||||
|
hasTestTag(SendConfirmScreenTestTags.RECIPIENT_BLOCK)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val recipientMemo: KNode = child {
|
||||||
|
hasTestTag(SendConfirmScreenTestTags.RECIPIENT_MEMO)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val provider: KNode = child {
|
val provider: KNode = child {
|
||||||
hasText(getResourceString(R.string.express_provider))
|
hasText(getResourceString(R.string.express_provider))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
@ -145,6 +167,12 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun feeBlockCurrency(symbol: String): KNode = child {
|
||||||
|
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
|
||||||
|
hasAnyDescendant(withText(symbol))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val refreshButton: KNode = child {
|
val refreshButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
hasText(getResourceString(CoreUiR.string.warning_button_refresh))
|
hasText(getResourceString(CoreUiR.string.warning_button_refresh))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||||
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
|
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
|
||||||
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gasless fee selector modal: the `NetworkFee` route (fee-paying token row + selected speed) and the
|
||||||
|
* `ChooseToken` route. The `ChooseSpeed` route is covered by [SendSelectNetworkFeeBottomSheetPageObject].
|
||||||
|
*/
|
||||||
|
class SendFeeSelectorBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<SendFeeSelectorBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val networkFeeTitle: KNode = child {
|
||||||
|
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.common_network_fee_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val chooseTokenTitle: KNode = child {
|
||||||
|
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.fee_selector_choose_token_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val feeTokenRow: KNode = child {
|
||||||
|
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun feeTokenItem(tokenName: String): KNode = child {
|
||||||
|
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||||
|
hasAnyChild(withText(tokenName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun feeSpeedItemTitle(speed: String): KNode = child {
|
||||||
|
hasTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE)
|
||||||
|
hasText(speed)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val applyButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.common_apply)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val notEnoughFundsError: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.gasless_not_enough_funds_to_cover_token_fee))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onSendFeeSelectorBottomSheet(function: SendFeeSelectorBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -9,7 +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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import com.tangem.features.send.v2.impl.R as SendR
|
import com.tangem.features.send.impl.R as SendR
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ class SwapSuccessPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App Transfers reuses the swap success screen; in transfer mode its title is "Transfer in progress".
|
||||||
|
val transferInProgressTitle: KNode = child {
|
||||||
|
hasTestTag(TransactionSuccessScreenTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.transfer_in_progress_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val closeButton: KNode = child {
|
val closeButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
hasAnyDescendant(withText(getResourceString(R.string.common_close)))
|
hasAnyDescendant(withText(getResourceString(R.string.common_close)))
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,12 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun feeBlockCurrency(symbol: String): KNode = child {
|
||||||
|
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
|
||||||
|
hasAnyDescendant(withText(symbol))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val receiveAmountShimmer: KNode = child {
|
val receiveAmountShimmer: KNode = child {
|
||||||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
|
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +78,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bestRateText: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.express_provider_best_rate))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val errorNotificationTitle: KNode = child {
|
val errorNotificationTitle: KNode = child {
|
||||||
hasTestTag(NotificationTestTags.TITLE)
|
hasTestTag(NotificationTestTags.TITLE)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
|
|
@ -119,6 +130,12 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun insufficientFeeForTransferNotificationTitle(feeCoinName: String): KNode = child {
|
||||||
|
hasTestTag(NotificationTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.warning_send_blocked_funds_for_fee_title, feeCoinName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun warningTitle(title: String): KNode = child {
|
fun warningTitle(title: String): KNode = child {
|
||||||
hasTestTag(NotificationTestTags.TITLE)
|
hasTestTag(NotificationTestTags.TITLE)
|
||||||
hasText(title)
|
hasText(title)
|
||||||
|
|
@ -147,6 +164,23 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasText(getResourceString(R.string.common_swap))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val transferButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.swapping_transfer_action))
|
||||||
|
}
|
||||||
|
|
||||||
|
val transferTitle: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.common_transfer))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentPill testTag is the PredefinedPercentAmount enum name; MAX == "MAX".
|
||||||
|
val maxAmountButton: KNode = child {
|
||||||
|
hasTestTag("MAX")
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val youSwapBlock: KNode = child {
|
val youSwapBlock: KNode = child {
|
||||||
hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER)
|
hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER)
|
||||||
hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title_v2)))
|
hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title_v2)))
|
||||||
|
|
@ -178,6 +212,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
val swapFiatAmount: KNode = child {
|
val swapFiatAmount: KNode = child {
|
||||||
hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT)
|
hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT)
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val swapSelectTokenIcon: KNode = child {
|
val swapSelectTokenIcon: KNode = child {
|
||||||
|
|
@ -210,6 +245,12 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
hasText(getResourceString(R.string.common_choose_token))
|
hasText(getResourceString(R.string.common_choose_token))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transfer mode auto-fills the memo/destination tag — the manual Send-address field must never render here.
|
||||||
|
val destinationTagField: KNode = child {
|
||||||
|
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =
|
internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.semantics.SemanticsProperties
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
import androidx.compose.ui.test.SemanticsMatcher
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.utils.LazyListItemNode
|
|
||||||
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.NotificationTestTags
|
import com.tangem.core.ui.test.NotificationTestTags
|
||||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
|
||||||
import com.tangem.features.tokendetails.impl.R
|
import com.tangem.features.tokendetails.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
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 io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
@ -36,18 +33,8 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val availableStakingBlockTitle: KNode = child {
|
fun availableStakingBlockText(apy: String): KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE)
|
hasText(getResourceString(R.string.token_details_earn_staking_subtitle, apy))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val availableStakingBlockText: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT)
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val availableStakingBlockCurrencyIcon: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON)
|
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,69 +49,45 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingDot: KNode = child {
|
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT)
|
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val stakingTokenAmount: KNode = child {
|
val stakingTokenAmount: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
|
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingChevronIcon: KNode = child {
|
val stakingTitle: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON)
|
hasText(getResourceString(R.string.common_staking))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val stakingTitle: KNode = child {
|
val stakingEnabledTitle: KNode = child {
|
||||||
hasText(getResourceString(R.string.staking_native))
|
hasText(getResourceString(R.string.staking_enabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val horizontalActionChips = KLazyListNode(
|
val fiatBalance: KNode = child {
|
||||||
semanticsProvider = semanticsProvider,
|
hasAnyAncestor(withTestTag(TokenDetailsScreenTestTags.BALANCE_FIAT))
|
||||||
viewBuilderAction = { hasTestTag(BaseActionButtonsBlockTestTags.HORIZONTAL_ACTION_CHIPS) },
|
addSemanticsMatcher(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text))
|
||||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
useUnmergedTree = true
|
||||||
positionMatcher = { position ->
|
|
||||||
SemanticsMatcher.expectValue(
|
|
||||||
LazyListItemPositionSemantics,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
fun receiveButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
|
||||||
hasText(getResourceString(R.string.common_receive))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
val addFundsButton: KNode = child {
|
||||||
fun swapButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasAnyDescendant(withText(getResourceString(R.string.tangempay_card_details_add_funds)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
val swapButton: KNode = child {
|
||||||
fun sellButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_sell))
|
hasAnyDescendant(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
val transferButton: KNode = child {
|
||||||
fun buyButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||||
hasText(getResourceString(R.string.common_buy))
|
hasAnyDescendant(withText(getResourceString(R.string.common_transfer)))
|
||||||
}
|
useUnmergedTree = true
|
||||||
|
|
||||||
@OptIn(ExperimentalTestApi::class)
|
|
||||||
fun sendButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
|
|
||||||
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
|
||||||
hasText(getResourceString(R.string.common_send))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
fun networkFeeNotificationIcon(feeCurrencyName: String): KNode = child {
|
||||||
|
|
@ -204,7 +167,6 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT))
|
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT))
|
||||||
hasAnyDescendant(withTestTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_CHEVRON_ICON))
|
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||||
|
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 TransferBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<TransferBottomSheetPageObject>(
|
||||||
|
semanticsProvider = semanticsProvider,
|
||||||
|
viewBuilderAction = { hasTestTag(BaseBottomSheetTestTags.CONTAINER) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val sendButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_send)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val swapButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val sellButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_sell)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasAnyChild(withText(getResourceString(R.string.common_close)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onTransferBottomSheet(function: TransferBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.TransactionHistoryItemTestTags
|
||||||
|
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 TxHistoryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<TxHistoryPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
fun transactionItem(title: String): KNode = child {
|
||||||
|
hasTestTag(TransactionHistoryItemTestTags.ITEM)
|
||||||
|
hasAnyDescendant(withText(title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transactionAmount(title: String): KNode = transactionItem(title).child {
|
||||||
|
hasTestTag(TransactionHistoryItemTestTags.AMOUNT)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transactionCurrency(title: String): KNode = transactionItem(title).child {
|
||||||
|
hasTestTag(TransactionHistoryItemTestTags.CURRENCY)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transactionConfirmedStatus(title: String): KNode = transactionItem(title).child {
|
||||||
|
hasTestTag(TransactionHistoryItemTestTags.STATUS_CONFIRMED)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onTxHistoryScreen(function: TxHistoryPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
|
|
@ -9,11 +10,16 @@ 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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<WalletSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<WalletSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val screenContainer: KNode = child {
|
||||||
|
hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER)
|
||||||
|
}
|
||||||
|
|
||||||
val topAppBarBackButton: KNode = child {
|
val topAppBarBackButton: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +28,31 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM)
|
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 {
|
val linkMoreCardsButton: KNode = walletSettingsItem.child {
|
||||||
hasText(getResourceString(R.string.details_row_title_create_backup))
|
hasText(getResourceString(R.string.details_row_title_create_backup))
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +69,16 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
||||||
hasText(getResourceString(R.string.settings_forget_wallet))
|
hasText(getResourceString(R.string.settings_forget_wallet))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val renameWalletButton: KNode = child {
|
||||||
|
hasTestTag(WalletSettingsScreenTestTags.RENAME_BUTTON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun walletNameValue(name: String): KNode = walletSettingsItem.child {
|
||||||
|
hasText(name)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val accountsListContainer: KNode = walletSettingsItem.child {
|
val accountsListContainer: KNode = walletSettingsItem.child {
|
||||||
hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER)
|
hasTestTag(WalletSettingsScreenTestTags.ACCOUNTS_CONTAINER)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.tangem.screens
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
|
||||||
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
import com.tangem.core.ui.test.WarningBottomSheetTestTags
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
|
|
@ -31,27 +30,23 @@ class WarningBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsP
|
||||||
}
|
}
|
||||||
|
|
||||||
val okGotItButton: KNode = child {
|
val okGotItButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(WarningBottomSheetTestTags.BUTTON_SECONDARY)
|
||||||
hasText(getResourceString(R.string.warning_button_ok))
|
hasText(getResourceString(R.string.warning_button_ok))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val gotItButton: KNode = child {
|
val gotItButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||||
hasText(getResourceString(R.string.common_got_it))
|
hasText(getResourceString(R.string.common_got_it))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val cancelButton: KNode = child {
|
val cancelButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(WarningBottomSheetTestTags.BUTTON_PRIMARY)
|
||||||
hasText(getResourceString(R.string.common_cancel))
|
hasText(getResourceString(R.string.common_cancel))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val connectAnywayButton: KNode = child {
|
val connectAnywayButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(WarningBottomSheetTestTags.BUTTON_SECONDARY)
|
||||||
hasText(getResourceString(R.string.wc_alert_connect_anyway))
|
hasText(getResourceString(R.string.wc_alert_connect_anyway))
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.tangem.screens.accounts
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
|
import com.tangem.core.ui.test.accounts.AccountInfoEditScreenTestTags
|
||||||
|
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 AccountInfoEditPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<AccountInfoEditPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val screenContainer: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.ACCOUNT_DETAILS_CONTAINER)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountNameField: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.NAME_FIELD)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountCurrentIcon: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.SELECTED_ICON)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountColorOption: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.COLOR_OPTION)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountTypeOption: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.TYPE_OPTION)
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveAccountButton: KNode = child {
|
||||||
|
hasTestTag(AccountInfoEditScreenTestTags.SAVE_ACCOUNT_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
val crossButton: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onAccountInfoEditorScreen(function: AccountInfoEditPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -26,7 +26,7 @@ class TangemPayCardPagePageObject(semanticsProvider: SemanticsNodeInteractionsPr
|
||||||
}
|
}
|
||||||
|
|
||||||
val showDetailsButton: KNode = child {
|
val showDetailsButton: KNode = child {
|
||||||
hasTestTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON)
|
hasTestTag(TangemPayTestTags.SHOW_DETAILS_ROW)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package com.tangem.tests
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class AppCurrencyTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("781")
|
||||||
|
@DisplayName("App Currency: change of equivalent")
|
||||||
|
@Test
|
||||||
|
fun changeAppCurrencyTest() {
|
||||||
|
val currenciesScenario = "currencies_api"
|
||||||
|
val appSettingsState = "AppSettings"
|
||||||
|
val targetCurrency = "EUR"
|
||||||
|
val targetSymbol = "€"
|
||||||
|
val token = "Bitcoin"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = { resetWireMockScenarioState(currenciesScenario) },
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$currenciesScenario' to '$appSettingsState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = currenciesScenario, state = appSettingsState)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
synchronizeAddresses()
|
||||||
|
step("Open wallet details") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'App settings' button") {
|
||||||
|
onDetailsScreen { appSettingsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'App currency' button") {
|
||||||
|
onAppSettingsScreen { currencyButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on search button") {
|
||||||
|
onAppCurrencySelectorScreen { searchActionButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Search currency '$targetCurrency'") {
|
||||||
|
onAppCurrencySelectorScreen { searchField.performTextInput(targetCurrency) }
|
||||||
|
}
|
||||||
|
step("Click on currency '$targetCurrency'") {
|
||||||
|
onAppCurrencySelectorScreen { currencyItem(targetCurrency).performClick() }
|
||||||
|
}
|
||||||
|
step("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("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.
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onMainScreen { totalBalanceText.assertTextContains(targetSymbol, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on token '$token'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(token).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert token fiat balance contains '$targetSymbol'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTokenDetailsScreen { fiatBalance.assertTextContains(targetSymbol, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,8 +44,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,8 +91,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,8 +165,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,8 +251,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -336,8 +336,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -425,8 +425,8 @@ class BuyTokenTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.clickWithAssertion() }
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onChooseTokenScreen {
|
onChooseTokenBottomSheet {
|
||||||
topAppBarTitle.assertIsDisplayed()
|
title.assertIsDisplayed()
|
||||||
tokenWithTitle(tokenTitle).clickWithAssertion()
|
tokenWithTitle(tokenTitle).clickWithAssertion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,21 @@ import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.domain.models.scan.ProductType
|
import com.tangem.domain.models.scan.ProductType
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
import com.tangem.screens.*
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.content.Firmware412MockContent
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.content.S2CMockContent
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.content.SingleCurrencyMockContent
|
||||||
|
import com.tangem.tap.domain.sdk.mocks.content.V3MockContent
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class DetailsTest : BaseTestCase() {
|
class DetailsTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("836")
|
||||||
|
@DisplayName("Details: (Wallet) fields")
|
||||||
@Test
|
@Test
|
||||||
fun walletWithoutBackupDetailsTest() =
|
fun walletWithoutBackupDetailsTest() =
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
|
|
@ -46,70 +53,26 @@ class DetailsTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
onWalletSettingsScreen {
|
onWalletSettingsScreen {
|
||||||
step("Assert 'Link more cards' button is visible") {
|
step("Assert 'Link more cards' button is visible") {
|
||||||
|
scrollToLinkMoreCards()
|
||||||
linkMoreCardsButton.assertIsDisplayed()
|
linkMoreCardsButton.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
step("Assert 'Card Settings' button is visible") {
|
step("Assert 'Card Settings' button is visible") {
|
||||||
|
scrollToDeviceSettings()
|
||||||
deviceSettingsButton.assertIsDisplayed()
|
deviceSettingsButton.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
step("Assert 'Referral program' button is visible") {
|
step("Assert 'Referral program' button is visible") {
|
||||||
|
scrollToReferralProgram()
|
||||||
referralProgramButton.assertIsDisplayed()
|
referralProgramButton.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
step("Assert 'Forget wallet' button is visible") {
|
step("Assert 'Forget wallet' button is visible") {
|
||||||
|
scrollToForgetWallet()
|
||||||
forgetWalletButton.assertIsDisplayed()
|
forgetWalletButton.assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
@AllureId("837")
|
||||||
fun wallet2DetailsTest() =
|
@DisplayName("Details: (Note) fields")
|
||||||
setupHooks().run {
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen(productType = ProductType.Wallet2)
|
|
||||||
}
|
|
||||||
onMainScreenTopBar {
|
|
||||||
step("Open wallet details") {
|
|
||||||
moreButton.clickWithAssertion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDetailsScreen {
|
|
||||||
step("Assert 'Wallet connect' button is visible") {
|
|
||||||
walletConnectButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Scan card' button is visible") {
|
|
||||||
scanCardButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Buy Tangem card' button is visible") {
|
|
||||||
buyTangemButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'App settings' button is visible") {
|
|
||||||
appSettingsButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Contact support' button is visible") {
|
|
||||||
contactSupportButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Terms or service' button is visible") {
|
|
||||||
toSButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Open 'Wallet settings' screen") {
|
|
||||||
walletNameButton.clickWithAssertion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onWalletSettingsScreen {
|
|
||||||
step("Assert 'Link more cards' button does not exist") {
|
|
||||||
linkMoreCardsButton.assertIsNotDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Card Settings' button is visible") {
|
|
||||||
deviceSettingsButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Referral program' button is visible") {
|
|
||||||
referralProgramButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
step("Assert 'Forget wallet' button is visible") {
|
|
||||||
forgetWalletButton.assertIsDisplayed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun noteDetailsTest() =
|
fun noteDetailsTest() =
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
|
|
@ -154,6 +117,214 @@ 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("838")
|
||||||
|
@DisplayName("Details: (v3 multicurrency) fields")
|
||||||
|
@Test
|
||||||
|
fun v3MultiCurrencyDetailsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen(mockContent = V3MockContent)
|
||||||
|
}
|
||||||
|
onMainScreenTopBar {
|
||||||
|
step("Open wallet details") {
|
||||||
|
moreButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDetailsScreen {
|
||||||
|
step("Assert 'Wallet connect' button is displayed") {
|
||||||
|
walletConnectButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Buy Tangem card' button is displayed") {
|
||||||
|
buyTangemButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'App settings' button is displayed") {
|
||||||
|
appSettingsButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Contact support' button is displayed") {
|
||||||
|
contactSupportButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Terms of service' button is displayed") {
|
||||||
|
toSButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert app version is displayed") {
|
||||||
|
versionName.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9832")
|
||||||
|
@DisplayName("Details: (single currency) fields")
|
||||||
|
@Test
|
||||||
|
fun singleCurrencyDetailsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen(mockContent = SingleCurrencyMockContent)
|
||||||
|
}
|
||||||
|
onMainScreenTopBar {
|
||||||
|
step("Open wallet details") {
|
||||||
|
moreButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDetailsScreen {
|
||||||
|
step("Assert 'Wallet connect' button is not displayed") {
|
||||||
|
walletConnectButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Buy Tangem card' button is displayed") {
|
||||||
|
buyTangemButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'App settings' button is displayed") {
|
||||||
|
appSettingsButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Contact support' button is displayed") {
|
||||||
|
contactSupportButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Terms of service' button is displayed") {
|
||||||
|
toSButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert app version is displayed") {
|
||||||
|
versionName.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("841")
|
||||||
|
@DisplayName("Details: (S2C) fields")
|
||||||
|
@Test
|
||||||
|
fun s2cDetailsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen(mockContent = S2CMockContent)
|
||||||
|
}
|
||||||
|
onMainScreenTopBar {
|
||||||
|
step("Open wallet details") {
|
||||||
|
moreButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDetailsScreen {
|
||||||
|
step("Assert 'Wallet connect' button is not displayed") {
|
||||||
|
walletConnectButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Buy Tangem card' button is displayed") {
|
||||||
|
buyTangemButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'App settings' button is displayed") {
|
||||||
|
appSettingsButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Contact support' button is displayed") {
|
||||||
|
contactSupportButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Terms of service' button is displayed") {
|
||||||
|
toSButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert app version is displayed") {
|
||||||
|
versionName.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parked: createWalletActions adds Sell for single-wallet cards with no isStart2Coin() check.
|
||||||
|
@Ignore("[REDACTED_JIRA]")
|
||||||
|
@AllureId("2869")
|
||||||
|
@DisplayName("Details: (S2C) no trade buttons and standard details")
|
||||||
|
@Test
|
||||||
|
fun s2cNoTradeButtonsDetailsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen(mockContent = S2CMockContent)
|
||||||
|
}
|
||||||
|
onMainScreen {
|
||||||
|
step("Assert 'Buy' button is not displayed") {
|
||||||
|
buyButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Transfer' button is not displayed") {
|
||||||
|
transferButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button is not displayed") {
|
||||||
|
swapButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMainScreenTopBar {
|
||||||
|
step("Open wallet details") {
|
||||||
|
moreButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDetailsScreen {
|
||||||
|
step("Assert 'Wallet connect' button is not displayed") {
|
||||||
|
walletConnectButton.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AllureId("3647")
|
@AllureId("3647")
|
||||||
@DisplayName("Referral program: validate screen")
|
@DisplayName("Referral program: validate screen")
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -199,4 +370,32 @@ class DetailsTest : BaseTestCase() {
|
||||||
onReferralProgramScreen { participateButton.assertIsDisplayed() }
|
onReferralProgramScreen { participateButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("222")
|
||||||
|
@DisplayName("Details: ToS screen opening")
|
||||||
|
@Test
|
||||||
|
fun tosScreenOpeningTest() {
|
||||||
|
val tosUrl = "https://tangem.com/tangem_tos.html"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Open wallet details") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'ToS' screen") {
|
||||||
|
onDetailsScreen { toSButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Verify 'ToS' screen opened") {
|
||||||
|
onDisclaimerScreen {
|
||||||
|
title.assertIsDisplayed()
|
||||||
|
webView.assertIsDisplayed()
|
||||||
|
webView.assertContentDescriptionContains(tosUrl, true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import com.tangem.screens.onSendScreen
|
||||||
import com.tangem.screens.onStoriesScreen
|
import com.tangem.screens.onStoriesScreen
|
||||||
import com.tangem.screens.onTokenDetailsScreen
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
import com.tangem.screens.onMainScreenTopBar
|
import com.tangem.screens.onMainScreenTopBar
|
||||||
|
import com.tangem.screens.onTransferBottomSheet
|
||||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
|
@ -94,8 +95,11 @@ class FeedbackTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$sendAmount' in input text field") {
|
step("Type '$sendAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
fun groupTokensTest() {
|
fun groupTokensTest() {
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
val tokenTitle = "Ethereum"
|
val tokenTitle = "Ethereum"
|
||||||
val tokenNetwork = "Ethereum network"
|
val networkTitleOrganize = "Ethereum"
|
||||||
|
val networkTitleMain = "Ethereum network"
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
|
|
@ -39,17 +40,20 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
step("Open organize menu") {
|
||||||
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
step("Click 'Group' button") {
|
step("Click 'Group' button") {
|
||||||
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were grouped on 'Organize tokens' screen") {
|
step("Assert tokens were grouped on 'Organize tokens' screen") {
|
||||||
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
|
onOrganizeTokensScreen { tokenNetworkGroupTitle(networkTitleOrganize).assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'Apply' button") {
|
step("Click 'Apply' button") {
|
||||||
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were grouped on 'Main screen'") {
|
step("Assert tokens were grouped on 'Main screen'") {
|
||||||
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
|
onMainScreen { tokenNetworkGroupTitle(networkTitleMain).assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Open 'Organize tokens' screen") {
|
step("Open 'Organize tokens' screen") {
|
||||||
openOrganizeTokensScreen()
|
openOrganizeTokensScreen()
|
||||||
|
|
@ -60,17 +64,20 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
tokenWithTitle(tokenTitle).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Click 'Ungroup' button") {
|
step("Open organize menu") {
|
||||||
onOrganizeTokensScreen { ungroupButton.clickWithAssertion() }
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click 'Group' checkbox again to ungroup") {
|
||||||
|
onOrganizeTokensScreen { groupButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were ungrouped on 'Organize tokens' screen") {
|
step("Assert tokens were ungrouped on 'Organize tokens' screen") {
|
||||||
onOrganizeTokensScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
|
onOrganizeTokensScreen { tokenNetworkGroupTitle(networkTitleOrganize).assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'Apply' button") {
|
step("Click 'Apply' button") {
|
||||||
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
onOrganizeTokensScreen { applyButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert tokens were ungrouped on 'Main screen'") {
|
step("Assert tokens were ungrouped on 'Main screen'") {
|
||||||
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsNotDisplayed() }
|
onMainScreen { tokenNetworkGroupTitle(networkTitleMain).assertIsNotDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +192,9 @@ class OrganizeTokensTest : BaseTestCase() {
|
||||||
tokenWithTitleAndPosition(polExMaticTitle, 3).assertIsDisplayed()
|
tokenWithTitleAndPosition(polExMaticTitle, 3).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
step("Open organize menu") {
|
||||||
|
onOrganizeTokensScreen { organizeMenuButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
step("Click 'By Balance' button") {
|
step("Click 'By Balance' button") {
|
||||||
onOrganizeTokensScreen {
|
onOrganizeTokensScreen {
|
||||||
sortByBalanceButton.clickWithAssertion()
|
sortByBalanceButton.clickWithAssertion()
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(cardType)
|
openMainScreen(cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '${cardType.name}' $cardBlockchain card") {
|
step("Check 'Main' screen for '${cardType.name}' $cardBlockchain card") {
|
||||||
checkSingleCurrencyMainScreen(
|
checkSingleCurrencyMainScreen(cardTitle = cardType.name)
|
||||||
cardBlockchain = cardBlockchain,
|
|
||||||
cardTitle = cardType.name,
|
|
||||||
withTransactions = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +53,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$cardName' $cardBlockchain card") {
|
step("Check 'Main' screen for '$cardName' $cardBlockchain card") {
|
||||||
checkSingleCurrencyMainScreen(cardBlockchain = cardBlockchain, cardTitle = cardName)
|
checkSingleCurrencyMainScreen(cardTitle = cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +62,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: Card with Secp256k1 curve")
|
@DisplayName("Scan: Card with Secp256k1 curve")
|
||||||
@Test
|
@Test
|
||||||
fun secpk1CurveCardScanTest() {
|
fun secpk1CurveCardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: MockContent = Secpk1CurveMockContent
|
val cardType: MockContent = Secpk1CurveMockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "card with Secp256k1 curve"
|
val card = "card with Secp256k1 curve"
|
||||||
|
|
@ -75,12 +70,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on $card") {
|
step("Open 'Main Screen' on $card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for $card curve") {
|
||||||
checkMultiCurrencyMainScreen(
|
checkMultiCurrencyMainScreen(cardTitle = cardName)
|
||||||
devicesCount = devicesCount,
|
|
||||||
cardTitle = cardName,
|
|
||||||
withWalletImage = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,11 +90,7 @@ class ScanCardTest : BaseTestCase() {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for $card with blockchain: '$cardBlockchain'") {
|
step("Check 'Main' screen for $card with blockchain: '$cardBlockchain'") {
|
||||||
checkSingleCurrencyMainScreen(
|
checkSingleCurrencyMainScreen(cardTitle = cardName)
|
||||||
cardBlockchain = cardBlockchain,
|
|
||||||
cardTitle = cardName,
|
|
||||||
withWalletImage = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +99,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Shiba' card")
|
@DisplayName("Scan: 'Shiba' card")
|
||||||
@Test
|
@Test
|
||||||
fun shibaCardScanTest() {
|
fun shibaCardScanTest() {
|
||||||
val devicesCount = "2 devices"
|
|
||||||
val cardType: MockContent = ShibaMockContent
|
val cardType: MockContent = ShibaMockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "Shiba"
|
val card = "Shiba"
|
||||||
|
|
@ -121,8 +107,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card' card") {
|
step("Open 'Main Screen' on '$card' card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +117,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Ring'")
|
@DisplayName("Scan: 'Ring'")
|
||||||
@Test
|
@Test
|
||||||
fun ringScanTest() {
|
fun ringScanTest() {
|
||||||
val devicesCount = "3 devices"
|
|
||||||
val cardType: ProductType = ProductType.Ring
|
val cardType: ProductType = ProductType.Ring
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val ring = "Ring"
|
val ring = "Ring"
|
||||||
|
|
@ -140,8 +125,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$ring'") {
|
step("Open 'Main Screen' on '$ring'") {
|
||||||
openMainScreen(productType = cardType)
|
openMainScreen(productType = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$ring'") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +135,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Wallet' card")
|
@DisplayName("Scan: 'Wallet' card")
|
||||||
@Test
|
@Test
|
||||||
fun walletCardScanTest() {
|
fun walletCardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: ProductType = ProductType.Wallet
|
val cardType: ProductType = ProductType.Wallet
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
|
|
||||||
|
|
@ -158,8 +142,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$cardName' card") {
|
step("Open 'Main Screen' on '$cardName' card") {
|
||||||
openMainScreen(productType = cardType)
|
openMainScreen(productType = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$cardName' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$cardName' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +152,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: 'Wallet 2' card")
|
@DisplayName("Scan: 'Wallet 2' card")
|
||||||
@Test
|
@Test
|
||||||
fun wallet2ScanTest() {
|
fun wallet2ScanTest() {
|
||||||
val devicesCount = "2 devices"
|
|
||||||
val cardType: MockContent = Wallet2MockContent
|
val cardType: MockContent = Wallet2MockContent
|
||||||
val cardName = "Wallet"
|
val cardName = "Wallet"
|
||||||
val card = "Wallet 2"
|
val card = "Wallet 2"
|
||||||
|
|
@ -177,8 +160,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card' card") {
|
step("Open 'Main Screen' on '$card' card") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card' card") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +170,6 @@ class ScanCardTest : BaseTestCase() {
|
||||||
@DisplayName("Scan: Card with 4.12 firmware")
|
@DisplayName("Scan: Card with 4.12 firmware")
|
||||||
@Test
|
@Test
|
||||||
fun firmware412CardScanTest() {
|
fun firmware412CardScanTest() {
|
||||||
val devicesCount = "1 device"
|
|
||||||
val cardType: MockContent = Firmware412MockContent
|
val cardType: MockContent = Firmware412MockContent
|
||||||
val cardName = "Tangem card"
|
val cardName = "Tangem card"
|
||||||
val card = "card with 4.12 firmware"
|
val card = "card with 4.12 firmware"
|
||||||
|
|
@ -196,8 +178,8 @@ class ScanCardTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$card'") {
|
step("Open 'Main Screen' on '$card'") {
|
||||||
openMainScreen(mockContent = cardType)
|
openMainScreen(mockContent = cardType)
|
||||||
}
|
}
|
||||||
step("Check 'Main' screen for '$card' with devices count = '$devicesCount'") {
|
step("Check 'Main' screen for '$card'") {
|
||||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
checkMultiCurrencyMainScreen(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.tangem.tests
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.domain.models.scan.ProductType
|
||||||
|
import com.tangem.scenarios.openDeviceSettingsScreen
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.scanCardInDeviceSettings
|
||||||
|
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: available for Twin cards")
|
||||||
|
@Test
|
||||||
|
fun securityModeOpensForTwinsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen(productType = ProductType.Twins, isTwinsCard = true)
|
||||||
|
}
|
||||||
|
step("Open 'Device settings' screen") {
|
||||||
|
openDeviceSettingsScreen()
|
||||||
|
}
|
||||||
|
step("Scan card in 'Device settings'") {
|
||||||
|
scanCardInDeviceSettings()
|
||||||
|
}
|
||||||
|
step("Assert 'Security mode' row is enabled") {
|
||||||
|
onDeviceSettingsScreen { securityModeRow.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Click on 'Security mode' button") {
|
||||||
|
onDeviceSettingsScreen { securityModeRow.performClick() }
|
||||||
|
}
|
||||||
|
onSecurityModeScreen {
|
||||||
|
step("Assert 'Long tap' option is displayed") {
|
||||||
|
longTapOptionDescription.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
step("Assert 'Save changes' button is displayed") {
|
||||||
|
saveChangesButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9831")
|
||||||
|
@DisplayName("Security Mode: unavailable for single-capability cards")
|
||||||
|
@Test
|
||||||
|
fun securityModeRowDisabledForOtherCardsTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Open 'Device settings' screen") {
|
||||||
|
openDeviceSettingsScreen()
|
||||||
|
}
|
||||||
|
step("Scan card in 'Device settings'") {
|
||||||
|
scanCardInDeviceSettings()
|
||||||
|
}
|
||||||
|
step("Assert 'Security mode' row title is displayed") {
|
||||||
|
onDeviceSettingsScreen { securityModeRowTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Security mode' row is disabled") {
|
||||||
|
onDeviceSettingsScreen { securityModeRow.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,20 +56,14 @@ class StakingTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking title' is displayed") {
|
step("Assert 'Staking title' is displayed") {
|
||||||
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingEnabledTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking fiat amount' is displayed") {
|
step("Assert 'Staking fiat amount' is displayed") {
|
||||||
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking dot' is displayed") {
|
|
||||||
onTokenDetailsScreen { stakingDot.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Staking token amount' is displayed") {
|
step("Assert 'Staking token amount' is displayed") {
|
||||||
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Staking block chevron icon' is displayed") {
|
|
||||||
onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +133,7 @@ class StakingTest : BaseTestCase() {
|
||||||
val scenarioName = "staking_eth_pol_balances_android"
|
val scenarioName = "staking_eth_pol_balances_android"
|
||||||
val scenarioState = "Started"
|
val scenarioState = "Started"
|
||||||
val stakingAmount = "1"
|
val stakingAmount = "1"
|
||||||
|
val stakingApy = "2.84%"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
|
|
@ -172,13 +167,10 @@ class StakingTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
|
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Available staking block' title is displayed") {
|
step("Assert 'Available staking block' title is displayed") {
|
||||||
onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() }
|
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Available staking block' text is displayed") {
|
step("Assert 'Available staking block' text is displayed") {
|
||||||
onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() }
|
onTokenDetailsScreen { availableStakingBlockText(stakingApy).assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Assert 'Available staking block' currency icon is displayed") {
|
|
||||||
onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Click on 'Stake' button") {
|
step("Click on 'Stake' button") {
|
||||||
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
|
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.tangem.tests
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class WalletRenameTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("2264")
|
||||||
|
@DisplayName("Wallet details: rename wallet")
|
||||||
|
@Test
|
||||||
|
fun renameWalletTest() =
|
||||||
|
setupHooks().run {
|
||||||
|
val newWalletName = "Tangem QA"
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Open wallet details") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'Wallet settings' screen") {
|
||||||
|
onDetailsScreen { walletNameButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Rename' button") {
|
||||||
|
onWalletSettingsScreen {
|
||||||
|
scrollToRenameButton()
|
||||||
|
renameWalletButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Enter new wallet name '$newWalletName'") {
|
||||||
|
onDialog { inputField.performTextReplacement(newWalletName) }
|
||||||
|
}
|
||||||
|
step("Click on 'OK' button") {
|
||||||
|
onDialog { okButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert new wallet name '$newWalletName' is displayed on 'Wallet settings' screen") {
|
||||||
|
onWalletSettingsScreen { walletNameValue(newWalletName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Back' button") {
|
||||||
|
onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert new wallet name '$newWalletName' is displayed on 'Details' screen") {
|
||||||
|
onDetailsScreen { walletNameValue(newWalletName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Back' button") {
|
||||||
|
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert new wallet name '$newWalletName' is displayed on 'Main' screen") {
|
||||||
|
onMainScreen { walletNameText.assertTextContains(newWalletName) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
package com.tangem.tests
|
package com.tangem.tests
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
|
import com.tangem.tap.domain.sdk.mocks.content.DevWalletMockContent
|
||||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithSeedPhraseMockContent
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.Allure.step
|
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.accounts
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.REFERRAL_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.REFERRAL_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.clickAndWaitFor
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -10,7 +11,9 @@ import com.tangem.core.ui.R
|
||||||
import com.tangem.scenarios.*
|
import com.tangem.scenarios.*
|
||||||
import com.tangem.screens.accounts.onAccountDetailsScreen
|
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||||
import com.tangem.screens.accounts.onArchivedAccountsScreen
|
import com.tangem.screens.accounts.onArchivedAccountsScreen
|
||||||
|
import com.tangem.screens.onDetailsScreen
|
||||||
import com.tangem.screens.onDialog
|
import com.tangem.screens.onDialog
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
import com.tangem.screens.onWalletSettingsScreen
|
import com.tangem.screens.onWalletSettingsScreen
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
|
@ -164,8 +167,8 @@ class AccountArchivationsTest : BaseTestCase() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@AllureId("5976")
|
@AllureId("5976")
|
||||||
@DisplayName("Accounts: restore an archived account")
|
@DisplayName("Accounts: restore a simple archived account")
|
||||||
fun restoreArchivedAccountTest() {
|
fun restoreSimpleArchivedAccountTest() {
|
||||||
val archivedAccountName = "Account 3"
|
val archivedAccountName = "Account 3"
|
||||||
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
|
val userAccountsInitialState = "TwoAccountsWithArchivedAccounts"
|
||||||
val userAccountsAfterArchivationState = "ReadyToRestore"
|
val userAccountsAfterArchivationState = "ReadyToRestore"
|
||||||
|
|
@ -202,6 +205,108 @@ class AccountArchivationsTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("5980")
|
||||||
|
@DisplayName("Accounts: restore archived account with custom token transfer")
|
||||||
|
fun restoreArchivedAccountWithCustomTokensTest() {
|
||||||
|
val mainAccountName = "Main account"
|
||||||
|
val archivedAccountName = "Account 2"
|
||||||
|
val customTokenName = "Ethereum"
|
||||||
|
val expectedArchivedTokensInfo = "1 token"
|
||||||
|
val userAccountsInitialState = "OneAccountWithArchivedCustomToken"
|
||||||
|
val userAccountsReadyToRestoreState = "ReadyToRestoreCustomToken"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeSection = {
|
||||||
|
setWireMockScenarioState(userTokensScenario, userAccountsInitialState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(userTokensScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open wallet settings") { openWalletSettingsScreen() }
|
||||||
|
step("Open 'Archived accounts' screen") { openArchivedAccountsScreen() }
|
||||||
|
|
||||||
|
step("Verify archived account '$archivedAccountName' shows '$expectedArchivedTokensInfo'") {
|
||||||
|
onArchivedAccountsScreen {
|
||||||
|
val row = findArchivedAccountItemByName(archivedAccountName)
|
||||||
|
row.container.assertIsDisplayed()
|
||||||
|
row.subtitle.assertTextContains(expectedArchivedTokensInfo, substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Switch WireMock to '$userAccountsReadyToRestoreState'") {
|
||||||
|
setWireMockScenarioState(userTokensScenario, userAccountsReadyToRestoreState)
|
||||||
|
}
|
||||||
|
step("Click restore button for '$archivedAccountName'") {
|
||||||
|
onArchivedAccountsScreen {
|
||||||
|
findArchivedAccountItemByName(archivedAccountName)
|
||||||
|
.restoreButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert custom token migration dialog is displayed") {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert dialog text mentions main account '$mainAccountName'") {
|
||||||
|
onDialog { text.assertTextContains(mainAccountName, substring = true) }
|
||||||
|
}
|
||||||
|
step("Assert dialog text mentions restoring account '$archivedAccountName'") {
|
||||||
|
onDialog { text.assertTextContains(archivedAccountName, substring = true) }
|
||||||
|
}
|
||||||
|
step("Confirm migration in dialog") {
|
||||||
|
onDialog { gotItButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert 'Wallet settings' screen is displayed") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert restored account '$archivedAccountName' is in active accounts list") {
|
||||||
|
onWalletSettingsScreen { accountItem(archivedAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Navigate back to wallet details") {
|
||||||
|
onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Navigate back to main screen") {
|
||||||
|
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert main account '$mainAccountName' is visible on main screen") {
|
||||||
|
onMainScreen { findAccountSectionByName(mainAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert restored account '$archivedAccountName' is visible on main screen") {
|
||||||
|
onMainScreen { findAccountSectionByName(archivedAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Expand main account '$mainAccountName'") {
|
||||||
|
onMainScreen { findAccountSectionByName(mainAccountName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert '$customTokenName' is NOT displayed under main account") {
|
||||||
|
onMainScreen { assertTokenDoesNotExist(customTokenName) }
|
||||||
|
}
|
||||||
|
step("Expand main account '$mainAccountName'") {
|
||||||
|
onMainScreen { findAccountSectionByName(mainAccountName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert '$customTokenName' is NOT displayed under main account") {
|
||||||
|
onMainScreen {
|
||||||
|
assertTokenDoesNotExist(customTokenName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Expand restored account '$archivedAccountName' and assert '$customTokenName' is displayed") {
|
||||||
|
onMainScreen {
|
||||||
|
findAccountSectionByName(archivedAccountName).clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onMainScreen { findTokenInAnyAccountByName(customTokenName).assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@AllureId("7962")
|
@AllureId("7962")
|
||||||
@DisplayName("Accounts: restore archived account error")
|
@DisplayName("Accounts: restore archived account error")
|
||||||
|
|
@ -250,4 +355,5 @@ class AccountArchivationsTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,492 @@
|
||||||
|
package com.tangem.tests.accounts
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.longClick
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.R
|
||||||
|
import com.tangem.common.extensions.clickAndWaitFor
|
||||||
|
import com.tangem.common.extensions.clickOnSystemButton
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.DerivationPathHelper
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setClipboardText
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.screens.accounts.onAccountInfoEditorScreen
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class AccountCreationTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val userTokensScenario = "user_tokens_api"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("5504")
|
||||||
|
@DisplayName("Accounts: account creation network error handling")
|
||||||
|
fun accountCreationErrorTest() {
|
||||||
|
val accountName = "Account 2"
|
||||||
|
val userAccountsGetErrorState = "AccountsGetError"
|
||||||
|
val userAccountsPutErrorState = "AccountsPutError"
|
||||||
|
val userAccountsBeforeCreationState = "AccountReadyToCreate"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeSection = {
|
||||||
|
setWireMockScenarioState(userTokensScenario, userAccountsGetErrorState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(userTokensScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open wallet settings") { openWalletSettingsScreen() }
|
||||||
|
step("Start account creation") { startAccountCreation() }
|
||||||
|
|
||||||
|
step("Enter account name: '$accountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performClick()
|
||||||
|
accountNameField.performTextInput(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click 'Add account' button (GET accounts is blocked)") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert error dialog details") {
|
||||||
|
assertErrorDialog(
|
||||||
|
expectedTitle = getResourceString(R.string.common_something_went_wrong),
|
||||||
|
expectedMessage = getResourceString(com.tangem.core.ui.R.string.account_generic_error_dialog_message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Dismiss error dialog") { dismissErrorDialog() }
|
||||||
|
step("Assert still on account creation screen") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Unblock GET accounts, block PUT accounts") {
|
||||||
|
setWireMockScenarioState(userTokensScenario, userAccountsPutErrorState)
|
||||||
|
}
|
||||||
|
step("Click 'Add account' button again (PUT accounts is blocked)") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert still on account creation screen") {
|
||||||
|
assertErrorDialog(
|
||||||
|
expectedTitle = getResourceString(R.string.common_something_went_wrong),
|
||||||
|
expectedMessage = getResourceString(R.string.account_generic_error_dialog_message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Unblock both 'accounts' requests") {
|
||||||
|
setWireMockScenarioState(userTokensScenario, userAccountsBeforeCreationState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Dismiss error dialog") { dismissErrorDialog() }
|
||||||
|
step("Assert still on account creation screen") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Click 'Add account' button again (both requests unblocked)") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Manage Tokens' title is displayed") {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Close 'Manage Tokens' screen") {
|
||||||
|
onManageTokensScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Wallet settings' screen is displayed") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert new account '$accountName' appears in accounts list") {
|
||||||
|
onWalletSettingsScreen { accountItem(accountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("5507")
|
||||||
|
@DisplayName("Accounts: name field verifications")
|
||||||
|
fun accountsCreationNameFieldValidationTest() {
|
||||||
|
val accountName = "TestAccount12"
|
||||||
|
val longName = "A".repeat(21)
|
||||||
|
val emptyPlaceholderValue = "New account"
|
||||||
|
val editedName = "Edited"
|
||||||
|
val context = device.context
|
||||||
|
val pasteButtonName = "Paste"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Set clipboard text '$longName'") {
|
||||||
|
setClipboardText(context,longName)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open 'Wallet settings' screen") { openWalletSettingsScreen() }
|
||||||
|
step("Click on 'Add account' button") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Edit account details' dialog screen appears") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Enter account name manually: '$accountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performClick()
|
||||||
|
accountNameField.performTextInput(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert name input is stable (keyboard doesn't flicker)") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button is enabled") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Clear the 'Edit name' field") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTextClearance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button becomes inactive when field is empty") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Paste name from clipboard: '$accountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTextReplacement(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert pasted text is displayed in 'Account name' field") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button is enabled") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Edit the entered name (clear and retype)") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTextReplacement(editedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert edited name in 'Account name' field is displayed") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(editedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Delete all text and leave 'Account name' field empty") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTextClearance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button is inactive") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Type name with more than 20 symbols") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTextReplacement(longName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert text over 20 symbols was not pasted and placeholder remains empty") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(emptyPlaceholderValue, substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button is inactive") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Clear text field") {
|
||||||
|
onAccountInfoEditorScreen { accountNameField.performTextClearance() }
|
||||||
|
}
|
||||||
|
step("Paste text longer than 20 characters to 'Account name' field") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performTouchInput { longClick(durationMillis = 2_000L) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on system 'Paste' button to paste clipboard text") {
|
||||||
|
clickOnSystemButton(pasteButtonName)
|
||||||
|
}
|
||||||
|
step("Assert text over 20 symbols was not pasted and placeholder remains empty") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(emptyPlaceholderValue, substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Add account' button is inactive") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.assertIsNotEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("5505")
|
||||||
|
@DisplayName(
|
||||||
|
"Accounts: check unsaved changes notification " +
|
||||||
|
"after attempt to close edited account creation form"
|
||||||
|
)
|
||||||
|
fun accountsCreationUnsavedChangesForNameFieldNotificationTest() {
|
||||||
|
val accountName = "Hikarik Test"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open 'Wallet settings' screen") { openWalletSettingsScreen() }
|
||||||
|
step("Click on 'Add account' button") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert edit account details dialog screen appears") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Enter account name manually: '$accountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performClick()
|
||||||
|
accountNameField.performTextInput(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Tap 'Cross' button to attempt closing the screen") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
crossButton.clickWithAssertion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Verify 'Unsaved changes' screen parts") {
|
||||||
|
checkUnsavedChangesCreationModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Tap 'Keep Editing' button to stay on screen") {
|
||||||
|
onDialog { keepEditButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert app still on 'Create account' screen") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert previously entered data is preserved") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.assertTextContains(accountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Tap 'Cross' button to attempt closing the screen") {
|
||||||
|
onAccountInfoEditorScreen { crossButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Unsaved changes' alert is displayed again") {
|
||||||
|
onDialog { dialogContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Tap 'Discard' button to discard and close") {
|
||||||
|
onDialog { discardButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Create account' screen is closed and 'Wallet settings' displayed again") {
|
||||||
|
onWalletSettingsScreen {
|
||||||
|
screenContainer.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Verify no new account has appeared in the list") {
|
||||||
|
onWalletSettingsScreen {
|
||||||
|
accountItem(accountName).assertDoesNotExist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("5502")
|
||||||
|
@DisplayName("Accounts: account creation, accounts mode and per-account token derivation")
|
||||||
|
fun accountCreationAndDerivationTest() {
|
||||||
|
val createdAccountName = "Account 2"
|
||||||
|
val accountReadyState = "AccountReadyToCreateDerivation"
|
||||||
|
val accountIndex = "1"
|
||||||
|
val btcTokenName = "Bitcoin"
|
||||||
|
val ethTokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeSection = {
|
||||||
|
setWireMockScenarioState(userTokensScenario, accountReadyState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(userTokensScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open wallet settings") { openWalletSettingsScreen() }
|
||||||
|
step("Start account creation") { startAccountCreation() }
|
||||||
|
|
||||||
|
step("Enter account name: '$createdAccountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performClick()
|
||||||
|
accountNameField.performTextInput(createdAccountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert account creation screen with derivation hint is displayed") {
|
||||||
|
onAccountInfoEditorScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Click 'Add account' and wait for 'Manage Tokens'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Close 'Manage Tokens' screen") {
|
||||||
|
onManageTokensScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Wallet settings' screen is displayed") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert new account '$createdAccountName' appears (last) in accounts list") {
|
||||||
|
onWalletSettingsScreen { accountItem(createdAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Navigate back to wallet details") {
|
||||||
|
onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Navigate back to main screen") {
|
||||||
|
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert accounts mode is on main: account '$createdAccountName' section is visible") {
|
||||||
|
onMainScreen { findAccountSectionByName(createdAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert per-account token derivation paths from the domain account model") {
|
||||||
|
val account = awaitCryptoPortfolioAccount(derivationIndex = accountIndex.toInt())
|
||||||
|
|
||||||
|
val btcPaths = account.derivationPathsForToken(btcTokenName)
|
||||||
|
assertTrue(
|
||||||
|
"Expected a $btcTokenName derivation with 3rd node = $accountIndex' (account index). Paths: $btcPaths",
|
||||||
|
btcPaths.any { DerivationPathHelper.nodeAt(it, index1Based = 3) == "$accountIndex'" },
|
||||||
|
)
|
||||||
|
|
||||||
|
val ethPaths = account.derivationPathsForToken(ethTokenName)
|
||||||
|
assertTrue(
|
||||||
|
"Expected an $ethTokenName derivation with 5th node = $accountIndex (account index). Paths: $ethPaths",
|
||||||
|
ethPaths.any { DerivationPathHelper.nodeAt(it, index1Based = 5) == accountIndex },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AllureId("8746")
|
||||||
|
@DisplayName("Accounts: empty account placeholder and 'Add tokens' entry to manage tokens")
|
||||||
|
fun emptyAccountPlaceholderTest() {
|
||||||
|
val createdAccountName = "Account 2"
|
||||||
|
val accountReadyState = "AccountReadyToCreateEmpty"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeSection = {
|
||||||
|
setWireMockScenarioState(userTokensScenario, accountReadyState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(userTokensScenario)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Open 'Main Screen'") { openMainScreen() }
|
||||||
|
step("Synchronize addresses") { synchronizeAddresses() }
|
||||||
|
step("Open wallet settings") { openWalletSettingsScreen() }
|
||||||
|
step("Start account creation") { startAccountCreation() }
|
||||||
|
|
||||||
|
step("Enter account name: '$createdAccountName'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
accountNameField.performClick()
|
||||||
|
accountNameField.performTextInput(createdAccountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Add account' and wait for 'Manage Tokens'") {
|
||||||
|
onAccountInfoEditorScreen {
|
||||||
|
saveAccountButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Close 'Manage Tokens' without adding any token") {
|
||||||
|
onManageTokensScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Wallet settings' screen is displayed") {
|
||||||
|
onWalletSettingsScreen { addAccountButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert new empty account '$createdAccountName' appears in accounts list") {
|
||||||
|
onWalletSettingsScreen { accountItem(createdAccountName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Navigate back to wallet details") {
|
||||||
|
onWalletSettingsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Navigate back to main screen") {
|
||||||
|
onDetailsScreen { topAppBarBackButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Expand empty account '$createdAccountName' section") {
|
||||||
|
onMainScreen {
|
||||||
|
scrollToAccountSection(createdAccountName)
|
||||||
|
findAccountSectionByName(createdAccountName).clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onMainScreen { emptyAccountTokensPlaceholder.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert empty tokens placeholder is displayed") {
|
||||||
|
onMainScreen { emptyAccountTokensPlaceholder.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Add tokens' button is displayed under the placeholder") {
|
||||||
|
onMainScreen { emptyAccountAddTokensButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Add tokens' button") {
|
||||||
|
onMainScreen {
|
||||||
|
emptyAccountAddTokensButton.clickAndWaitFor(
|
||||||
|
rule = composeTestRule,
|
||||||
|
expectedCondition = {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Manage Tokens' screen is opened for the account") {
|
||||||
|
onManageTokensScreen { topAppBarTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -383,11 +383,17 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen' on '$cardName' card") {
|
step("Open 'Main Screen' on '$cardName' card") {
|
||||||
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
openMainScreen(mockContent = cardType, isTwinsCard = true)
|
||||||
}
|
}
|
||||||
step("Assert 'Buy' button is displayed") {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
onMainScreen { buyButton.assertIsDisplayed() }
|
onMainScreen { addFundsButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Buy' button") {
|
step("Click on 'Add funds' button") {
|
||||||
onMainScreen { buyButton.performClick() }
|
onMainScreen { addFundsButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on '$tokenTitle'") {
|
||||||
|
onAddFundsBottomSheet { userTokenWithTitle(tokenTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Buy' button in bottom sheet") {
|
||||||
|
onGetTokenBottomSheet { buyButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Confirm' button in 'Dialog'") {
|
step("Click on 'Confirm' button in 'Dialog'") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
|
|
@ -426,10 +432,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.performClick() }
|
onMainScreen { addFundsButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Choose token' screen title is displayed") {
|
step("Assert 'Choose token' screen title is displayed") {
|
||||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert token with title: '$tokenTitle' is displayed") {
|
step("Assert token with title: '$tokenTitle' is displayed") {
|
||||||
onChooseTokenScreen { tokenWithTitle(tokenTitle).assertIsDisplayed() }
|
onChooseTokenBottomSheet { tokenWithTitle(tokenTitle).assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -449,14 +455,14 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
onMainScreen { transferButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Sell' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onMainScreen { sellButton.performClick() }
|
onMainScreen { transferButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' token screen title is displayed") {
|
step("Assert 'Choose token' title is displayed") {
|
||||||
onSellScreen { title.assertIsDisplayed() }
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -485,7 +491,7 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.performClick() }
|
onMainScreen { addFundsButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
||||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' to return to main screen") {
|
step("Press 'Back' to return to main screen") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -498,22 +504,21 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { swapButton.performClick() }
|
onMainScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check 'Action is unavailable' dialog") {
|
step("Check 'Action is unavailable' dialog") {
|
||||||
checkActionIsUnavailableDialog()
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
checkActionIsUnavailableDialog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Click on 'Ok' button") {
|
step("Click on 'Ok' button") {
|
||||||
onDialog { okButton.performClick() }
|
onDialog { okButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
onMainScreen { transferButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Sell' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onMainScreen { sellButton.performClick() }
|
onMainScreen { transferButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check 'Action is unavailable' dialog") {
|
step("Assert 'Choose token' title is displayed") {
|
||||||
checkActionIsUnavailableDialog()
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Click on 'Ok' button") {
|
|
||||||
onDialog { okButton.performClick() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +548,7 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { addFundsButton.performClick() }
|
onMainScreen { addFundsButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
step("Assert 'Choose token' screen opens (Add funds is always available)") {
|
||||||
onChooseTokenScreen { topAppBarTitle.assertIsDisplayed() }
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' to return to main screen") {
|
step("Press 'Back' to return to main screen") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -561,17 +566,14 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
||||||
step("Click on 'Ok' button") {
|
step("Click on 'Ok' button") {
|
||||||
onDialog { okButton.performClick() }
|
onDialog { okButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onMainScreen { sellButton.assertIsDisplayed() }
|
onMainScreen { transferButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Sell' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onMainScreen { sellButton.performClick() }
|
onMainScreen { transferButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check 'Action is unavailable' dialog") {
|
step("Assert 'Choose token' title is displayed") {
|
||||||
checkActionIsUnavailableDialog()
|
onChooseTokenBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Click on 'Ok' button") {
|
|
||||||
onDialog { okButton.performClick() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,21 @@ package com.tangem.tests.actionButtons
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.extensions.assertIsDimmed
|
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
|
||||||
import com.tangem.scenarios.goToQrCodeBottomSheet
|
import com.tangem.scenarios.goToQrCodeBottomSheet
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.openSendFromTokenDetails
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onAddFundsBottomSheet
|
||||||
import com.tangem.screens.onMainScreen
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onSendScreen
|
||||||
import com.tangem.screens.onSwapStoriesScreen
|
import com.tangem.screens.onSwapStoriesScreen
|
||||||
import com.tangem.screens.onSwapTokenScreen
|
import com.tangem.screens.onSwapTokenScreen
|
||||||
import com.tangem.screens.onTokenDetailsScreen
|
import com.tangem.screens.onTokenDetailsScreen
|
||||||
|
import com.tangem.screens.onTransferBottomSheet
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
|
@ -37,20 +42,41 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is displayed") {
|
step("Assert 'Add funds' button is displayed") {
|
||||||
onTokenDetailsScreen { receiveButton().assertIsDisplayed() }
|
onTokenDetailsScreen { addFundsButton.assertIsDisplayed() }
|
||||||
}
|
|
||||||
step("Assert 'Buy' button is displayed") {
|
|
||||||
onTokenDetailsScreen { buyButton().assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Send' button is displayed") {
|
|
||||||
onTokenDetailsScreen { sendButton().assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is displayed") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is displayed") {
|
step("Assert 'Transfer' button is displayed") {
|
||||||
onTokenDetailsScreen { sellButton().assertIsDisplayed() }
|
onTokenDetailsScreen { transferButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Add funds' button") {
|
||||||
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Buy' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { buyButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { swapButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Receive' button in bottom sheet is displayed") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { swapButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Sell' button in bottom sheet is displayed") {
|
||||||
|
onTransferBottomSheet { sellButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,20 +98,41 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is not dimmed") {
|
step("Assert 'Add funds' button is enabled") {
|
||||||
onTokenDetailsScreen { receiveButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { addFundsButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Buy' button is not dimmed") {
|
step("Assert 'Swap' button is disabled") {
|
||||||
onTokenDetailsScreen { buyButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not dimmed") {
|
step("Assert 'Transfer' button is enabled") {
|
||||||
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { transferButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Click on 'Add funds' button") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is dimmed") {
|
step("Assert 'Buy' button in bottom sheet is enabled") {
|
||||||
onTokenDetailsScreen { sellButton().assertIsDimmed() }
|
onAddFundsBottomSheet { buyButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||||
|
onAddFundsBottomSheet { swapButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Receive' button in bottom sheet is enabled") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button in bottom sheet is enabled") {
|
||||||
|
onTransferBottomSheet { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button in bottom sheet is disabled") {
|
||||||
|
onTransferBottomSheet { swapButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
step("Assert 'Sell' button in bottom sheet is disabled") {
|
||||||
|
onTransferBottomSheet { sellButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +156,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -140,8 +187,11 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Receive' button") {
|
step("Click on 'Add funds' button") {
|
||||||
onTokenDetailsScreen { receiveButton().performClick() }
|
onTokenDetailsScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Receive' button in bottom sheet") {
|
||||||
|
onAddFundsBottomSheet { receiveButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Go to QR code bottom sheet") {
|
step("Go to QR code bottom sheet") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
|
@ -153,4 +203,58 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("591")
|
||||||
|
@DisplayName("Action buttons (token details screen): send available for funded token, unavailable for empty token")
|
||||||
|
@Test
|
||||||
|
fun checkSendAvailabilityForFundedAndEmptyTokenTest() {
|
||||||
|
val emptyTokenTitle = "Polygon"
|
||||||
|
val fundedTokenTitle = "Ethereum"
|
||||||
|
val polygonBalanceScenarioName = "polygon_coin_balance"
|
||||||
|
val polygonBalanceScenarioState = "ZeroBalance"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(polygonBalanceScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$polygonBalanceScenarioName' to state: '$polygonBalanceScenarioState'") {
|
||||||
|
setWireMockScenarioState(polygonBalanceScenarioName, polygonBalanceScenarioState)
|
||||||
|
}
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$emptyTokenTitle'") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(emptyTokenTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Transfer' button is not displayed for the empty token") {
|
||||||
|
onTokenDetailsScreen { transferButton.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Go back to 'Main Screen'") {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
}
|
||||||
|
step("Assert 'Main Screen' is displayed") {
|
||||||
|
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$fundedTokenTitle'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(fundedTokenTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Transfer' button is displayed for the funded token") {
|
||||||
|
onTokenDetailsScreen { transferButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open the send flow from token details") {
|
||||||
|
openSendFromTokenDetails()
|
||||||
|
}
|
||||||
|
step("Assert 'Send' screen is displayed") {
|
||||||
|
onSendScreen { amountInputTextField.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.tangem.tests.addFunds
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.assertTextContainsSafe
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onAddTokenBottomSheet
|
||||||
|
import com.tangem.screens.onAddFundsBottomSheet
|
||||||
|
import com.tangem.screens.onBuyTokenDetailsScreen
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
|
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 BuyTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("587")
|
||||||
|
@DisplayName("Buy. Display tokens available for purchase")
|
||||||
|
@Test
|
||||||
|
fun buyDisplayTokensAvailableForPurchaseTest() {
|
||||||
|
val token = "Bitcoin"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on 'Add Funds' button") {
|
||||||
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on $token in Wallet list") {
|
||||||
|
onAddFundsBottomSheet { userTokenWithTitle(token).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Buy' button") {
|
||||||
|
onAddFundsBottomSheet { buyTokenButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Verify 'Buy $token' title is displayed") {
|
||||||
|
onBuyTokenDetailsScreen {
|
||||||
|
topBarTitle.assertTextContainsSafe("Buy $token", substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("590")
|
||||||
|
@DisplayName("Buy. Adding trending token to the main screen")
|
||||||
|
@Test
|
||||||
|
fun buyAddingTrendingTokenToMainScreenTest() {
|
||||||
|
val token = "Solana"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on 'Add Funds' button") {
|
||||||
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on $token in Trending list") {
|
||||||
|
onAddFundsBottomSheet { trendingTokenWithTitle(token).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Add' button") {
|
||||||
|
onAddTokenBottomSheet { addButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Close 'Get token' screen") {
|
||||||
|
onAddFundsBottomSheet { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Verify token $token exists on main screen") {
|
||||||
|
onMainScreen { assertTokenExists(token) }
|
||||||
|
}
|
||||||
|
step("Click on 'Add Funds' button") {
|
||||||
|
onMainScreen { addFundsButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Verify token $token in Wallet list") {
|
||||||
|
onAddFundsBottomSheet { userTokenWithTitle(token).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package com.tangem.tests.balance
|
|
||||||
|
|
||||||
import androidx.compose.ui.test.longClick
|
|
||||||
import com.tangem.common.BaseTestCase
|
|
||||||
import com.tangem.scenarios.openMainScreen
|
|
||||||
import com.tangem.scenarios.synchronizeAddresses
|
|
||||||
import com.tangem.screens.onMainScreen
|
|
||||||
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 TotalBalanceLongTapTest : BaseTestCase() {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@AllureId("3965")
|
|
||||||
@DisplayName("Total balance: check long tap on block without biometry")
|
|
||||||
fun whenBiometryIsOffTest() {
|
|
||||||
setupHooks().run {
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Long tap on total balance block") {
|
|
||||||
onMainScreen {
|
|
||||||
totalBalanceContainer.performTouchInput {
|
|
||||||
longClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Assert 'Rename' button is displayed") {
|
|
||||||
onMainScreen { totalBalanceMenuRenameWallet.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Delete' button is not displayed") {
|
|
||||||
onMainScreen { totalBalanceMenuDeleteWallet.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.balance
|
||||||
import androidx.compose.ui.test.longClick
|
import androidx.compose.ui.test.longClick
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
|
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.extensions.*
|
import com.tangem.common.extensions.*
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -73,7 +74,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
||||||
}
|
}
|
||||||
step("Open 'Markets screen'") {
|
step("Open 'Markets screen'") {
|
||||||
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
onMainScreen { marketsSheetDragHandle.clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Click on $tokenTitle token") {
|
step("Click on $tokenTitle token") {
|
||||||
|
|
@ -82,28 +83,25 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
}
|
}
|
||||||
step("Click on 'Add to portfolio' button") {
|
step("Click on 'Add' button in 'Markets' bottom sheet") {
|
||||||
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
|
onMarketsScreen { addButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on main network") {
|
step("Click on 'Add' button in 'Add token' bottom sheet") {
|
||||||
onMarketsScreen { mainNetworkSuffix.performClick() }
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
onAddTokenBottomSheet { addButton.performClick() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Click on 'Add' button") {
|
step("Press 'Back' button") {
|
||||||
onDialog { addButton.clickWithAssertion() }
|
|
||||||
}
|
|
||||||
step("Assert 'Continue' is not displayed") {
|
|
||||||
onDialog { addButton.assertIsNotDisplayed() }
|
|
||||||
}
|
|
||||||
step("Click on 'Later' button") {
|
|
||||||
onDialog { laterButton.clickWithAssertion() }
|
|
||||||
}
|
|
||||||
step("Go back to 'Markets: tokens list'") {
|
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsScreen { topBarBackButton.clickWithAssertion() }
|
device.uiDevice.pressBack()
|
||||||
}
|
}
|
||||||
step("Close 'Markets screen'") {
|
step("Press 'Back' button") {
|
||||||
onSearchBar { searchField.assertIsDisplayed() }
|
waitForIdle()
|
||||||
swipeMarketsBlock(SwipeDirection.DOWN)
|
device.uiDevice.pressBack()
|
||||||
|
}
|
||||||
|
step("Press 'Back' button") {
|
||||||
|
waitForIdle()
|
||||||
|
device.uiDevice.pressBack()
|
||||||
}
|
}
|
||||||
step("Assert $updatedBalance is displayed in total balance") {
|
step("Assert $updatedBalance is displayed in total balance") {
|
||||||
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
|
onMainScreen { totalBalanceText.assertTextContains(updatedBalance) }
|
||||||
|
|
@ -201,8 +199,11 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
step("Assert 'Token details screen' open") {
|
step("Assert 'Token details screen' open") {
|
||||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'More' button") {
|
step("Click on 'Back' button") {
|
||||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||||
|
onMainScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert $TOTAL_BALANCE is displayed in total balance") {
|
step("Assert $TOTAL_BALANCE is displayed in total balance") {
|
||||||
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
package com.tangem.tests.hotWallet
|
||||||
|
|
||||||
|
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.CREATE_USER_WALLET_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.PROVIDERS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.SEED_PHRASE_HAPPY_PATH
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WALLET_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.restartApp
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.openMainScreenWithExistingHotWallet
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import com.tangem.screens.accounts.onAccountDetailsScreen
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
import com.tangem.core.ui.R as CoreUiR
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class AssetsDiscoveryTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DISCOVERY_TIMEOUT_MILLIS = 120_000L
|
||||||
|
|
||||||
|
const val SCENARIO_STATE_STARTED = "Started"
|
||||||
|
const val SCENARIO_STATE_EMPTY = "Empty"
|
||||||
|
const val SCENARIO_STATE_ALREADY_EXISTS = "AlreadyExists"
|
||||||
|
const val SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT = "AssetsDiscoveryRedirect"
|
||||||
|
const val SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH = "AssetsDiscoveryHappyPath"
|
||||||
|
const val SCENARIO_STATE_NON_ZERO_EVM_BALANCES = "NonZeroEvmBalances"
|
||||||
|
const val SCENARIO_STATE_NON_ZERO_EVM_BALANCES_SLOW = "NonZeroEvmBalancesSlow"
|
||||||
|
|
||||||
|
val EXPECTED_DISCOVERED_TOKENS = listOf(
|
||||||
|
"Ethereum",
|
||||||
|
"Polygon",
|
||||||
|
"Tether",
|
||||||
|
)
|
||||||
|
|
||||||
|
val TOKENS_THAT_MUST_NOT_APPEAR = listOf(
|
||||||
|
"Solana",
|
||||||
|
"USDC",
|
||||||
|
)
|
||||||
|
|
||||||
|
val BACKEND_PRE_POPULATED_TOKENS = listOf(
|
||||||
|
"Bitcoin",
|
||||||
|
"Ethereum",
|
||||||
|
"Polygon",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9280")
|
||||||
|
@DisplayName("Hot wallet: new import — Discovery → Sync → Banner → Check here happy path")
|
||||||
|
@Test
|
||||||
|
fun newHotWalletImportHappyPathTest() {
|
||||||
|
val packageName = getTargetContext().packageName
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(PROVIDERS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT)
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH)
|
||||||
|
setWireMockScenarioState(WALLET_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO, state = SCENARIO_STATE_NON_ZERO_EVM_BALANCES)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(WALLET_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import a new hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_HAPPY_PATH)
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' progress loader is shown (discovery is in flight)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Wait for 'Wallet successfully imported' banner (discovery completes)") {
|
||||||
|
flakySafely(timeoutMs = DISCOVERY_TIMEOUT_MILLIS) {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert expected discovered tokens are visible in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
EXPECTED_DISCOVERED_TOKENS.forEach { token ->
|
||||||
|
tokenRowWithTitle(token).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Tap 'Check here' (Manage tokens) on the banner") {
|
||||||
|
onMainScreen { walletImportedBannerCheckHereButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Manage Tokens' screen is opened") {
|
||||||
|
onManageTokensScreen { searchField.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Return to main screen") {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Assert banner is hidden after navigating into Manage Tokens") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Force-close and re-launch the app") {
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
step("Assert banner is NOT shown again after relaunch") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert previously discovered tokens still appear in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
EXPECTED_DISCOVERED_TOKENS.forEach { token ->
|
||||||
|
tokenRowWithTitle(token).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert zero-balance and spam tokens are NOT shown in the assets list") {
|
||||||
|
onMainScreen {
|
||||||
|
TOKENS_THAT_MUST_NOT_APPEAR.forEach { token ->
|
||||||
|
assertTokenDoesNotExist(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9284")
|
||||||
|
@DisplayName("Hot wallet: token added manually during Discovery — no duplicate created")
|
||||||
|
@Test
|
||||||
|
fun manualTokenAddDuringDiscoveryNoDuplicateTest() {
|
||||||
|
val tetherTitle = "Tether"
|
||||||
|
val ethereumNetworkTitle = "ETHEREUM"
|
||||||
|
val accountName = getResourceString(CoreUiR.string.account_main_account_title)
|
||||||
|
val expectedTokensCount = 2
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(PROVIDERS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_REDIRECT)
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_ASSETS_DISCOVERY_HAPPY_PATH)
|
||||||
|
setWireMockScenarioState(WALLET_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(
|
||||||
|
MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO,
|
||||||
|
state = SCENARIO_STATE_NON_ZERO_EVM_BALANCES_SLOW,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(WALLET_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import a new hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_HAPPY_PATH)
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' progress loader is shown (discovery is in flight)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open wallet details from top bar") {
|
||||||
|
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Open 'Wallet settings'") {
|
||||||
|
onDetailsScreen { walletNameButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open account: '$accountName'") {
|
||||||
|
onWalletSettingsScreen { accountItem(accountName).performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Manage Tokens' from account details") {
|
||||||
|
onAccountDetailsScreen { manageTokensButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Search for '$tetherTitle' in Manage Tokens") {
|
||||||
|
onManageTokensScreen {
|
||||||
|
searchField.performClick()
|
||||||
|
searchField.performTextInput(tetherTitle)
|
||||||
|
}
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Expand '$tetherTitle'") {
|
||||||
|
onManageTokensScreen { tokenItem(tetherTitle).clickWithAssertion() }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Enable the $ethereumNetworkTitle network") {
|
||||||
|
onManageTokensScreen { networkSwitch(ethereumNetworkTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Save Manage Tokens changes") {
|
||||||
|
onManageTokensScreen { saveButton.clickWithAssertion() }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Navigate back to main screen") {
|
||||||
|
repeat(times = 3) {
|
||||||
|
device.uiDevice.pressBack()
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Wait for 'Wallet successfully imported' banner (discovery completes after delay)") {
|
||||||
|
flakySafely(timeoutMs = DISCOVERY_TIMEOUT_MILLIS) {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$tetherTitle' is in the assets list (manual add + discovery merged)") {
|
||||||
|
onMainScreen { tokenRowWithTitle(tetherTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert assets list contains exactly $expectedTokensCount tokens (no duplicate after merge)") {
|
||||||
|
onMainScreen { assertTokensCount(expectedTokensCount) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9282")
|
||||||
|
@DisplayName("Hot wallet: re-import existing wallet — 200 OK, no Discovery, tokens from backend")
|
||||||
|
@Test
|
||||||
|
fun reimportExistingHotWalletTest() {
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO, state = SCENARIO_STATE_ALREADY_EXISTS)
|
||||||
|
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, state = SCENARIO_STATE_STARTED)
|
||||||
|
setWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO, state = SCENARIO_STATE_EMPTY)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(CREATE_USER_WALLET_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(MORALIS_EVM_TOKEN_BALANCES_API_SCENARIO)
|
||||||
|
},
|
||||||
|
).run {
|
||||||
|
step("Import an existing hot wallet from seed phrase") {
|
||||||
|
openMainScreenWithExistingHotWallet(SEED_PHRASE_12)
|
||||||
|
}
|
||||||
|
step("Assert tokens from backend are displayed immediately") {
|
||||||
|
BACKEND_PRE_POPULATED_TOKENS.forEach { token ->
|
||||||
|
onMainScreen { tokenRowWithTitle(token).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Restoring' loader is NOT displayed (discovery did not start)") {
|
||||||
|
onMainScreen { restoringProgressText.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Wallet successfully imported' banner is NOT displayed") {
|
||||||
|
onMainScreen { walletImportedBanner.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,9 @@ package com.tangem.tests.main
|
||||||
|
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.SwipeDirection
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.openMainScreen
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
|
@ -37,7 +39,7 @@ class MainScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8748")
|
@AllureId("8748")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with single token no accounts")
|
@DisplayName("Main: check 'Add & Manage' button with single token no accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() {
|
fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() {
|
||||||
val scenarioState = "Cardano"
|
val scenarioState = "Cardano"
|
||||||
|
|
@ -58,14 +60,14 @@ class MainScreenTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Assert 'Add & Manage' button is not displayed") {
|
step("Assert 'Add & Manage' button is displayed") {
|
||||||
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
|
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8749")
|
@AllureId("8749")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with single token two accounts")
|
@DisplayName("Main: check 'Add & Manage' button with single token two accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() {
|
fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() {
|
||||||
val scenarioState = "TwoAccountsSingleTokenEach"
|
val scenarioState = "TwoAccountsSingleTokenEach"
|
||||||
|
|
@ -84,10 +86,10 @@ class MainScreenTest : BaseTestCase() {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
}
|
}
|
||||||
step("Assert 'Add & Manage' button is displayed") {
|
step("Assert 'Add & Manage' button is displayed") {
|
||||||
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click 'Add & Manage' button") {
|
step("Click 'Add & Manage' button") {
|
||||||
onMainScreen { addAndManageButtonNode.clickWithAssertion() }
|
onMainScreen { addAndManageButton().clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Organize tokens' option is not displayed (nothing to organize)") {
|
step("Assert 'Organize tokens' option is not displayed (nothing to organize)") {
|
||||||
onAddAndManageBottomSheet { organizeTokensButton.assertIsNotDisplayed() }
|
onAddAndManageBottomSheet { organizeTokensButton.assertIsNotDisplayed() }
|
||||||
|
|
@ -99,7 +101,7 @@ class MainScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllureId("8750")
|
@AllureId("8750")
|
||||||
@DisplayName("Main: check 'Organize tokens' button with multiple tokens two accounts")
|
@DisplayName("Main: check 'Add & Manage' button with multiple tokens two accounts")
|
||||||
@Test
|
@Test
|
||||||
fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() {
|
fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() {
|
||||||
val scenarioState = "TwoAccountsMixed"
|
val scenarioState = "TwoAccountsMixed"
|
||||||
|
|
@ -117,8 +119,11 @@ class MainScreenTest : BaseTestCase() {
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
}
|
}
|
||||||
|
step("Swipe up") {
|
||||||
|
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f, endHeightRatio = 0.1f)
|
||||||
|
}
|
||||||
step("Assert 'Add & Manage' button is displayed") {
|
step("Assert 'Add & Manage' button is displayed") {
|
||||||
onMainScreen { addAndManageButtonNode.assertIsDisplayed()}
|
onMainScreen { addAndManageButtonNode.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import org.junit.Test
|
||||||
class WarningsTest : BaseTestCase() {
|
class WarningsTest : BaseTestCase() {
|
||||||
|
|
||||||
@AllureId("184")
|
@AllureId("184")
|
||||||
@DisplayName("Token list: hide token by long tap")
|
@DisplayName("Warnings: missing address warning")
|
||||||
@Test
|
@Test
|
||||||
fun checkUnavailableNetworksWarningTest() {
|
fun checkUnavailableNetworksWarningTest() {
|
||||||
val scenarioState = "MissingDerivation"
|
val scenarioState = "MissingDerivation"
|
||||||
|
|
@ -38,9 +38,6 @@ class WarningsTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses(isBalanceAvailable = false)
|
synchronizeAddresses(isBalanceAvailable = false)
|
||||||
}
|
}
|
||||||
step("Assert 'Missing addresses' notification icon is displayed") {
|
|
||||||
onMainScreen { missingAddressNotificationIcon.assertIsDisplayed() }
|
|
||||||
}
|
|
||||||
step("Assert 'Missing addresses' notification title is displayed") {
|
step("Assert 'Missing addresses' notification title is displayed") {
|
||||||
onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() }
|
onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.tests.markets
|
package com.tangem.tests.markets
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.annotations.ApiEnv
|
import com.tangem.common.annotations.ApiEnv
|
||||||
import com.tangem.common.annotations.ApiEnvConfig
|
import com.tangem.common.annotations.ApiEnvConfig
|
||||||
|
|
@ -39,6 +40,7 @@ class MarketsExchangesTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
@Test
|
@Test
|
||||||
@AllureId("56")
|
@AllureId("56")
|
||||||
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
|
||||||
|
|
@ -53,16 +55,15 @@ class MarketsExchangesTest : BaseTestCase() {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' screen") {
|
step("Open 'Markets' screen") {
|
||||||
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
onMainScreen { marketsSheetDragHandle.clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Click on '$tokenName' token") {
|
step("Click on '$tokenName' token") {
|
||||||
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
}
|
}
|
||||||
step("Scroll down") {
|
step("Scroll to 'Listed on exchanges' block") {
|
||||||
swipeVertical(SwipeDirection.UP)
|
onMarketsScreen { scrollToListedOnBlock() }
|
||||||
swipeVertical(SwipeDirection.UP)
|
|
||||||
}
|
}
|
||||||
step("Assert 'Listed on exchanges' block has title") {
|
step("Assert 'Listed on exchanges' block has title") {
|
||||||
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
onMarketsScreen { listedOnBlockContainer.assertIsDisplayed() }
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ class RecentBlockTest : BaseTestCase() {
|
||||||
val sendAmount = "1"
|
val sendAmount = "1"
|
||||||
val txHistoryScenarioState = "11OutgoingTransactions"
|
val txHistoryScenarioState = "11OutgoingTransactions"
|
||||||
val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq"
|
val recipientAddressBase = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaq"
|
||||||
val shortenedRecipientAddress = "DJ2TaZ5vvp3mBLugU...Li4uYaq123456789b"
|
val longRecipientAddress = recipientAddressBase + "123456789b"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
|
|
@ -261,7 +261,7 @@ class RecentBlockTest : BaseTestCase() {
|
||||||
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = recentTransactionAmount1)
|
checkRecentAddressItem(address = DOGECOIN_ADDRESS, description = recentTransactionAmount1)
|
||||||
}
|
}
|
||||||
step("Check recent address item №2") {
|
step("Check recent address item №2") {
|
||||||
checkRecentAddressItem(address = shortenedRecipientAddress, description = recentTransactionAmount2)
|
checkRecentAddressItem(address = longRecipientAddress, description = recentTransactionAmount2)
|
||||||
}
|
}
|
||||||
step("Check recent address item №3") {
|
step("Check recent address item №3") {
|
||||||
checkRecentAddressItem(address = recipientAddressBase + "k", description = recentTransactionAmount2)
|
checkRecentAddressItem(address = recipientAddressBase + "k", description = recentTransactionAmount2)
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,11 @@ class SendAddressScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
|
@ -46,8 +47,11 @@ class SendConfirmScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
@ -123,8 +127,11 @@ class SendConfirmScreenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Send' button") {
|
step("Click on 'Transfer' button") {
|
||||||
onTokenDetailsScreen { sendButton().performClick() }
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Type '$inputAmount' in input text field") {
|
step("Type '$inputAmount' in input text field") {
|
||||||
onSendScreen {
|
onSendScreen {
|
||||||
|
|
@ -320,4 +327,42 @@ class SendConfirmScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("557")
|
||||||
|
@DisplayName("Send (Confirm screen): send a second transaction while the first is still pending")
|
||||||
|
@Test
|
||||||
|
fun sendSecondTransactionWhileFirstActiveTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
val inputAmount = "0.001"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Enter amount '$inputAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
// Hold-to-confirm is swallowed while the fee is still settling — wait for it to load first.
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSendSuccessScreen { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open the send flow again from token details") {
|
||||||
|
openSendFromTokenDetails()
|
||||||
|
}
|
||||||
|
step("Enter amount '$inputAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_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.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
|
@ -281,13 +282,13 @@ class SendFeeScreenTest : BaseTestCase() {
|
||||||
fun checkNetworkFeeBottomSheetForBitcoinTest() {
|
fun checkNetworkFeeBottomSheetForBitcoinTest() {
|
||||||
val tokenName = "Bitcoin"
|
val tokenName = "Bitcoin"
|
||||||
val tokenAmount = "0.00000001"
|
val tokenAmount = "0.00000001"
|
||||||
val feeAmount = "$2.86"
|
val feeAmount = "$0.48"
|
||||||
val fiatFeeAmount = "$0.24"
|
val fiatFeeAmount = "$0.24"
|
||||||
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
val feeUpTo = getResourceString(R.string.send_max_fee)
|
val feeUpTo = getResourceString(R.string.send_max_fee)
|
||||||
val feeUpToValue = "0.0000264 BTC"
|
val feeUpToValue = "0.0000044 BTC"
|
||||||
val newFeeUpToValue = "0.0000022 BTC"
|
val newFeeUpToValue = "0.0000022 BTC"
|
||||||
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
|
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
|
||||||
val satoshiValue = "2"
|
val satoshiValue = "2"
|
||||||
|
|
@ -443,4 +444,30 @@ class SendFeeScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("547")
|
||||||
|
@DisplayName("Send (Fee screen): network fee recalculates on speed switch and sends")
|
||||||
|
@Test
|
||||||
|
fun recalculateFeeOnSpeedSwitchAndSendTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
val inputAmount = "0.8"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Enter amount '$inputAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = inputAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
val marketFee = getNetworkFeeAmount()
|
||||||
|
step("Switch the network fee to 'Fast'") {
|
||||||
|
switchFeeToFastAndApply()
|
||||||
|
}
|
||||||
|
assertNetworkFeeChanged(marketFee)
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.tangem.tests.send.feeScreen
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completing a send paid with a fee in the token itself (no native fee coin), on a hot wallet:
|
||||||
|
* VeChain's VeThor and Terra Classic's TerraClassicUSD.
|
||||||
|
*/
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SendTokenFeeTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val tokenAmount = "1"
|
||||||
|
|
||||||
|
@AllureId("4907")
|
||||||
|
@DisplayName("Send (Fee in token): send VeThor and complete the transaction")
|
||||||
|
@Test
|
||||||
|
fun sendVeThorWithFeeInTokenTest() {
|
||||||
|
val tokenName = "VeThor"
|
||||||
|
val scenarioState = "Vechain"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
assertNetworkFeeContains("\$")
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4908")
|
||||||
|
@DisplayName("Send (Fee in token): send TerraClassicUSD and complete the transaction")
|
||||||
|
@Test
|
||||||
|
fun sendTerraClassicUsdWithFeeInTokenTest() {
|
||||||
|
val tokenName = "TerraClassicUSD"
|
||||||
|
val scenarioState = "Terra"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendScreenWithHotWallet(seedPhrase = SVS_SEED_PHRASE_12, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = TERRA_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
waitUntilNetworkFeeIsStable { readNetworkFeeAmount() }
|
||||||
|
assertNetworkFeeContains("\$")
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
package com.tangem.tests.send.gasless
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
|
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.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.enterAmountAndOpenSendConfirm
|
||||||
|
import com.tangem.scenarios.enterRecipientAndOpenSendConfirm
|
||||||
|
import com.tangem.scenarios.openSendScreen
|
||||||
|
import com.tangem.scenarios.selectStablecoinAsFeeToken
|
||||||
|
import com.tangem.screens.onSendConfirmScreen
|
||||||
|
import com.tangem.screens.onSendFeeSelectorBottomSheet
|
||||||
|
import com.tangem.screens.onSendScreen
|
||||||
|
import com.tangem.screens.onSendSelectNetworkFeeBottomSheet
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gasless network-fee behaviour on the send summary (fee selector): availability, calculation,
|
||||||
|
* speed options, switching the fee token, and balance-driven notifications. All run on the default
|
||||||
|
* (cold) wallet without signing a transaction.
|
||||||
|
*/
|
||||||
|
@HiltAndroidTest
|
||||||
|
class GaslessFeeTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val scenarioState = "PolygonUSDC"
|
||||||
|
private val tokenName = "USDC"
|
||||||
|
private val nativeTokenName = "Polygon"
|
||||||
|
private val tokenAmount = "1"
|
||||||
|
|
||||||
|
@AllureId("5061")
|
||||||
|
@DisplayName("Gasless: Network fee on summary is selectable and the stablecoin is available for the fee")
|
||||||
|
@Test
|
||||||
|
fun checkNetworkFeeTokenSelectionAvailableTest() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' block with token selection is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeSelectorTitle.assertIsDisplayed()
|
||||||
|
selectFeeIcon.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Network fee' block") {
|
||||||
|
onSendConfirmScreen { feeSelectorBlock.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' bottom sheet is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$tokenName' is available for the fee payment") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5062")
|
||||||
|
@DisplayName("Gasless: network fee for a stablecoin is calculated and shown in the stablecoin")
|
||||||
|
@Test
|
||||||
|
fun checkFeeCalculatedInStablecoinTest() {
|
||||||
|
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert the fee is shown under the '$marketSpeed' speed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert the network fee is calculated in '$tokenName' (not in the coin) on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeBlockCurrency(tokenName).assertIsDisplayed()
|
||||||
|
feeAmount.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5064")
|
||||||
|
@DisplayName("Gasless: only Market speed is available when paying the fee with a stablecoin")
|
||||||
|
@Test
|
||||||
|
fun checkOnlyMarketSpeedAvailableForStablecoinFeeTest() {
|
||||||
|
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSpeed = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSpeed = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Click on 'Network fee' block") {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeSelectorBlock.assertIsDisplayed()
|
||||||
|
feeSelectorBlock.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' bottom sheet is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' bottom sheet is displayed after token selection") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { networkFeeTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$marketSpeed' speed is displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$fastSpeed' speed is not displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(fastSpeed).assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$slowSpeed' speed is not displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(slowSpeed).assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on '$marketSpeed' fee row") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose speed' bottom sheet did not open for stablecoin fee") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5068")
|
||||||
|
@DisplayName("Gasless: switching the fee token back to the coin restores the standard fee flow")
|
||||||
|
@Test
|
||||||
|
fun checkSwitchFeeTokenBackToCoinTest() {
|
||||||
|
val nativeSymbol = "POL"
|
||||||
|
val feeCoverageTitle = getResourceString(R.string.send_network_fee_warning_title)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Open the fee token selector again via the '$tokenName' fee token") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Switch the fee token back to '$nativeTokenName'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert the network fee is now paid in '$nativeSymbol' on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { feeBlockCurrency(nativeSymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee coverage' notification is not displayed (standard fee flow)") {
|
||||||
|
onSendConfirmScreen { warningTitle(feeCoverageTitle).assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button is enabled") {
|
||||||
|
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5063")
|
||||||
|
@DisplayName("Gasless: insufficient stablecoin balance to cover the fee shows error and blocks send")
|
||||||
|
@Test
|
||||||
|
fun checkInsufficientBalanceForFeeTest() {
|
||||||
|
val usdcBalanceScenario = "polygon_usdc_balance"
|
||||||
|
val lowBalanceState = "LowBalance"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(usdcBalanceScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
|
||||||
|
}
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Click on 'Max' button") {
|
||||||
|
onSendScreen { maxButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Enter the recipient and open the 'Send confirm' screen") {
|
||||||
|
enterRecipientAndOpenSendConfirm(ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5097")
|
||||||
|
@DisplayName("Gasless: no insufficient-coin-for-fee notification is shown when gasless covers the fee")
|
||||||
|
@Test
|
||||||
|
fun checkNoInsufficientCoinNotificationWhenGaslessTest() {
|
||||||
|
val coinBalanceScenario = "polygon_coin_balance"
|
||||||
|
val zeroBalanceState = "ZeroBalance"
|
||||||
|
val feeBlockedTitlePart = getResourceString(R.string.warning_send_blocked_funds_for_fee_title, "X")
|
||||||
|
.substringAfter("X ")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(coinBalanceScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$coinBalanceScenario' to '$zeroBalanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = coinBalanceScenario, state = zeroBalanceState)
|
||||||
|
}
|
||||||
|
step("Open 'Send' screen for '$tokenName'") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = scenarioState)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Assert the fee defaults to '$tokenName' (gasless covers the missing coin)") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { feeBlockCurrency(tokenName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert the insufficient-coin-for-fee notification is not shown") {
|
||||||
|
onSendConfirmScreen { warningTitleContaining(feeBlockedTitlePart).assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button is enabled") {
|
||||||
|
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
package com.tangem.tests.send.gasless
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gasless send lifecycle: signing and broadcasting a stablecoin-fee transaction (hot wallet),
|
||||||
|
* the max-amount fee reservation, and the completed gasless transaction in the token history.
|
||||||
|
*/
|
||||||
|
@HiltAndroidTest
|
||||||
|
class GaslessSendTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val scenarioState = "PolygonUSDC"
|
||||||
|
private val tokenName = "USDC"
|
||||||
|
private val currencySymbol = "USDC"
|
||||||
|
private val nativeTokenName = "Polygon"
|
||||||
|
private val hotWalletTokensState = "PolygonUSDCHotWallet"
|
||||||
|
private val tokenAmount = "1"
|
||||||
|
|
||||||
|
@AllureId("5069")
|
||||||
|
@DisplayName("Gasless: max amount reserves the stablecoin fee and stays sendable")
|
||||||
|
@Test
|
||||||
|
fun checkMaxAmountSendTest() {
|
||||||
|
val feeCoverageTitle = getResourceString(R.string.send_network_fee_warning_title)
|
||||||
|
val feeCoverageMessagePart = getResourceString(R.string.common_network_fee_warning_content, "", "")
|
||||||
|
.substringBefore("(")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openGaslessSendScreenWithHotWallet(
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
tokenName = tokenName,
|
||||||
|
userTokensState = hotWalletTokensState,
|
||||||
|
quotesState = scenarioState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on 'Max' button") {
|
||||||
|
onSendScreen { maxButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Enter the recipient and open the 'Send confirm' screen") {
|
||||||
|
enterRecipientAndOpenSendConfirm(ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee coverage' notification title is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { warningTitle(feeCoverageTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee coverage' notification text is displayed (amount reduced by fee)") {
|
||||||
|
onSendConfirmScreen { warningMessageContaining(feeCoverageMessagePart).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Send' button is enabled (enough left for the fee)") {
|
||||||
|
onSendConfirmScreen { sendButton.assertIsEnabled() }
|
||||||
|
}
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5065")
|
||||||
|
@DisplayName("Gasless: sign and send a stablecoin transaction with the stablecoin fee")
|
||||||
|
@Test
|
||||||
|
fun checkSignAndSendGaslessTransactionTest() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open the send flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openGaslessSendScreenWithHotWallet(
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
tokenName = tokenName,
|
||||||
|
userTokensState = hotWalletTokensState,
|
||||||
|
quotesState = scenarioState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = ETHEREUM_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert gasless fee is paid in '$currencySymbol' and 'Send' is enabled") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeBlockCurrency(currencySymbol).assertIsDisplayed()
|
||||||
|
sendButton.assertIsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5066")
|
||||||
|
@DisplayName("Gasless: completed gasless transaction is shown in token transaction history")
|
||||||
|
@Test
|
||||||
|
fun checkGaslessTransactionInHistoryTest() {
|
||||||
|
val sentAmount = "1.00"
|
||||||
|
val gaslessFeeAmount = "0.10"
|
||||||
|
val sentTitle = getResourceString(R.string.common_sent)
|
||||||
|
val gaslessFeeTitle = getResourceString(R.string.gasless_transaction_fee)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$scenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = scenarioState)
|
||||||
|
}
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Token details' screen is displayed") {
|
||||||
|
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Wait for gasless '$gaslessFeeTitle' transaction in history") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTxHistoryScreen { transactionItem(gaslessFeeTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$sentTitle' transaction is displayed") {
|
||||||
|
onTxHistoryScreen { transactionItem(sentTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$sentTitle' amount '$sentAmount' is displayed in '$currencySymbol'") {
|
||||||
|
onTxHistoryScreen {
|
||||||
|
transactionAmount(sentTitle).assertTextContains(sentAmount, substring = true)
|
||||||
|
transactionCurrency(sentTitle).assertTextEquals(currencySymbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert gasless '$gaslessFeeTitle' amount '$gaslessFeeAmount' is displayed in '$currencySymbol'") {
|
||||||
|
onTxHistoryScreen {
|
||||||
|
transactionAmount(gaslessFeeTitle).assertTextContains(gaslessFeeAmount, substring = true)
|
||||||
|
transactionCurrency(gaslessFeeTitle).assertTextEquals(currencySymbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert gasless '$gaslessFeeTitle' status is confirmed") {
|
||||||
|
onTxHistoryScreen { transactionConfirmedStatus(gaslessFeeTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
package com.tangem.tests.send.sendViaSwap
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.R
|
||||||
|
import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.PROVIDERS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gasless send-via-swap: paying the network fee with the stablecoin while converting it through an
|
||||||
|
* express swap. Covers the fee-token selection on the swap summary, the stablecoin balance validation
|
||||||
|
* against the gasless fee, and the full signed swap-and-send on a hot wallet.
|
||||||
|
*/
|
||||||
|
@HiltAndroidTest
|
||||||
|
class GaslessSendViaSwapTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val tokenName = "USDC"
|
||||||
|
private val currencySymbol = "USDC"
|
||||||
|
private val nativeTokenName = "Polygon"
|
||||||
|
private val swapTokenName = "Bitcoin"
|
||||||
|
private val mainNetwork = "MAIN"
|
||||||
|
private val providerName = "Changelly"
|
||||||
|
private val tokenAmount = "1"
|
||||||
|
private val hotWalletTokensState = "PolygonUSDCHotWallet"
|
||||||
|
private val quotesState = "PolygonUSDC"
|
||||||
|
private val assetsScenarioName = "express_api_assets"
|
||||||
|
private val assetsExchangeEnabledState = "BitcoinExchangeEnabled"
|
||||||
|
private val providersState = "HotWalletSvS"
|
||||||
|
|
||||||
|
@AllureId("5120")
|
||||||
|
@DisplayName("Gasless Send via Swap: the network fee is selectable and payable with the stablecoin")
|
||||||
|
@Test
|
||||||
|
fun checkFeeTokenSelectionForSwapTest() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendViaSwapScreenWithHotWallet(
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
tokenName = tokenName,
|
||||||
|
swapTokenName = swapTokenName,
|
||||||
|
networkName = swapTokenName,
|
||||||
|
networkType = mainNetwork,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' block with token selection is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen {
|
||||||
|
feeSelectorTitle.assertIsDisplayed()
|
||||||
|
selectFeeIcon.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Network fee' block") {
|
||||||
|
onSendConfirmScreen { feeSelectorBlock.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' bottom sheet is displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$tokenName' is available for the fee payment") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert the network fee is calculated in '$currencySymbol' on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5121")
|
||||||
|
@DisplayName("Gasless Send via Swap: insufficient stablecoin balance to cover the fee blocks the swap")
|
||||||
|
@Test
|
||||||
|
fun checkBalanceValidationForFeeTest() {
|
||||||
|
val usdcBalanceScenario = "polygon_usdc_balance"
|
||||||
|
val lowBalanceState = "LowBalance"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(usdcBalanceScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendViaSwapScreenWithHotWallet(
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
tokenName = tokenName,
|
||||||
|
swapTokenName = swapTokenName,
|
||||||
|
networkName = swapTokenName,
|
||||||
|
networkType = mainNetwork,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5122")
|
||||||
|
@DisplayName("Gasless Send via Swap: sign and send a swap paying the fee with the stablecoin")
|
||||||
|
@Test
|
||||||
|
fun checkSendViaSwapFinalScreenAndSendTest() {
|
||||||
|
val exchangeStatusScenario = "exchange_status_provider"
|
||||||
|
val changellyStatusState = "Changelly"
|
||||||
|
val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(exchangeStatusScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$exchangeStatusScenario' to '$changellyStatusState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = exchangeStatusScenario, state = changellyStatusState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
|
||||||
|
openSendViaSwapScreenWithHotWallet(
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
tokenName = tokenName,
|
||||||
|
swapTokenName = swapTokenName,
|
||||||
|
networkName = swapTokenName,
|
||||||
|
networkType = mainNetwork,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
|
||||||
|
enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with '$tokenName' via the fee selector") {
|
||||||
|
selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert the sent '$tokenName' amount is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { primaryAmount.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert the recipient address is displayed") {
|
||||||
|
onSendConfirmScreen { recipientAddress(BITCOIN_RECIPIENT_ADDRESS).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert the amount to receive after the swap is displayed") {
|
||||||
|
onSendConfirmScreen { secondaryAmount.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert the network fee is paid in '$currencySymbol'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Sign, send and open the 'Transaction sent' screen") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendSuccessScreenViaLongClickOnSendButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Check 'Send via swap' success screen") {
|
||||||
|
checkSendViaSwapSuccessScreen()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSendSuccessScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Express status' item with title '$expressStatusItemTitle' is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.XRP_ACTIVATED_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.extractText
|
import com.tangem.common.extensions.extractText
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
|
@ -665,4 +666,163 @@ class SendViaSwapTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("9924")
|
||||||
|
@DisplayName("Send via Swap: editing the memo persists the latest value")
|
||||||
|
@Test
|
||||||
|
fun sendViaSwapMemoEditPersistsLatestValueTest() {
|
||||||
|
val tokenName = "Bitcoin"
|
||||||
|
val swapTokenName = "XRP"
|
||||||
|
val networkName = "XRP Ledger"
|
||||||
|
val inputAmount = "0.001"
|
||||||
|
val recipientAddress = XRP_ACTIVATED_RECIPIENT_ADDRESS
|
||||||
|
val memoFirst = "11111111"
|
||||||
|
val memoSecond = "22222222"
|
||||||
|
val hotWalletScenarioState = "HotWalletSvS"
|
||||||
|
val quotesScenarioState = "Ripple"
|
||||||
|
val bitcoinBalanceScenarioName = "bitcoin_utxo"
|
||||||
|
val bitcoinBalanceScenarioState = "BalanceHotWalletSvS"
|
||||||
|
val assetsScenarioName = "express_api_assets"
|
||||||
|
val assetsScenarioState = "BitcoinExchangeEnabledWithXRP"
|
||||||
|
val coinsScenarioName = "coins_api"
|
||||||
|
val coinsScenarioState = "WithXRP"
|
||||||
|
val providersScenarioName = "networks_providers"
|
||||||
|
val providersScenarioState = "HotWalletSvS"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(bitcoinBalanceScenarioName)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(coinsScenarioName)
|
||||||
|
resetWireMockScenarioState(providersScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
|
||||||
|
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$hotWalletScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$quotesScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$bitcoinBalanceScenarioName' to state: '$bitcoinBalanceScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = bitcoinBalanceScenarioName, state = bitcoinBalanceScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$assetsScenarioName' to state: '$assetsScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$coinsScenarioName' to state: '$coinsScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = coinsScenarioName, state = coinsScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$providersScenarioName' to state: '$providersScenarioState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = providersScenarioName, state = providersScenarioState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open 'Main Screen' with existing hot wallet") {
|
||||||
|
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Transfer' button") {
|
||||||
|
onTokenDetailsScreen { transferButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Send' button in bottom sheet") {
|
||||||
|
onTransferBottomSheet { sendButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on 'Swap to another token' button") {
|
||||||
|
onSendScreen { swapToAnotherTokenButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Type '$swapTokenName' in search text field") {
|
||||||
|
onSendViaSwapScreen {
|
||||||
|
searchField.performClick()
|
||||||
|
searchField.performTextInput(swapTokenName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on token: '$swapTokenName'") {
|
||||||
|
onSendViaSwapScreen { tokenItem(swapTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on '$networkName' network") {
|
||||||
|
onChooseNetworkBottomSheet { networkItem(networkName).performClick() }
|
||||||
|
}
|
||||||
|
step("Type '$inputAmount' in text field") {
|
||||||
|
onSendScreen { amountInputTextField.performTextInput(inputAmount) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendScreen {
|
||||||
|
nextButton.assertIsEnabled()
|
||||||
|
nextButton.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
|
||||||
|
}
|
||||||
|
step("Type '$memoFirst' in 'Destination Tag' field") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendAddressScreen { destinationTagTextField.performTextReplacement(memoFirst) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Destination Tag' field value is '$memoFirst'") {
|
||||||
|
onSendAddressScreen { destinationTagTextField.assertTextContains(memoFirst) }
|
||||||
|
}
|
||||||
|
step("Open 'Send confirm' screen via 'Next' button") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaNextButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert recipient memo on 'Send confirm' screen contains '$memoFirst'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { recipientMemo.assertTextContains(memoFirst, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit-mode re-entry from Confirm uses the 'Continue' button, not 'Next'.
|
||||||
|
step("Click on recipient block to edit memo") {
|
||||||
|
onSendConfirmScreen { recipientBlock.performClick() }
|
||||||
|
}
|
||||||
|
step("Replace 'Destination Tag' field value with '$memoSecond'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendAddressScreen { destinationTagTextField.performTextReplacement(memoSecond) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Destination Tag' field value is '$memoSecond'") {
|
||||||
|
onSendAddressScreen { destinationTagTextField.assertTextContains(memoSecond) }
|
||||||
|
}
|
||||||
|
step("Return to 'Send confirm' screen via 'Continue' button") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaContinueButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert recipient memo on 'Send confirm' screen contains '$memoSecond'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { recipientMemo.assertTextContains(memoSecond, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Click on recipient block to edit memo") {
|
||||||
|
onSendConfirmScreen { recipientBlock.performClick() }
|
||||||
|
}
|
||||||
|
step("Replace 'Destination Tag' field value back with '$memoFirst'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendAddressScreen { destinationTagTextField.performTextReplacement(memoFirst) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Destination Tag' field value is '$memoFirst'") {
|
||||||
|
onSendAddressScreen { destinationTagTextField.assertTextContains(memoFirst) }
|
||||||
|
}
|
||||||
|
step("Return to 'Send confirm' screen via 'Continue' button") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaContinueButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert recipient memo on 'Send confirm' screen contains '$memoFirst'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendConfirmScreen { recipientMemo.assertTextContains(memoFirst, substring = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,11 @@ import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.DOGECOIN_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.DOGECOIN_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
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.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.scenarios.checkSendWarning
|
import com.tangem.scenarios.checkSendWarning
|
||||||
|
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||||
import com.tangem.scenarios.openSendScreen
|
import com.tangem.scenarios.openSendScreen
|
||||||
import com.tangem.screens.onSendAddressScreen
|
import com.tangem.screens.onSendAddressScreen
|
||||||
import com.tangem.screens.onSendScreen
|
import com.tangem.screens.onSendScreen
|
||||||
|
|
@ -20,8 +22,8 @@ import org.junit.Test
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class DogecoinWarningsTest : BaseTestCase() {
|
class DogecoinWarningsTest : BaseTestCase() {
|
||||||
private val tokenName = "Dogecoin"
|
private val tokenName = "Dogecoin"
|
||||||
private val amountToLeaveLessThanDust = "5.78654978"
|
private val amountToLeaveLessThanDust = "5.7045"
|
||||||
private val amountToLeaveMoreThanDust = "5.7"
|
private val amountToLeaveMoreThanDust = "5.6"
|
||||||
private val amountGreaterThanDust = "0.02"
|
private val amountGreaterThanDust = "0.02"
|
||||||
private val amountLessThanDust = "0.005"
|
private val amountLessThanDust = "0.005"
|
||||||
private val dustAmount = "DOGE 0.01"
|
private val dustAmount = "DOGE 0.01"
|
||||||
|
|
@ -56,8 +58,10 @@ class DogecoinWarningsTest : BaseTestCase() {
|
||||||
step("Type address in input text field") {
|
step("Type address in input text field") {
|
||||||
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
|
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
|
||||||
}
|
}
|
||||||
step("Click on 'Next' button") {
|
step("Open 'Send confirm' screen via 'Next' button") {
|
||||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaNextButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'Invalid amount warning' is displayed") {
|
step("Assert 'Invalid amount warning' is displayed") {
|
||||||
checkSendWarning(
|
checkSendWarning(
|
||||||
|
|
@ -93,8 +97,10 @@ class DogecoinWarningsTest : BaseTestCase() {
|
||||||
step("Type address in input text field") {
|
step("Type address in input text field") {
|
||||||
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
|
onSendAddressScreen { addressTextField.performTextReplacement(DOGECOIN_RECIPIENT_ADDRESS) }
|
||||||
}
|
}
|
||||||
step("Click on 'Next' button") {
|
step("Open 'Send confirm' screen via 'Next' button") {
|
||||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaNextButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'Invalid amount warning' is not displayed") {
|
step("Assert 'Invalid amount warning' is not displayed") {
|
||||||
checkSendWarning(
|
checkSendWarning(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
package com.tangem.tests.send.warnings
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.KASPA_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.checkSendWarning
|
||||||
|
import com.tangem.scenarios.openSendScreen
|
||||||
|
import com.tangem.screens.onSendAddressScreen
|
||||||
|
import com.tangem.screens.onSendScreen
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class KaspaDustWarningsTest : BaseTestCase() {
|
||||||
|
private val tokenName = "Kaspa"
|
||||||
|
private val amountLessThanMinimum = "0.1"
|
||||||
|
private val amountExactlyMinimum = "0.2"
|
||||||
|
private val amountMoreThanMinimum = "0.3"
|
||||||
|
private val amountToLeaveMoreThanMinimumChange = "0.5"
|
||||||
|
private val amountToLeaveExactlyMinimumChange = "0.79"
|
||||||
|
private val amountToLeaveLessThanMinimumChange = "0.85"
|
||||||
|
|
||||||
|
private val kaspaUTXOScenarioName = "kaspa_utxo"
|
||||||
|
private val dustState = "dust"
|
||||||
|
|
||||||
|
private val dustAmount = "KAS 0.20"
|
||||||
|
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)
|
||||||
|
private val invalidAmountMessage = getResourceString(
|
||||||
|
R.string.send_notification_invalid_minimum_amount_text,
|
||||||
|
dustAmount, dustAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
@AllureId("4685")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is displayed, when sending less than minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsDisplayedWhenSendingLessThanMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountLessThanMinimum' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountLessThanMinimum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9860")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is NOT displayed, when sending exactly minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsNotDisplayedWhenSendingExactlyMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountExactlyMinimum' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountExactlyMinimum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is not displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage,
|
||||||
|
isDisplayed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4683")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is NOT displayed, when sending more than minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsNotDisplayedWhenSendingMoreThanMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountMoreThanMinimum' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountMoreThanMinimum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is not displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage,
|
||||||
|
isDisplayed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4684")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is NOT displayed, when change is more than minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsNotDisplayedWhenChangeIsMoreThanMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountToLeaveMoreThanMinimumChange' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountToLeaveMoreThanMinimumChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is not displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage,
|
||||||
|
isDisplayed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4682")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is displayed, when change is less than minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsDisplayedWhenChangeIsLessThanMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountToLeaveLessThanMinimumChange' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountToLeaveLessThanMinimumChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("9861")
|
||||||
|
@DisplayName("Warnings: invalid amount warning is NOT displayed, when change is exactly minimum amount (Kaspa)")
|
||||||
|
@Test
|
||||||
|
fun warningIsNotDisplayedWhenChangeIsExactlyMinimum() {
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(kaspaUTXOScenarioName)
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$kaspaUTXOScenarioName' to state: '$dustState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = kaspaUTXOScenarioName, state = dustState)
|
||||||
|
}
|
||||||
|
step("Open 'Send Screen' with token: $tokenName") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$amountToLeaveExactlyMinimumChange' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(amountToLeaveExactlyMinimumChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type address in input text field") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Invalid amount warning' is not displayed") {
|
||||||
|
checkSendWarning(
|
||||||
|
title = invalidAmountTitle,
|
||||||
|
message = invalidAmountMessage,
|
||||||
|
isDisplayed = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,12 @@ import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.KASPA_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.KASPA_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
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.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.checkSendWarning
|
import com.tangem.scenarios.checkSendWarning
|
||||||
|
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||||
import com.tangem.scenarios.openSendScreen
|
import com.tangem.scenarios.openSendScreen
|
||||||
import com.tangem.screens.onSendAddressScreen
|
import com.tangem.screens.onSendAddressScreen
|
||||||
import com.tangem.screens.onSendScreen
|
import com.tangem.screens.onSendScreen
|
||||||
|
|
@ -145,8 +147,10 @@ class KaspaWarningsTest : BaseTestCase() {
|
||||||
step("Type address in input text field") {
|
step("Type address in input text field") {
|
||||||
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
onSendAddressScreen { addressTextField.performTextReplacement(KASPA_RECIPIENT_ADDRESS) }
|
||||||
}
|
}
|
||||||
step("Click on 'Next' button") {
|
step("Click 'Next' button until 'Send Confirm' screen opens") {
|
||||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
runCatching { openSendConfirmScreenViaNextButton() }.isSuccess
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'UTXO limit warning' is displayed") {
|
step("Assert 'UTXO limit warning' is displayed") {
|
||||||
checkSendWarning(
|
checkSendWarning(
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package com.tangem.tests.send.warnings
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
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.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_ACTIVATED_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.constants.TestConstants.XLM_NON_ACTIVATED_RECIPIENT_ADDRESS
|
import com.tangem.common.constants.TestConstants.XLM_NON_ACTIVATED_RECIPIENT_ADDRESS
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.scenarios.checkSendWarning
|
import com.tangem.scenarios.checkSendWarning
|
||||||
|
import com.tangem.scenarios.openSendConfirmScreenViaNextButton
|
||||||
import com.tangem.scenarios.openSendScreen
|
import com.tangem.scenarios.openSendScreen
|
||||||
import com.tangem.screens.onSendAddressScreen
|
import com.tangem.screens.onSendAddressScreen
|
||||||
import com.tangem.screens.onSendConfirmScreen
|
import com.tangem.screens.onSendConfirmScreen
|
||||||
|
|
@ -110,8 +112,10 @@ class StellarWarningsTest : BaseTestCase() {
|
||||||
step("Type non activated address in input text field") {
|
step("Type non activated address in input text field") {
|
||||||
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
|
onSendAddressScreen { addressTextField.performTextReplacement(XLM_NON_ACTIVATED_RECIPIENT_ADDRESS) }
|
||||||
}
|
}
|
||||||
step("Click on 'Next' button") {
|
step("Open 'Send confirm' screen via 'Next' button") {
|
||||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
openSendConfirmScreenViaNextButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'Invalid reserve amount warning' is not displayed") {
|
step("Assert 'Invalid reserve amount warning' is not displayed") {
|
||||||
checkSendWarning(
|
checkSendWarning(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,542 @@
|
||||||
|
package com.tangem.tests.swap
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.R as CommonR
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
|
import com.tangem.common.extensions.extractText
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gasless swap on a CEX route, paying the swap network fee with a stablecoin. Covers the best-rate
|
||||||
|
* block / unchanged rate (5117), the fee-token selector bottom sheet (5111), the network-fee
|
||||||
|
* selection for a USDC -> POL swap (5110), the signed swap reaching the provider (5116, hot wallet),
|
||||||
|
* the insufficient-stablecoin-balance-for-fee error (5118), the stablecoin fee shown with its
|
||||||
|
* fiat equivalent (5112), switching the fee token between the coin and the stablecoin (5114), and the
|
||||||
|
* fee-selection options when paying with a token (5115), and the max amount reserving the fee (5113).
|
||||||
|
* Validation cases run on the default (cold) wallet without signing.
|
||||||
|
*/
|
||||||
|
@HiltAndroidTest
|
||||||
|
class GaslessSwapTest : BaseTestCase() {
|
||||||
|
|
||||||
|
private val tokenName = "USDC"
|
||||||
|
private val currencySymbol = "USDC"
|
||||||
|
private val swapTokenName = "Ethereum"
|
||||||
|
private val nativeTokenName = "Polygon"
|
||||||
|
private val inputAmount = "50"
|
||||||
|
private val userTokensState = "PolygonUSDCEthereum"
|
||||||
|
private val quotesState = "PolygonUSDC"
|
||||||
|
private val assetsScenarioName = "express_api_assets"
|
||||||
|
private val assetsExchangeEnabledState = "BitcoinExchangeEnabled"
|
||||||
|
|
||||||
|
@AllureId("5117")
|
||||||
|
@DisplayName("Gasless Swap: CEX best rate stays the same when the fee is paid with a stablecoin")
|
||||||
|
@Test
|
||||||
|
fun checkBestRateUnchangedWithStablecoinFeeTest() {
|
||||||
|
var capturedReceiveAmount: String? = null
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Assert 'Best rate' label is displayed") {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { bestRateText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Capture the received amount (exchange rate) before changing the fee token") {
|
||||||
|
onSwapTokenScreen { capturedReceiveAmount = receiveAmount.extractText() }
|
||||||
|
}
|
||||||
|
step("Pay the network fee with the stablecoin '$tokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = nativeTokenName, newFeeToken = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert the received amount (rate) is unchanged after selecting the stablecoin fee") {
|
||||||
|
val expected = requireNotNull(capturedReceiveAmount) { "Receive amount was not captured" }
|
||||||
|
onSwapTokenScreen {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { receiveAmount.assertTextEquals(expected) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5111")
|
||||||
|
@DisplayName("Gasless Swap: fee-token selector — coin has a speed choice, stablecoin only Market, token is selectable")
|
||||||
|
@Test
|
||||||
|
fun checkFeeSelectorBottomSheetTest() {
|
||||||
|
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSpeed = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSpeed = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Open the 'Network fee' bottom sheet") {
|
||||||
|
openSwapNetworkFeeSelector()
|
||||||
|
}
|
||||||
|
step("Assert the '$nativeTokenName' fee coin is displayed") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Open 'Choose speed' for the '$nativeTokenName' fee") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose speed' offers multiple speeds ('$marketSpeed', '$fastSpeed') for the coin") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendSelectNetworkFeeBottomSheet {
|
||||||
|
chooseSpeedTitle.assertIsDisplayed()
|
||||||
|
regularFeeSelectorItem(marketSpeed).assertIsDisplayed()
|
||||||
|
regularFeeSelectorItem(fastSpeed).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Select '$marketSpeed' speed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSpeed).performClick() }
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' is displayed and '$tokenName' is available for the fee") {
|
||||||
|
onSendFeeSelectorBottomSheet {
|
||||||
|
chooseTokenTitle.assertIsDisplayed()
|
||||||
|
feeTokenItem(tokenName).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert only '$marketSpeed' speed is available for the stablecoin fee") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet {
|
||||||
|
feeSpeedItemTitle(marketSpeed).assertIsDisplayed()
|
||||||
|
feeSpeedItemTitle(fastSpeed).assertIsNotDisplayed()
|
||||||
|
feeSpeedItemTitle(slowSpeed).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on '$marketSpeed' fee row") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose speed' bottom sheet did not open for the stablecoin fee") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert the network fee is shown in '$currencySymbol' on the swap screen") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5110")
|
||||||
|
@DisplayName("Gasless Swap: network fee with fee-token selection is shown for a USDC -> POL swap")
|
||||||
|
@Test
|
||||||
|
fun checkNetworkFeeSelectionForSwapTest() {
|
||||||
|
val receiveTokenName = "Polygon"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$receiveTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = receiveTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Assert 'Network fee' block with token selection is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
networkFeeBlock.assertIsDisplayed()
|
||||||
|
selectFeeIcon.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Open the 'Network fee' bottom sheet") {
|
||||||
|
openSwapNetworkFeeSelector()
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' is displayed and '$tokenName' is available for the fee") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet {
|
||||||
|
chooseTokenTitle.assertIsDisplayed()
|
||||||
|
feeTokenItem(tokenName).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5116")
|
||||||
|
@DisplayName("Gasless Swap: sign a swap paying the fee with the stablecoin and reach the provider")
|
||||||
|
@Test
|
||||||
|
fun checkSignSwapWithStablecoinFeeTest() {
|
||||||
|
val hotWalletTokensState = "PolygonUSDCHotWallet"
|
||||||
|
val receiveTokenName = "Polygon"
|
||||||
|
val providerName = "Changelly"
|
||||||
|
val exchangeStatusScenario = "exchange_status_provider"
|
||||||
|
val changellyStatusState = "Changelly"
|
||||||
|
val expressStatusItemTitle = getResourceString(CommonR.string.express_exchange_by, providerName)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(exchangeStatusScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$exchangeStatusScenario' to '$changellyStatusState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = exchangeStatusScenario, state = changellyStatusState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$receiveTokenName' on an existing hot wallet") {
|
||||||
|
openSwapAmountScreen(
|
||||||
|
fromTokenName = tokenName,
|
||||||
|
receiveTokenName = receiveTokenName,
|
||||||
|
amount = inputAmount,
|
||||||
|
seedPhrase = SVS_SEED_PHRASE_12,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with the stablecoin '$tokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = nativeTokenName, newFeeToken = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert the network fee is paid in '$currencySymbol'") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
feeBlockCurrency(currencySymbol).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Confirm the swap by holding the 'Swap' button and sign") {
|
||||||
|
confirmSwapByHolding()
|
||||||
|
}
|
||||||
|
step("Assert the 'Swap in progress' screen is displayed (transaction sent to provider)") {
|
||||||
|
onSwapSuccessScreen {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapSuccessScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Express status' item with title '$expressStatusItemTitle' is displayed") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5118")
|
||||||
|
@DisplayName("Gasless Swap: insufficient stablecoin balance to cover the fee shows an error and blocks the swap")
|
||||||
|
@Test
|
||||||
|
fun checkInsufficientBalanceForFeeTest() {
|
||||||
|
val usdcBalanceScenario = "polygon_usdc_balance"
|
||||||
|
val lowBalanceState = "LowBalance"
|
||||||
|
val lowAmount = "0.0005"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
resetWireMockScenarioState(usdcBalanceScenario)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = lowAmount)
|
||||||
|
}
|
||||||
|
step("Open the 'Network fee' bottom sheet") {
|
||||||
|
openSwapNetworkFeeSelector()
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Not enough funds' error is displayed in the fee selector") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5112")
|
||||||
|
@DisplayName("Gasless Swap: the stablecoin network fee is shown with its fiat (dollar) equivalent")
|
||||||
|
@Test
|
||||||
|
fun checkStablecoinFeeShownWithFiatTest() {
|
||||||
|
val fiatSign = "$"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with the stablecoin '$tokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = nativeTokenName, newFeeToken = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert the network fee is shown in '$currencySymbol' with its fiat ('$fiatSign') equivalent") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
feeBlockCurrency(currencySymbol).assertIsDisplayed()
|
||||||
|
feeAmount.assertTextContains(fiatSign, substring = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5114")
|
||||||
|
@DisplayName("Gasless Swap: switching the fee token from the coin to the stablecoin and back updates the summary")
|
||||||
|
@Test
|
||||||
|
fun checkSwitchFeeTokenBetweenCoinAndStablecoinTest() {
|
||||||
|
val nativeSymbol = "POL"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Switch the fee token from the coin '$nativeTokenName' to the stablecoin '$tokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = nativeTokenName, newFeeToken = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert the network fee is now paid in '$currencySymbol' on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Switch the fee token from the stablecoin '$tokenName' back to the coin '$nativeTokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = tokenName, newFeeToken = nativeTokenName)
|
||||||
|
}
|
||||||
|
step("Assert the network fee is back to the coin ('$nativeSymbol') on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen { feeBlockCurrency(nativeSymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5115")
|
||||||
|
@DisplayName("Gasless Swap: fee selector offers token selection and no speed choice when paying with a token")
|
||||||
|
@Test
|
||||||
|
fun checkFeeSelectionOptionsTest() {
|
||||||
|
val marketSpeed = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSpeed = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSpeed = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = inputAmount)
|
||||||
|
}
|
||||||
|
step("Open the 'Network fee' bottom sheet") {
|
||||||
|
openSwapNetworkFeeSelector()
|
||||||
|
}
|
||||||
|
step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose token' is displayed and '$tokenName' is available for the fee") {
|
||||||
|
onSendFeeSelectorBottomSheet {
|
||||||
|
chooseTokenTitle.assertIsDisplayed()
|
||||||
|
feeTokenItem(tokenName).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Select '$tokenName' as the fee-paying token") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert only '$marketSpeed' speed is available for the token fee") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSendFeeSelectorBottomSheet {
|
||||||
|
feeSpeedItemTitle(marketSpeed).assertIsDisplayed()
|
||||||
|
feeSpeedItemTitle(fastSpeed).assertIsNotDisplayed()
|
||||||
|
feeSpeedItemTitle(slowSpeed).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on '$marketSpeed' fee row") {
|
||||||
|
onSendFeeSelectorBottomSheet { feeSpeedItemTitle(marketSpeed).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Choose speed' bottom sheet did not open when paying with a token") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Apply' button") {
|
||||||
|
onSendFeeSelectorBottomSheet { applyButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert the network fee is shown in '$currencySymbol' on the summary") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5113")
|
||||||
|
@DisplayName("Gasless Swap: the max amount reserves the stablecoin fee and stays valid without an insufficient error")
|
||||||
|
@Test
|
||||||
|
fun checkMaxAmountReservesFeeTest() {
|
||||||
|
val maxInputAmount = "100"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(assetsScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$userTokensState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
|
||||||
|
setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Open the swap amount screen for '$tokenName' -> '$swapTokenName' with the max amount '$maxInputAmount'") {
|
||||||
|
openSwapAmountScreen(fromTokenName = tokenName, receiveTokenName = swapTokenName, amount = maxInputAmount)
|
||||||
|
}
|
||||||
|
step("Pay the network fee with the stablecoin '$tokenName'") {
|
||||||
|
switchFeeTokenAndApply(currentFeeToken = nativeTokenName, newFeeToken = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert no insufficient-balance error and 'Swap' is enabled (the fee is reserved from the max amount)") {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
insufficientFundsErrorTitle.assertIsNotDisplayed()
|
||||||
|
swapButton.assertIsEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,6 @@ package com.tangem.tests.swap
|
||||||
import androidx.compose.ui.test.longClick
|
import androidx.compose.ui.test.longClick
|
||||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
|
||||||
import com.tangem.common.extensions.assertHasBadge
|
|
||||||
import com.tangem.common.extensions.restartApp
|
import com.tangem.common.extensions.restartApp
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
|
@ -18,101 +16,6 @@ import org.junit.Test
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class SwapStoriesTest : BaseTestCase() {
|
class SwapStoriesTest : BaseTestCase() {
|
||||||
|
|
||||||
@AllureId("5453")
|
|
||||||
@DisplayName("Check 'Swap' button badge on 'Main' screen")
|
|
||||||
@Test
|
|
||||||
fun checkMainScreenSwapButtonBadgeTest() {
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5454")
|
|
||||||
@DisplayName("Check 'Swap' button badge on token details screen")
|
|
||||||
@Test
|
|
||||||
fun checkTokenDetailsScreenSwapButtonTest() {
|
|
||||||
val tokenName = "Ethereum"
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Click on token with name: '$tokenName'") {
|
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5455")
|
|
||||||
@DisplayName("Check 'Swap' button badge on token details in 'Market' screen")
|
|
||||||
@Test
|
|
||||||
fun checkMarketTokenDetailsScreenSwapButtonTest() {
|
|
||||||
val tokenName = "Ethereum"
|
|
||||||
val badgeShown = "Badge shown"
|
|
||||||
val badgeHidden = "Badge hidden"
|
|
||||||
|
|
||||||
setupHooks().run {
|
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
|
||||||
openMainScreen()
|
|
||||||
}
|
|
||||||
step("Synchronize addresses") {
|
|
||||||
synchronizeAddresses()
|
|
||||||
}
|
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails)
|
|
||||||
}
|
|
||||||
step("Click on 'Close' button") {
|
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllureId("5469")
|
@AllureId("5469")
|
||||||
@DisplayName("Check unavailable swap stories on 'Main' screen")
|
@DisplayName("Check unavailable swap stories on 'Main' screen")
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -136,9 +39,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
@ -155,9 +55,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMainScreen { swapButton.assertIsDisplayed() }
|
onMainScreen { swapButton.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true)
|
||||||
}
|
}
|
||||||
|
|
@ -192,9 +89,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
@ -207,13 +101,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Restart app") {
|
step("Restart app") {
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
waitForIdle()
|
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
|
||||||
composeTestRule.mainClock.advanceTimeBy(500)
|
|
||||||
onMainScreen { swapButton.assertHasBadge() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||||
}
|
}
|
||||||
|
|
@ -228,8 +115,6 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
val scenarioErrorState = "Error"
|
val scenarioErrorState = "Error"
|
||||||
val packageName = getTargetContext().packageName
|
val packageName = getTargetContext().packageName
|
||||||
val tokenName = "Ethereum"
|
val tokenName = "Ethereum"
|
||||||
val badgeShown = "Badge shown"
|
|
||||||
val badgeHidden = "Badge hidden"
|
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeAppLaunchSection = {
|
additionalBeforeAppLaunchSection = {
|
||||||
|
|
@ -246,16 +131,15 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Open 'Token details' from 'Markets' screen for token '$tokenName'") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
openTokenDetailsFromMarketsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has not badge") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
step("Click on 'Close' button") {
|
step("Click on 'Close' button") {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
|
@ -266,16 +150,12 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Restart app") {
|
step("Restart app") {
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Assert 'Swap' button is displayed") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
|
||||||
}
|
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
onTokenDetailsScreen { swapButton.assertIsDisplayed() }
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen") {
|
step("Open 'Swap' screen") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = true)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,11 +211,8 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenName'") {
|
step("Click on token with name: '$tokenName'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button has badge") {
|
|
||||||
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
|
||||||
}
|
|
||||||
step("Click on 'Swap' button on 'Token details' screen") {
|
step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check stories changes") {
|
step("Check stories changes") {
|
||||||
checkStoriesChanges()
|
checkStoriesChanges()
|
||||||
|
|
@ -369,11 +246,11 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Synchronize addresses") {
|
step("Synchronize addresses") {
|
||||||
synchronizeAddresses()
|
synchronizeAddresses()
|
||||||
}
|
}
|
||||||
step("Open 'Markets' token details screen for token '$tokenName'") {
|
step("Open 'Token details' from 'Markets' screen for token '$tokenName'") {
|
||||||
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
openTokenDetailsFromMarketsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button on 'Markets' token details screen") {
|
step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Check stories changes") {
|
step("Check stories changes") {
|
||||||
checkStoriesChanges()
|
checkStoriesChanges()
|
||||||
|
|
@ -388,7 +265,7 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Open 'Swap' screen without stories") {
|
step("Open 'Swap' screen without stories") {
|
||||||
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -433,6 +310,17 @@ class SwapStoriesTest : BaseTestCase() {
|
||||||
step("Click on 'Close' button") {
|
step("Click on 'Close' button") {
|
||||||
onSwapTokenScreen { closeButton.performClick() }
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
}
|
}
|
||||||
|
step("Long click on token with name: '$tokenName' again to reopen actions menu") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen {
|
||||||
|
tokenWithTitleAndAddress(tokenName).performTouchInput {
|
||||||
|
longClick(
|
||||||
|
position = center,
|
||||||
|
durationMillis = 1000L,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
step("Open 'Swap' screen without stories") {
|
step("Open 'Swap' screen without stories") {
|
||||||
openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false)
|
openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.tangem.screens.*
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import io.qameta.allure.kotlin.AllureId
|
import io.qameta.allure.kotlin.AllureId
|
||||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
|
|
@ -50,7 +51,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -147,7 +148,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
disableMobileData()
|
disableMobileData()
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -201,7 +202,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -304,7 +305,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Close 'Stories' screen") {
|
step("Close 'Stories' screen") {
|
||||||
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
|
@ -510,7 +511,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is not dimmed. Swap available") {
|
step("Assert 'Swap' button is not dimmed. Swap available") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(false) }
|
onTokenDetailsScreen { swapButton.assertIsEnabled() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -519,7 +520,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
step("Press 'Back' button") {
|
step("Press 'Back' button") {
|
||||||
device.uiDevice.pressBack()
|
device.uiDevice.pressBack()
|
||||||
|
|
@ -528,7 +529,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
|
onTokenDetailsScreen { swapButton.assertIsNotEnabled() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -545,11 +546,11 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
val inputAmount = "0.99"
|
val inputAmount = "0.99"
|
||||||
val market = "Market"
|
val market = "Market"
|
||||||
val fast = "Fast"
|
val fast = "Fast"
|
||||||
val marketFeeAmount = "$1.12"
|
|
||||||
val fastFeeAmount = "$1.43"
|
|
||||||
|
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
|
|
||||||
|
var marketFee = 0.0
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
}
|
}
|
||||||
|
|
@ -575,19 +576,28 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
textInput.performTextReplacement(inputAmount)
|
textInput.performTextReplacement(inputAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Select '$market' fee type") {
|
step("Select '$market' fee type and capture its amount") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount)
|
marketFee = parseFeeAmount(selectFeeTypeAndReadFee(FeeType.Market))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Select '$fast' fee type") {
|
step("Select '$fast' fee type and assert it exceeds the '$market' fee") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount)
|
val fastFee = parseFeeAmount(selectFeeTypeAndReadFee(FeeType.Fast))
|
||||||
|
assertTrue(
|
||||||
|
"Expected '$fast' fee ($fastFee) to be greater than '$market' fee ($marketFee)",
|
||||||
|
fastFee > marketFee,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseFeeAmount(raw: String): Double = raw
|
||||||
|
.replace(Regex("[^0-9.]"), "")
|
||||||
|
.toDoubleOrNull()
|
||||||
|
?: error("Could not parse a numeric fee amount from '$raw'")
|
||||||
|
|
||||||
@AllureId("8536")
|
@AllureId("8536")
|
||||||
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
|
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -600,18 +610,24 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
val feeAmount = "$"
|
val feeAmount = "$"
|
||||||
val scenarioName = "eth_network_balance"
|
val scenarioName = "eth_network_balance"
|
||||||
val scenarioState = "LessThanDollar"
|
val scenarioState = "LessThanDollar"
|
||||||
|
val pairsScenarioName = "ethereum_from_pairs"
|
||||||
|
val pairsScenarioState = "DexProvider"
|
||||||
val networkName = "Ethereum"
|
val networkName = "Ethereum"
|
||||||
val currencySymbol = "ETH"
|
val currencySymbol = "ETH"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
resetWireMockScenarioState(scenarioName)
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
resetWireMockScenarioState(pairsScenarioName)
|
||||||
}
|
}
|
||||||
).run {
|
).run {
|
||||||
|
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
}
|
}
|
||||||
|
step("Set WireMock scenario: '$pairsScenarioName' to state: $pairsScenarioState") {
|
||||||
|
setWireMockScenarioState(scenarioName = pairsScenarioName, state = pairsScenarioState)
|
||||||
|
}
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
|
|
@ -679,6 +695,8 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
val marketFeeAmount = "$1."
|
val marketFeeAmount = "$1."
|
||||||
val scenarioName = "eth_fee_history"
|
val scenarioName = "eth_fee_history"
|
||||||
val scenarioState = "UnableToCoverFastFee"
|
val scenarioState = "UnableToCoverFastFee"
|
||||||
|
val pairsScenarioName = "ethereum_from_pairs"
|
||||||
|
val pairsScenarioState = "DexProvider"
|
||||||
val networkName = "Ethereum"
|
val networkName = "Ethereum"
|
||||||
val currencySymbol = "ETH"
|
val currencySymbol = "ETH"
|
||||||
|
|
||||||
|
|
@ -686,12 +704,16 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
resetWireMockScenarioState(scenarioName)
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
resetWireMockScenarioState(pairsScenarioName)
|
||||||
}
|
}
|
||||||
).run {
|
).run {
|
||||||
|
|
||||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||||
}
|
}
|
||||||
|
step("Set WireMock scenario: '$pairsScenarioName' to state: $pairsScenarioState") {
|
||||||
|
setWireMockScenarioState(scenarioName = pairsScenarioName, state = pairsScenarioState)
|
||||||
|
}
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
|
|
@ -728,7 +750,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
step("Select '$fastFeeType' fee type") {
|
step("Select '$fastFeeType' fee type") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
selectFeeTypeWithGasless(feeType = FeeType.Fast, fastFeeAmount)
|
selectFeeType(feeType = FeeType.Fast, selectedFeeAmount = fastFeeAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") {
|
step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") {
|
||||||
|
|
@ -744,7 +766,7 @@ class SwapTokenScreenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
step("Select '$marketFeeType' fee type") {
|
step("Select '$marketFeeType' fee type") {
|
||||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
selectFeeTypeWithGasless(feeType = FeeType.Market, marketFeeAmount)
|
selectFeeType(feeType = FeeType.Market, selectedFeeAmount = marketFeeAmount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is enabled") {
|
step("Assert 'Swap' button is enabled") {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
||||||
val tokensScenarioState = "SolanaUSDC"
|
val tokensScenarioState = "SolanaUSDC"
|
||||||
val balanceScenarioName = "solana_balance"
|
val balanceScenarioName = "solana_balance"
|
||||||
val balanceScenarioState = "Empty"
|
val balanceScenarioState = "Empty"
|
||||||
|
val pairsScenarioName = "solana_from_pairs"
|
||||||
|
val pairsScenarioState = "DexProvider"
|
||||||
val networkName = "Solana"
|
val networkName = "Solana"
|
||||||
val currencySymbol = "SOL"
|
val currencySymbol = "SOL"
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
||||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
resetWireMockScenarioState(balanceScenarioName)
|
resetWireMockScenarioState(balanceScenarioName)
|
||||||
|
resetWireMockScenarioState(pairsScenarioName)
|
||||||
}
|
}
|
||||||
).run {
|
).run {
|
||||||
|
|
||||||
|
|
@ -106,6 +109,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
||||||
step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") {
|
step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") {
|
||||||
setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState)
|
setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState)
|
||||||
}
|
}
|
||||||
|
step("Set WireMock scenario: '$pairsScenarioName' to state: $pairsScenarioState") {
|
||||||
|
setWireMockScenarioState(scenarioName = pairsScenarioName, state = pairsScenarioState)
|
||||||
|
}
|
||||||
|
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
openMainScreen()
|
openMainScreen()
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ package com.tangem.tests.tangempay
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.common.extensions.assertTextContainsSafe
|
import com.tangem.common.extensions.assertTextContainsSafe
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.extractText
|
import com.tangem.common.extensions.extractText
|
||||||
import com.tangem.common.extensions.pullToRefresh
|
|
||||||
import com.tangem.common.utils.assertClipboardTextEquals
|
import com.tangem.common.utils.assertClipboardTextEquals
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarios
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.scenarios.*
|
import com.tangem.scenarios.*
|
||||||
import com.tangem.screens.tangempay.*
|
import com.tangem.screens.tangempay.*
|
||||||
|
|
@ -23,7 +24,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
@AllureId("4549")
|
@AllureId("4549")
|
||||||
@DisplayName("Tangem Pay: change PIN code from card details")
|
@DisplayName("Tangem Pay: change PIN code from card details")
|
||||||
@Test
|
@Test
|
||||||
fun changePin_SetsNewPinCode_FromCardDetails() {
|
fun changePinSetsNewPinCodeFromCardDetailsTest() {
|
||||||
val newPin = "5217"
|
val newPin = "5217"
|
||||||
val pinSetupScenario = "tangem_pay_pin_setup"
|
val pinSetupScenario = "tangem_pay_pin_setup"
|
||||||
val pinNotSetState = "PinNotSet"
|
val pinNotSetState = "PinNotSet"
|
||||||
|
|
@ -31,6 +32,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
setWireMockScenarioState(pinSetupScenario, pinNotSetState)
|
setWireMockScenarioState(pinSetupScenario, pinNotSetState)
|
||||||
},
|
},
|
||||||
|
|
@ -52,11 +54,10 @@ class TangemPayTest : BaseTestCase() {
|
||||||
step("Enter PIN '$newPin'") {
|
step("Enter PIN '$newPin'") {
|
||||||
onTangemPayChangePinScreen { inputField.performTextInput(newPin) }
|
onTangemPayChangePinScreen { inputField.performTextInput(newPin) }
|
||||||
}
|
}
|
||||||
step("Click on 'Submit' button") {
|
step("Assert success screen title is displayed") {
|
||||||
onTangemPayChangePinScreen { submitButton.performClick() }
|
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||||
}
|
onTangemPayChangePinScreen { successTitle.assertIsDisplayed() }
|
||||||
step("Assert success screen is displayed") {
|
}
|
||||||
onTangemPayChangePinScreen { successTitle.assertIsDisplayed() }
|
|
||||||
}
|
}
|
||||||
step("Click on 'Done' button") {
|
step("Click on 'Done' button") {
|
||||||
onTangemPayChangePinScreen { doneButton.clickWithAssertion() }
|
onTangemPayChangePinScreen { doneButton.clickWithAssertion() }
|
||||||
|
|
@ -67,7 +68,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
@AllureId("4969")
|
@AllureId("4969")
|
||||||
@DisplayName("Tangem Pay: balance updates after transaction on payment account screen")
|
@DisplayName("Tangem Pay: balance updates after transaction on payment account screen")
|
||||||
@Test
|
@Test
|
||||||
fun balanceUpdatesAfterTransaction_OnPaymentAccountScreen() {
|
fun balanceUpdatesAfterTransactionOnPaymentAccountScreenTest() {
|
||||||
val balanceScenario = "tangem_pay_balance_update"
|
val balanceScenario = "tangem_pay_balance_update"
|
||||||
val initialState = "InitialBalance"
|
val initialState = "InitialBalance"
|
||||||
val afterTransactionState = "AfterTransaction"
|
val afterTransactionState = "AfterTransaction"
|
||||||
|
|
@ -75,6 +76,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
setWireMockScenarioState(balanceScenario, initialState)
|
setWireMockScenarioState(balanceScenario, initialState)
|
||||||
},
|
},
|
||||||
|
|
@ -90,7 +92,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
step("Switch WireMock scenario '$balanceScenario' to '$afterTransactionState'") {
|
step("Switch WireMock scenario '$balanceScenario' to '$afterTransactionState'") {
|
||||||
setWireMockScenarioState(balanceScenario, afterTransactionState)
|
setWireMockScenarioState(balanceScenario, afterTransactionState)
|
||||||
}
|
}
|
||||||
step("Pull to refresh") { pullToRefresh() }
|
step("Pull to refresh") { pullToRefreshTangemPay() }
|
||||||
step("Assert updated balance contains '9'") {
|
step("Assert updated balance contains '9'") {
|
||||||
onTangemPayMainScreen { balance.assertTextContainsSafe("9", substring = true) }
|
onTangemPayMainScreen { balance.assertTextContainsSafe("9", substring = true) }
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +102,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
@AllureId("4970")
|
@AllureId("4970")
|
||||||
@DisplayName("Tangem Pay: new transaction appears after mocked charge")
|
@DisplayName("Tangem Pay: new transaction appears after mocked charge")
|
||||||
@Test
|
@Test
|
||||||
fun transactionList_NewTransactionAppears_AfterMockedCharge() {
|
fun transactionListNewTransactionAppearsAfterMockedChargeTest() {
|
||||||
val historyScenario = "tangem_pay_transaction_history"
|
val historyScenario = "tangem_pay_transaction_history"
|
||||||
val initialState = "InitialEmpty"
|
val initialState = "InitialEmpty"
|
||||||
val afterTransactionState = "AfterTransaction"
|
val afterTransactionState = "AfterTransaction"
|
||||||
|
|
@ -109,6 +111,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
setWireMockScenarioState(historyScenario, initialState)
|
setWireMockScenarioState(historyScenario, initialState)
|
||||||
},
|
},
|
||||||
|
|
@ -126,7 +129,7 @@ class TangemPayTest : BaseTestCase() {
|
||||||
step("Switch WireMock scenario '$historyScenario' to '$afterTransactionState'") {
|
step("Switch WireMock scenario '$historyScenario' to '$afterTransactionState'") {
|
||||||
setWireMockScenarioState(historyScenario, afterTransactionState)
|
setWireMockScenarioState(historyScenario, afterTransactionState)
|
||||||
}
|
}
|
||||||
step("Pull to refresh") { pullToRefresh() }
|
step("Pull to refresh") { pullToRefreshTangemPay() }
|
||||||
step("Assert transaction from '$merchantName' is displayed") {
|
step("Assert transaction from '$merchantName' is displayed") {
|
||||||
onTangemPayMainScreen {
|
onTangemPayMainScreen {
|
||||||
transactionRowWithText(merchantName).assertIsDisplayed()
|
transactionRowWithText(merchantName).assertIsDisplayed()
|
||||||
|
|
@ -138,12 +141,13 @@ class TangemPayTest : BaseTestCase() {
|
||||||
@AllureId("4974")
|
@AllureId("4974")
|
||||||
@DisplayName("Tangem Pay: reveal and copy card number, expiration and CVC")
|
@DisplayName("Tangem Pay: reveal and copy card number, expiration and CVC")
|
||||||
@Test
|
@Test
|
||||||
fun revealAndCopyCardDetails_NumberExpirationCVC() {
|
fun revealAndCopyCardDetailsNumberExpirationCVCTest() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val eligibilityState = "PaeraCustomer"
|
val eligibilityState = "PaeraCustomer"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
},
|
},
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
|
|
@ -152,9 +156,11 @@ class TangemPayTest : BaseTestCase() {
|
||||||
).run {
|
).run {
|
||||||
openTangemPay()
|
openTangemPay()
|
||||||
step("Click on card button") {
|
step("Click on card button") {
|
||||||
|
waitForIdle()
|
||||||
onTangemPayMainScreen { cardButton.clickWithAssertion() }
|
onTangemPayMainScreen { cardButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on 'Show details' button") {
|
step("Click on 'Show details' button") {
|
||||||
|
waitForIdle()
|
||||||
onTangemPayCardPageScreen { showDetailsButton.clickWithAssertion() }
|
onTangemPayCardPageScreen { showDetailsButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert number, expiration and CVC values are visible") {
|
step("Assert number, expiration and CVC values are visible") {
|
||||||
|
|
@ -200,13 +206,14 @@ class TangemPayTest : BaseTestCase() {
|
||||||
@AllureId("4971")
|
@AllureId("4971")
|
||||||
@DisplayName("Tangem Pay: freeze card via confirmation sheet")
|
@DisplayName("Tangem Pay: freeze card via confirmation sheet")
|
||||||
@Test
|
@Test
|
||||||
fun freezeUnfreezeCard_TogglesCardState() {
|
fun freezeUnfreezeCardTogglesCardStateTest() {
|
||||||
val freezeScenario = "tangem_pay_card_freeze"
|
val freezeScenario = "tangem_pay_card_freeze"
|
||||||
val startedState = "Started"
|
val startedState = "Started"
|
||||||
val eligibilityState = "PaeraCustomer"
|
val eligibilityState = "PaeraCustomer"
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
setWireMockScenarioState(freezeScenario, startedState)
|
setWireMockScenarioState(freezeScenario, startedState)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||||
import com.tangem.common.extensions.assertTextContainsSafe
|
import com.tangem.common.extensions.assertTextContainsSafe
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarios
|
||||||
import com.tangem.common.utils.setWireMockScenarioState
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
import com.tangem.core.res.R as CoreResR
|
import com.tangem.core.res.R as CoreResR
|
||||||
import com.tangem.scenarios.*
|
import com.tangem.scenarios.*
|
||||||
|
|
@ -26,7 +27,7 @@ class TangemPayTopUpTest : BaseTestCase() {
|
||||||
@AllureId("4973")
|
@AllureId("4973")
|
||||||
@DisplayName("Tangem Pay: top up swaps Bitcoin to USDC and appends deposit to history")
|
@DisplayName("Tangem Pay: top up swaps Bitcoin to USDC and appends deposit to history")
|
||||||
@Test
|
@Test
|
||||||
fun topUpFromTangemPay_SwapsBitcoinToUSDC_AppendsDepositToHistory() {
|
fun topUpFromTangemPaySwapsBitcoinToUSDCAppendsDepositToHistoryTest() {
|
||||||
val bitcoinScenario = "bitcoin_utxo"
|
val bitcoinScenario = "bitcoin_utxo"
|
||||||
val expressAssetsScenario = "express_api_assets"
|
val expressAssetsScenario = "express_api_assets"
|
||||||
val balanceScenario = "tangem_pay_balance_update"
|
val balanceScenario = "tangem_pay_balance_update"
|
||||||
|
|
@ -43,6 +44,7 @@ class TangemPayTopUpTest : BaseTestCase() {
|
||||||
|
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalBeforeSection = {
|
additionalBeforeSection = {
|
||||||
|
resetWireMockScenarios()
|
||||||
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
setWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO, eligibilityState)
|
||||||
setWireMockScenarioState(bitcoinScenario, bitcoinBalanceState)
|
setWireMockScenarioState(bitcoinScenario, bitcoinBalanceState)
|
||||||
setWireMockScenarioState(expressAssetsScenario, expressAssetsState)
|
setWireMockScenarioState(expressAssetsScenario, expressAssetsState)
|
||||||
|
|
@ -62,6 +64,7 @@ class TangemPayTopUpTest : BaseTestCase() {
|
||||||
onTangemPayMainScreen { balance.assertTextContainsSafe("10", substring = true) }
|
onTangemPayMainScreen { balance.assertTextContainsSafe("10", substring = true) }
|
||||||
}
|
}
|
||||||
step("Click on 'Top Up' action chip") {
|
step("Click on 'Top Up' action chip") {
|
||||||
|
waitForIdle()
|
||||||
onTangemPayMainScreen { topUpButton.clickWithAssertion() }
|
onTangemPayMainScreen { topUpButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Assert 'Add Funds' sheet is displayed") {
|
step("Assert 'Add Funds' sheet is displayed") {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -210,6 +210,17 @@
|
||||||
android:scheme="tangem" />
|
android:scheme="tangem" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="survey"
|
||||||
|
android:scheme="tangem" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
@ -402,6 +413,14 @@
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- SurveySparrow SDK exports this activity (exported="true") in its own manifest. -->
|
||||||
|
<!-- We launch it only via an explicit in-app intent, so force it non-exported to -->
|
||||||
|
<!-- prevent any other installed app from loading arbitrary web content into it. -->
|
||||||
|
<activity
|
||||||
|
android:name="com.surveysparrow.ss_android_sdk.SsSurveyActivity"
|
||||||
|
android:exported="false"
|
||||||
|
tools:replace="android:exported" />
|
||||||
</application>
|
</application>
|
||||||
<queries>
|
<queries>
|
||||||
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
|
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 97ff5929f9ff4da53190eb10e94c45ac3bd05093
|
Subproject commit f983c6defd0b2240eb6f134aefe30f7e93696795
|
||||||
|
|
@ -5,9 +5,12 @@ import com.tangem.core.abtests.manager.ABTestsManager
|
||||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||||
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
|
import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor
|
||||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||||
|
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
|
import com.tangem.lib.auth.AuthFeatureToggles
|
||||||
|
import com.tangem.lib.auth.session.DeviceRegistrar
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
|
@ -49,4 +52,10 @@ interface ApplicationEntryPoint {
|
||||||
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
||||||
|
|
||||||
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
|
fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor
|
||||||
|
|
||||||
|
fun getDeviceKeyManager(): DeviceKeyManager
|
||||||
|
|
||||||
|
fun getDeviceRegistrar(): DeviceRegistrar
|
||||||
|
|
||||||
|
fun getAuthFeatureToggles(): AuthFeatureToggles
|
||||||
}
|
}
|
||||||
|
|
@ -177,12 +177,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
TangemLogger.i("Splash screen installed")
|
TangemLogger.i("Splash screen installed")
|
||||||
|
|
||||||
enableEdgeToEdge(
|
applyEdgeToEdge()
|
||||||
navigationBarStyle = SystemBarStyle.auto(
|
|
||||||
Color.Transparent.toArgb(),
|
|
||||||
Color.Transparent.toArgb(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -200,6 +195,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
|
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
|
||||||
|
splashScreen.setOnExitAnimationListener { provider ->
|
||||||
|
provider.remove()
|
||||||
|
applyEdgeToEdge()
|
||||||
|
}
|
||||||
|
|
||||||
installActivityDependencies()
|
installActivityDependencies()
|
||||||
observeAppThemeModeUpdates()
|
observeAppThemeModeUpdates()
|
||||||
|
|
@ -223,6 +222,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyEdgeToEdge() {
|
||||||
|
enableEdgeToEdge(
|
||||||
|
navigationBarStyle = SystemBarStyle.auto(
|
||||||
|
Color.Transparent.toArgb(),
|
||||||
|
Color.Transparent.toArgb(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setRootContent() {
|
private fun setRootContent() {
|
||||||
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
|
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
|
||||||
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
|
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
import com.tangem.domain.common.LogConfig
|
import com.tangem.domain.common.LogConfig
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
|
import com.tangem.lib.auth.AuthFeatureToggles
|
||||||
|
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||||
|
import com.tangem.lib.auth.session.DeviceRegistrar
|
||||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||||
|
|
@ -92,6 +95,15 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val sendTransactionSignerInfoInterceptor
|
private val sendTransactionSignerInfoInterceptor
|
||||||
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
|
get() = entryPoint.getSendTransactionSignerInfoInterceptor()
|
||||||
|
|
||||||
|
private val deviceKeyManager: DeviceKeyManager
|
||||||
|
get() = entryPoint.getDeviceKeyManager()
|
||||||
|
|
||||||
|
private val deviceRegistrar: DeviceRegistrar
|
||||||
|
get() = entryPoint.getDeviceRegistrar()
|
||||||
|
|
||||||
|
private val authFeatureToggles: AuthFeatureToggles
|
||||||
|
get() = entryPoint.getAuthFeatureToggles()
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
private val appScope = MainScope()
|
private val appScope = MainScope()
|
||||||
|
|
@ -132,6 +144,16 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
|
if (authFeatureToggles.isBackendAuthenticationEnabled) {
|
||||||
|
appScope.launch {
|
||||||
|
// Order matters: registration reads the device public key, so it must wait for
|
||||||
|
// generation to complete. Running them concurrently on first launch would race —
|
||||||
|
// register() would see `DeviceKeyUnavailable` and defer to the next app launch.
|
||||||
|
deviceKeyManager.generateIfMissing()
|
||||||
|
deviceRegistrar.register()
|
||||||
|
.onLeft { error -> TangemLogger.w("Device registration deferred: $error") }
|
||||||
|
}
|
||||||
|
}
|
||||||
walletsRepository = entryPoint.getWalletsRepository()
|
walletsRepository = entryPoint.getWalletsRepository()
|
||||||
|
|
||||||
apiConfigsManager.initialize()
|
apiConfigsManager.initialize()
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue