Updated on 2026-08-14
This commit is contained in:
parent
8922d23eec
commit
f3c2d98214
7 changed files with 962 additions and 30 deletions
|
|
@ -174,6 +174,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
installAppTheme()
|
||||
|
||||
val splashScreen = installSplashScreen()
|
||||
TangemLogger.i("Splash screen installed")
|
||||
|
||||
enableEdgeToEdge(
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
|
|
@ -350,6 +351,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
|
||||
|
||||
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
|
||||
if (isFromPush) {
|
||||
|
|
|
|||
|
|
@ -270,22 +270,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateLogFiles() {
|
||||
appLogsStore.deleteOldLogsFile()
|
||||
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) {
|
||||
appLogsStore.deleteLastLogFile()
|
||||
}
|
||||
|
||||
// Temporally logs are not saved
|
||||
// scope.launch {
|
||||
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
|
||||
// appLogsStore.deleteLastLogFile()
|
||||
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize components that need to be initialized before [super.onCreate] is called
|
||||
*/
|
||||
|
|
@ -389,6 +373,22 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
)
|
||||
}
|
||||
|
||||
private fun updateLogFiles() {
|
||||
appLogsStore.deleteOldLogsFile()
|
||||
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) {
|
||||
appLogsStore.deleteLastLogFile()
|
||||
}
|
||||
|
||||
// Temporarily logs are not saved
|
||||
// scope.launch {
|
||||
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
|
||||
// appLogsStore.deleteLastLogFile()
|
||||
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return createCoilImageLoader(
|
||||
context = this,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ internal class MainViewModel @Inject constructor(
|
|||
// await while initial route stack is initialized
|
||||
appRouterConfig.initializedState.first { it }
|
||||
|
||||
TangemLogger.withTag("MainActivity").i("Splash screen dismissed")
|
||||
isSplashScreenShown = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
|||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -19,6 +20,8 @@ internal class ProxyAppRouter(
|
|||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : AppRouter {
|
||||
|
||||
private val logger = TangemLogger.withTag("AppRouter")
|
||||
|
||||
private val routerScope: CoroutineScope
|
||||
get() = requireNotNull(config.routerScope) {
|
||||
"Router scope is not set in config"
|
||||
|
|
@ -47,12 +50,8 @@ internal class ProxyAppRouter(
|
|||
}
|
||||
|
||||
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
safeNavigate(onComplete, message = "Replace all routes with $routes") {
|
||||
runCatching {
|
||||
innerRouter.replaceAll(*routes, onComplete = onComplete)
|
||||
}.getOrElse {
|
||||
TangemLogger.e("Error", it)
|
||||
}
|
||||
safeNavigate(onComplete, message = "Replace all routes with ${routes.toList().ifEmpty { "<empty>" }}") {
|
||||
innerRouter.replaceAll(*routes, onComplete = onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,21 +75,20 @@ internal class ProxyAppRouter(
|
|||
|
||||
private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
TangemLogger.i(message)
|
||||
logger.i(message)
|
||||
|
||||
try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
TangemLogger.e("Error", e)
|
||||
onComplete(false)
|
||||
}
|
||||
runSuspendCatching(block = { block() })
|
||||
.onFailure { throwable ->
|
||||
logger.e(messageString = "Error", throwable = throwable)
|
||||
onComplete(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) {
|
||||
if (!isSuccess) {
|
||||
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage)))
|
||||
TangemLogger.w(errorMessage)
|
||||
logger.w(errorMessage)
|
||||
|
||||
with(receiver = config.snackbarHandler ?: return) {
|
||||
showSnackbar(
|
||||
|
|
|
|||
245
app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt
Normal file
245
app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package com.tangem.tap.routing
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.routing.configurator.AppRouterConfig
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProxyAppRouterTest {
|
||||
|
||||
private val innerRouter = mockk<Router>(relaxed = true)
|
||||
private val snackbarHandler = mockk<SnackbarHandler>(relaxed = true)
|
||||
private val analyticsExceptionHandler = mockk<AnalyticsExceptionHandler>(relaxed = true)
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val config = mockk<AppRouterConfig>(relaxed = true) {
|
||||
every { componentRouter } returns innerRouter
|
||||
every { stack } returns listOf(AppRoute.Wallet)
|
||||
every { snackbarHandler } returns this@ProxyAppRouterTest.snackbarHandler
|
||||
every { initializedState } returns MutableStateFlow(true)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(innerRouter, snackbarHandler, analyticsExceptionHandler, config)
|
||||
every { config.componentRouter } returns innerRouter
|
||||
every { config.stack } returns listOf(AppRoute.Wallet)
|
||||
every { config.snackbarHandler } returns snackbarHandler
|
||||
every { config.initializedState } returns MutableStateFlow(true)
|
||||
}
|
||||
|
||||
private fun createRouter(routerScope: CoroutineScope): ProxyAppRouter {
|
||||
every { config.routerScope } returns routerScope
|
||||
return ProxyAppRouter(config, dispatchers, analyticsExceptionHandler)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Push {
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
val route = AppRoute.AppSettings
|
||||
|
||||
// Act
|
||||
router.push(route)
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.push(route, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calls onComplete false when inner router throws`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
every { innerRouter.push(any(), any()) } throws RuntimeException("Navigation error")
|
||||
var result: Boolean? = null
|
||||
|
||||
// Act
|
||||
router.push(AppRoute.AppSettings) { result = it }
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ReplaceCurrent {
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
val route = AppRoute.AppSettings
|
||||
|
||||
// Act
|
||||
router.replaceCurrent(route)
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.replaceCurrent(route, any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ReplaceAll {
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
val route = AppRoute.Wallet
|
||||
|
||||
// Act
|
||||
router.replaceAll(route)
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.replaceAll(route, onComplete = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `catches exception silently via safeNavigate`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
every { innerRouter.replaceAll(*anyVararg(), onComplete = any()) } throws RuntimeException("Error")
|
||||
|
||||
// Act
|
||||
val actual = runCatching { router.replaceAll(AppRoute.Wallet) }.isSuccess
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isTrue()
|
||||
verify { innerRouter.replaceAll(AppRoute.Wallet, onComplete = any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Pop {
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
|
||||
// Act
|
||||
router.pop()
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.pop(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class PopTo {
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router with route`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
val route = AppRoute.Wallet
|
||||
|
||||
// Act
|
||||
router.popTo(route)
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.popTo(route, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delegates to inner router with routeClass`() = runTest {
|
||||
// Arrange
|
||||
val router = createRouter(this)
|
||||
|
||||
// Act
|
||||
router.popTo(AppRoute.Wallet::class)
|
||||
|
||||
// Assert
|
||||
verify { innerRouter.popTo(AppRoute.Wallet::class, any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class DefaultCompletionHandler {
|
||||
|
||||
@Test
|
||||
fun `does nothing on success`() {
|
||||
// Arrange
|
||||
val router = createRouter(mockk())
|
||||
|
||||
// Act
|
||||
router.defaultCompletionHandler(isSuccess = true, errorMessage = "error")
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { analyticsExceptionHandler.sendException(any()) }
|
||||
verify(exactly = 0) { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sends analytics and shows snackbar on failure`() {
|
||||
// Arrange
|
||||
val router = createRouter(mockk())
|
||||
|
||||
// Act
|
||||
router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed")
|
||||
|
||||
// Assert
|
||||
verify { analyticsExceptionHandler.sendException(any<ExceptionAnalyticsEvent>()) }
|
||||
verify { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sends analytics without snackbar when handler is null`() {
|
||||
// Arrange
|
||||
every { config.snackbarHandler } returns null
|
||||
val router = createRouter(mockk())
|
||||
|
||||
// Act
|
||||
router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed")
|
||||
|
||||
// Assert
|
||||
verify { analyticsExceptionHandler.sendException(any<ExceptionAnalyticsEvent>()) }
|
||||
verify(exactly = 0) { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Stack {
|
||||
|
||||
@Test
|
||||
fun `returns config stack`() {
|
||||
// Arrange
|
||||
val router = createRouter(mockk())
|
||||
val expected = listOf(AppRoute.Wallet)
|
||||
|
||||
// Act
|
||||
val actual = router.stack
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue