package com.tangem.common import android.Manifest import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.printToLog import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor import com.kaspersky.components.alluresupport.withForcedAllureSupport import com.kaspersky.components.composesupport.config.addComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.PromoId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import io.qameta.allure.kotlin.Allure import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.rules.TestWatcher import org.junit.runner.Description import javax.inject.Inject abstract class BaseTestCase : TestCase( kaspressoBuilder = Kaspresso.Builder.withForcedAllureSupport( shouldRecordVideo = false ).apply { stepWatcherInterceptors = stepWatcherInterceptors.filter { it !is ScreenshotStepInterceptor }.toMutableList() stepWatcherInterceptors.addAll( listOf( FailedStepScreenshotInterceptor(screenshots) ) ) }.addComposeSupport() ) { @Inject lateinit var apiConfigsManager: ApiConfigsManager @Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var promoRepository: PromoRepository @Inject lateinit var walletManagersStore: WalletManagersStore @Inject lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( Manifest.permission.POST_NOTIFICATIONS, ) /** * It is important to use `ComposeRule` without specifying an activity to ensure that the initialization order of * all test rules is fully controlled. */ val composeTestRule = createEmptyComposeRule() private val semanticTreePrinterRule = object : TestWatcher() { override fun failed(e: Throwable?, description: Description?) { runCatching { runBlocking { withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() } } } } } @Rule @JvmField val ruleChain: TestRule = RuleChain .outerRule(hiltRule) .around(applicationInjectionRule()) .around(permissionRule) .around(apiEnvironmentRule) .around(composeTestRule) .around(semanticTreePrinterRule) /** * Initialization order is important: * – DI dependencies must be injected first, * – then the API environment should be set up, * – and only after that the activity should be launched. */ protected fun setupHooks( additionalBeforeAppLaunchSection: () -> Unit = {}, additionalBeforeSection: () -> Unit = {}, additionalAfterSection: () -> Unit = {}, ) = before { 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 additionalBeforeAppLaunchSection() hiltRule.inject() runBlocking { appPreferencesStore.editData { mutablePreferences -> mutablePreferences.set( key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, value = false ) } appPreferencesStore.editData { mutablePreferences -> mutablePreferences.set( key = PreferencesKeys.getShouldShowNotificationKey("EnablePushesReminderNotification"), value = false ) } promoRepository.setNeverToShowWalletPromo(PromoId.Sepa) } apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) Intents.init() additionalBeforeSection() }.after { additionalAfterSection() Intents.release() } /** * Prints the Compose semantics tree to logcat for debugging UI tests. * * @param rootIndex Use rootIndex > 0, if you need to print semantics tree for bottom sheet. * Default: 0. * @param useUnmergedTree When true, shows unmerged tree with all individual nodes. * Use for accessing inner elements of compound components. * Default: false (merged tree - accessibility view). * @param tag Log tag for filtering in logcat. Default: "SEMANTIC_TREE". * @param maxDepth Maximum nesting level to print. Use to avoid log overflow. * Default: Int.MAX_VALUE (unlimited depth). */ fun printSemanticTree( rootIndex: Int = 0, useUnmergedTree: Boolean = false, tag: String = "SEMANTIC_TREE", maxDepth: Int = Int.MAX_VALUE ) { composeTestRule.onAllNodes(isRoot(), useUnmergedTree = useUnmergedTree)[rootIndex] .printToLog(tag, maxDepth) } fun printAllRoots( tag: String = "ComposeTree", ) { val roots = composeTestRule.onAllNodes(isRoot()) val count = roots.fetchSemanticsNodes().size repeat(count) { index -> roots[index].printToLog("$tag[$index]") } } fun waitForIdle() = composeTestRule.waitForIdle() private fun applicationInjectionRule(): ApplicationInjectionExecutionRule { return ApplicationInjectionExecutionRule( toggleStates = mapOf( "SWAP_REDESIGN_ENABLED" to false, "ACCOUNTS_FEATURE_ENABLED" to true, "GASLESS_APPROVAL_ENABLED" to true, "MAIN_SCREEN_QR_SCANNING_ENABLED" to true, ) ) } private companion object { const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl" const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L } }