Add notification troubleshoot test about blocked users.

This commit is contained in:
Benoit Marty 2025-09-22 20:12:26 +02:00
parent a7fb213ff7
commit c4d7d42141
31 changed files with 338 additions and 30 deletions

View file

@ -22,5 +22,6 @@ interface NotificationTroubleShootEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onDone()
fun openIgnoredUsers()
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.troubleshoot.api.test
interface NotificationTroubleshootNavigator {
fun openIgnoredUsers()
}

View file

@ -16,7 +16,10 @@ interface NotificationTroubleshootTest {
fun isRelevant(data: TestFilterData): Boolean = true
suspend fun run(coroutineScope: CoroutineScope)
suspend fun reset()
suspend fun quickFix(coroutineScope: CoroutineScope) {
suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
error("Quick fix not implemented, you need to override this method in your test")
}
}

View file

@ -17,6 +17,10 @@ data class NotificationTroubleshootTestState(
data object InProgress : Status
data object WaitingForUser : Status
data object Success : Status
data class Failure(val hasQuickFix: Boolean) : Status
data class Failure(
val hasQuickFix: Boolean,
val isCritical: Boolean = true,
val quickFixButtonString: String? = null,
) : Status
}
}

View file

@ -19,6 +19,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.services.analytics.api.ScreenTracker
@ContributesNode(SessionScope::class)
@ -26,15 +27,26 @@ import io.element.android.services.analytics.api.ScreenTracker
class TroubleshootNotificationsNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: TroubleshootNotificationsPresenter,
private val screenTracker: ScreenTracker,
) : Node(buildContext, plugins = plugins) {
factory: TroubleshootNotificationsPresenter.Factory,
) : Node(buildContext, plugins = plugins),
NotificationTroubleshootNavigator {
private val presenter = factory.create(
navigator = this,
)
private fun onDone() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.onDone()
}
}
override fun openIgnoredUsers() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.openIgnoredUsers()
}
}
@Composable
override fun View(modifier: Modifier) {
screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot)

View file

@ -12,14 +12,23 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import kotlinx.coroutines.launch
@Inject
class TroubleshootNotificationsPresenter(
@Assisted private val navigator: NotificationTroubleshootNavigator,
private val troubleshootTestSuite: TroubleshootTestSuite,
) : Presenter<TroubleshootNotificationsState> {
@AssistedFactory
fun interface Factory {
fun create(navigator: NotificationTroubleshootNavigator): TroubleshootNotificationsPresenter
}
@Composable
override fun present(): TroubleshootNotificationsState {
val coroutineScope = rememberCoroutineScope()
@ -34,7 +43,11 @@ class TroubleshootNotificationsPresenter(
troubleshootTestSuite.runTestSuite(this)
}
is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch {
troubleshootTestSuite.quickFix(event.testIndex, this)
troubleshootTestSuite.quickFix(
testIndex = event.testIndex,
coroutineScope = this,
navigator = navigator,
)
}
TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch {
troubleshootTestSuite.retryFailedTest(this)

View file

@ -31,8 +31,12 @@ open class TroubleshootNotificationsStateProvider : PreviewParameterProvider<Tro
aTroubleshootNotificationsState(
listOf(
aTroubleshootTestStateSuccess(),
aTroubleshootTestStateFailure(
isCritical = false,
hasQuickFix = true,
quickFixButtonString = "Custom quick fix",
),
aTroubleshootTestStateInProgress(),
aTroubleshootTestStateIdle(),
)
),
aTroubleshootNotificationsState(
@ -106,5 +110,14 @@ fun aTroubleshootTestStateWaitingForUser() =
fun aTroubleshootTestStateSuccess() =
aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Success)
fun aTroubleshootTestStateFailure(hasQuickFix: Boolean) =
aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix))
fun aTroubleshootTestStateFailure(
hasQuickFix: Boolean = false,
isCritical: Boolean = true,
quickFixButtonString: String? = null,
) = aTroubleshootTestState(
status = NotificationTroubleshootTestState.Status.Failure(
hasQuickFix = hasQuickFix,
isCritical = isCritical,
quickFixButtonString = quickFixButtonString,
)
)

View file

@ -64,11 +64,12 @@ private fun ColumnScope.TroubleshootTestView(
testState: NotificationTroubleshootTestState,
onQuickFixClick: () -> Unit,
) {
if ((testState.status as? Status.Idle)?.visible == false) return
val status = testState.status
if ((status as? Status.Idle)?.visible == false) return
ListItem(
headlineContent = { Text(text = testState.name) },
supportingContent = { Text(text = testState.description) },
trailingContent = when (testState.status) {
trailingContent = when (status) {
is Status.Idle -> null
Status.InProgress -> ListItemContent.Custom {
CircularProgressIndicator(
@ -98,20 +99,19 @@ private fun ColumnScope.TroubleshootTestView(
Icon(
contentDescription = null,
modifier = Modifier.size(24.dp),
imageVector = CompoundIcons.ErrorSolid(),
tint = ElementTheme.colors.textCriticalPrimary
imageVector = if (status.isCritical) CompoundIcons.ErrorSolid() else CompoundIcons.Warning(),
tint = ElementTheme.colors.iconCriticalPrimary,
)
}
}
)
if ((testState.status as? Status.Failure)?.hasQuickFix == true) {
if (status is Status.Failure && status.hasQuickFix) {
ListItem(
headlineContent = {
},
headlineContent = { },
trailingContent = ListItemContent.Custom {
Button(
text = stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action),
onClick = onQuickFixClick
text = status.quickFixButtonString ?: stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action),
onClick = onQuickFixClick,
)
}
)

View file

@ -11,6 +11,7 @@ import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.NotificationTroubleshoot
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
@ -90,8 +91,12 @@ class TroubleshootTestSuite(
)
}
suspend fun quickFix(testIndex: Int, coroutineScope: CoroutineScope) {
tests[testIndex].quickFix(coroutineScope)
suspend fun quickFix(
testIndex: Int,
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
tests[testIndex].quickFix(coroutineScope, navigator)
}
}
@ -104,7 +109,7 @@ fun List<NotificationTroubleshootTestState>.computeMainState(): AsyncAction<Unit
else -> {
if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) {
AsyncAction.ConfirmingNoParams
} else if (any { it.status is NotificationTroubleshootTestState.Status.Failure }) {
} else if (any { it.status.let { status -> status is NotificationTroubleshootTestState.Status.Failure && status.isCritical } }) {
AsyncAction.Failure(Exception("Some tests failed"))
} else {
AsyncAction.Success(Unit)

View file

@ -28,12 +28,13 @@ class DefaultNotificationTroubleShootEntryPointTest {
TroubleshootNotificationsNode(
buildContext = buildContext,
plugins = plugins,
presenter = createTroubleshootNotificationsPresenter(),
factory = { createTroubleshootNotificationsPresenter() },
screenTracker = FakeScreenTracker(),
)
}
val callback = object : NotificationTroubleShootEntryPoint.Callback {
override fun onDone() = lambdaError()
override fun openIgnoredUsers() = lambdaError()
}
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
.callback(callback)

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.troubleshoot.impl
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import kotlinx.coroutines.CoroutineScope
@ -50,7 +51,10 @@ class FakeNotificationTroubleshootTest(
}
}
override suspend fun quickFix(coroutineScope: CoroutineScope) {
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
updateState(NotificationTroubleshootTestState.Status.InProgress)
quickFixAction()?.let {
_state.emit(it)

View file

@ -13,9 +13,11 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -111,9 +113,13 @@ private fun createTroubleshootTestSuite(
}
internal fun createTroubleshootNotificationsPresenter(
navigator: NotificationTroubleshootNavigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = lambdaError()
},
troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(),
): TroubleshootNotificationsPresenter {
return TroubleshootNotificationsPresenter(
navigator = navigator,
troubleshootTestSuite = troubleshootTestSuite,
)
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.troubleshoot.test"
}
dependencies {
implementation(projects.libraries.troubleshoot.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.troubleshoot.test
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotificationTroubleshootNavigator(
private val openIgnoredUsersResult: () -> Unit = { lambdaError() },
) : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
}