Updated on 2026-08-14
This commit is contained in:
parent
aed32a47f1
commit
58e26ca5e6
8 changed files with 139 additions and 14 deletions
41
Marathonfile
Normal file
41
Marathonfile
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: "Tangem Android UI Tests"
|
||||||
|
outputDir: "build/reports/marathon"
|
||||||
|
|
||||||
|
testOutputTimeoutMillis: 180000 # 3 minutes
|
||||||
|
testBatchTimeoutMillis: 600000 # 10 minutes
|
||||||
|
|
||||||
|
vendorConfiguration:
|
||||||
|
type: "Android"
|
||||||
|
applicationApk: "app/build/outputs/apk/google/mocked/app-google-mocked.apk"
|
||||||
|
testApplicationApk: "app/build/outputs/apk/androidTest/google/mocked/app-google-mocked-androidTest.apk"
|
||||||
|
autoGrantPermission: true
|
||||||
|
applicationPmClear: true
|
||||||
|
testApplicationPmClear: true
|
||||||
|
# wiremockBaseUrl redirects API calls from wiremock.tests-d.com to local WireMock
|
||||||
|
# This is used on CI where each emulator has its own WireMock instance via adb reverse
|
||||||
|
instrumentationArgs:
|
||||||
|
wiremockBaseUrl: "http://localhost:8080"
|
||||||
|
allureConfiguration:
|
||||||
|
enabled: false # we collect allure via adb pull, not through marathon
|
||||||
|
|
||||||
|
poolingStrategy:
|
||||||
|
type: "omni"
|
||||||
|
|
||||||
|
batchingStrategy:
|
||||||
|
type: "isolate"
|
||||||
|
|
||||||
|
sortingStrategy:
|
||||||
|
type: "no-sorting"
|
||||||
|
|
||||||
|
retryStrategy:
|
||||||
|
type: "fixed-quota"
|
||||||
|
totalAllowedRetryQuota: 30
|
||||||
|
retryPerTestQuota: 1
|
||||||
|
|
||||||
|
flakinessStrategy:
|
||||||
|
type: "ignore"
|
||||||
|
|
||||||
|
analyticsConfiguration:
|
||||||
|
type: "disabled"
|
||||||
|
|
||||||
|
debug: false
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||||
import androidx.compose.ui.test.printToLog
|
import androidx.compose.ui.test.printToLog
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
|
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
|
||||||
import com.kaspersky.components.alluresupport.withForcedAllureSupport
|
import com.kaspersky.components.alluresupport.withForcedAllureSupport
|
||||||
|
|
@ -19,6 +20,7 @@ import com.tangem.common.rules.ApiEnvironmentRule
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
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.utils.WireMockRedirectInterceptor
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.promo.models.PromoId
|
import com.tangem.domain.promo.models.PromoId
|
||||||
import com.tangem.tap.MainActivity
|
import com.tangem.tap.MainActivity
|
||||||
|
|
@ -86,6 +88,9 @@ abstract class BaseTestCase : TestCase(
|
||||||
additionalAfterSection: () -> Unit = {},
|
additionalAfterSection: () -> Unit = {},
|
||||||
) = before {
|
) = before {
|
||||||
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
|
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
|
||||||
|
// Setup WireMock redirect for CI with local WireMock instances
|
||||||
|
val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
|
||||||
|
WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
|
||||||
hiltRule.inject()
|
hiltRule.inject()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
|
|
@ -147,4 +152,8 @@ abstract class BaseTestCase : TestCase(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,31 @@
|
||||||
package com.tangem.common.utils
|
package com.tangem.common.utils
|
||||||
|
|
||||||
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the WireMock base URL to use.
|
||||||
|
*/
|
||||||
|
private fun getWireMockBaseUrl(): String =
|
||||||
|
WireMockRedirectInterceptor.overriddenBaseUrl ?: DEFAULT_WIREMOCK_URL
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method uses to set WireMock scenario state
|
* Method uses to set WireMock scenario state
|
||||||
* @param scenarioName Name of the scenario to modify
|
* @param scenarioName Name of the scenario to modify
|
||||||
* @param state The target state to set (must be one of the scenario's possibleStates)
|
* @param state The target state to set (must be one of the scenario's possibleStates)
|
||||||
* @param baseUrl WireMock base URL
|
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
|
||||||
* @return true if state was set successfully, false otherwise
|
* @return true if state was set successfully, false otherwise
|
||||||
*/
|
*/
|
||||||
fun setWireMockScenarioState(
|
fun setWireMockScenarioState(
|
||||||
scenarioName: String,
|
scenarioName: String,
|
||||||
state: String,
|
state: String,
|
||||||
baseUrl: String = "[REDACTED_ENV_URL]"
|
baseUrl: String = getWireMockBaseUrl()
|
||||||
): Boolean {
|
): Boolean {
|
||||||
Timber.i("=== WireMock Scenario Set ===")
|
Timber.i("=== WireMock Scenario Set ===")
|
||||||
Timber.i("Setting scenario '$scenarioName' to state: $state")
|
Timber.i("Setting scenario '$scenarioName' to state: $state")
|
||||||
|
|
@ -46,8 +55,9 @@ fun setWireMockScenarioState(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method checks accessibility of WireMock
|
* Method checks accessibility of WireMock
|
||||||
|
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
|
||||||
*/
|
*/
|
||||||
fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
|
||||||
val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("$baseUrl/__admin/scenarios")
|
.url("$baseUrl/__admin/scenarios")
|
||||||
|
|
@ -69,8 +79,9 @@ fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to reset all WireMock scenarios
|
* Method to reset all WireMock scenarios
|
||||||
|
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
|
||||||
*/
|
*/
|
||||||
fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
|
||||||
Timber.i("=== WireMock Scenarios Reset ===")
|
Timber.i("=== WireMock Scenarios Reset ===")
|
||||||
Timber.i("Base URL: $baseUrl")
|
Timber.i("Base URL: $baseUrl")
|
||||||
|
|
||||||
|
|
@ -105,13 +116,13 @@ fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
||||||
* Method to reset a specific WireMock scenario to its initial state
|
* Method to reset a specific WireMock scenario to its initial state
|
||||||
* @param scenarioName Name of the scenario to reset
|
* @param scenarioName Name of the scenario to reset
|
||||||
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
|
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
|
||||||
* @param baseUrl WireMock base URL
|
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
|
||||||
* @return true if reset was successful, false otherwise
|
* @return true if reset was successful, false otherwise
|
||||||
*/
|
*/
|
||||||
fun resetWireMockScenarioState(
|
fun resetWireMockScenarioState(
|
||||||
scenarioName: String,
|
scenarioName: String,
|
||||||
initialState: String = "Started",
|
initialState: String = "Started",
|
||||||
baseUrl: String = "[REDACTED_ENV_URL]"
|
baseUrl: String = getWireMockBaseUrl()
|
||||||
): Boolean {
|
): Boolean {
|
||||||
Timber.i("=== WireMock Scenario Reset ===")
|
Timber.i("=== WireMock Scenario Reset ===")
|
||||||
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
|
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||||
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||||
|
|
@ -334,15 +335,23 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
ExceptionHandler.append(blockchainExceptionHandler)
|
ExceptionHandler.append(blockchainExceptionHandler)
|
||||||
|
|
||||||
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
|
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
|
||||||
BlockchainSdkRetrofitBuilder.interceptors = listOf(
|
BlockchainSdkRetrofitBuilder.interceptors = buildList {
|
||||||
createNetworkLoggingInterceptor(),
|
if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||||
ChuckerInterceptor(this),
|
add(WireMockRedirectInterceptor())
|
||||||
)
|
}
|
||||||
|
add(createNetworkLoggingInterceptor())
|
||||||
|
add(ChuckerInterceptor(this@TangemApplication))
|
||||||
|
}
|
||||||
|
|
||||||
TangemApiServiceSettings.addInterceptors(
|
TangemApiServiceSettings.addInterceptors(
|
||||||
createNetworkLoggingInterceptor(),
|
*buildList {
|
||||||
ChuckerInterceptor(this),
|
if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||||
NetworkLogsSaveInterceptor(appLogsStore),
|
add(WireMockRedirectInterceptor())
|
||||||
|
}
|
||||||
|
add(createNetworkLoggingInterceptor())
|
||||||
|
add(ChuckerInterceptor(this@TangemApplication))
|
||||||
|
add(NetworkLogsSaveInterceptor(appLogsStore))
|
||||||
|
}.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,9 @@
|
||||||
<certificates src="user" />
|
<certificates src="user" />
|
||||||
</trust-anchors>
|
</trust-anchors>
|
||||||
</base-config>
|
</base-config>
|
||||||
|
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||||
|
<domain includeSubdomains="false">localhost</domain>
|
||||||
|
</domain-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.tangem.datasource.api.utils.WriteTimeout
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||||
|
import com.tangem.datasource.utils.WireMockRedirectInterceptor
|
||||||
import com.tangem.datasource.utils.addHeaders
|
import com.tangem.datasource.utils.addHeaders
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
|
@ -77,6 +78,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
||||||
.client(
|
.client(
|
||||||
OkHttpClient.Builder()
|
OkHttpClient.Builder()
|
||||||
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
|
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
|
||||||
|
.applyWireMockRedirect()
|
||||||
.let {
|
.let {
|
||||||
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
|
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +183,14 @@ internal class RetrofitApiBuilder @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun OkHttpClient.Builder.applyWireMockRedirect(): OkHttpClient.Builder {
|
||||||
|
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||||
|
addInterceptor(interceptor = WireMockRedirectInterceptor())
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
|
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
|
||||||
if (apiConfigId in excludedApiForLogging) return this
|
if (apiConfigId in excludedApiForLogging) return this
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.tangem.datasource.utils
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
|
||||||
|
*/
|
||||||
|
class WireMockRedirectInterceptor : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val override = overriddenBaseUrl ?: return chain.proceed(chain.request())
|
||||||
|
|
||||||
|
val request = chain.request()
|
||||||
|
val url = request.url.toString()
|
||||||
|
|
||||||
|
if (url.contains(WIREMOCK_REMOTE_URL)) {
|
||||||
|
val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/'))
|
||||||
|
Timber.d("WireMockRedirect: $url -> $newUrl")
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(newUrl)
|
||||||
|
.build()
|
||||||
|
return chain.proceed(newRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val WIREMOCK_REMOTE_URL = "[REDACTED_ENV_URL]"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override base URL for WireMock requests.
|
||||||
|
* When null (default), requests go to wiremock.tests-d.com.
|
||||||
|
* When set (e.g., "http://localhost:8080"), requests are redirected to local WireMock instance.
|
||||||
|
*/
|
||||||
|
var overriddenBaseUrl: String? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit ab6aee1d49ac0cfca644330a525dced725d5a257
|
Subproject commit dc9df17919126bce93482559fe26602f9801eb42
|
||||||
Loading…
Add table
Add a link
Reference in a new issue