Merge pull request #5394 from element-hq/feature/bma/testIgnoredUser

Add troubleshhot notification test about blocked users
This commit is contained in:
Benoit Marty
2025-09-23 12:31:56 +02:00
committed by GitHub
46 changed files with 529 additions and 216 deletions

View File

@@ -93,6 +93,8 @@ allprojects {
// Fix compilation warning for annotations
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
freeCompilerArgs.add("-Xannotation-default-target=first-only")
// Opt-in to context receivers
freeCompilerArgs.add("-Xcontext-parameters")
}
}
}

View File

@@ -208,6 +208,10 @@ class PreferencesFlowNode(
navigateUp()
}
}
override fun openIgnoredUsers() {
backstack.push(NavTarget.BlockedUsers)
}
})
.build()
}

View File

@@ -43,5 +43,6 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test)
}

View File

@@ -15,6 +15,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.R
import io.element.android.libraries.permissions.impl.action.PermissionActions
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.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@@ -54,7 +55,10 @@ class NotificationTroubleshootCheckPermissionTest(
override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
// Do not bother about asking the permission inline, just lead the user to the settings
permissionActions.openSettings()
}

View File

@@ -8,11 +8,12 @@
package io.element.android.libraries.permissions.impl.troubleshoot
import android.os.Build
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.impl.action.FakePermissionActions
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
@@ -28,10 +29,7 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -47,10 +45,7 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -74,17 +69,14 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = actions,
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix
launch {
sut.quickFix(this)
backgroundScope.launch {
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
// Run the test again (IRL it will be done thanks to the resuming of the application)
sut.run(this)
}
@@ -109,13 +101,10 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = actions,
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
}

View File

@@ -72,6 +72,7 @@ dependencies {
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.features.call.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.services.appnavstate.test)

View File

@@ -44,7 +44,7 @@ class CurrentPushProviderTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.push.impl.troubleshoot
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.impl.R
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.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@ContributesIntoSet(SessionScope::class)
@Inject
class IgnoredUsersTest(
private val matrixClient: MatrixClient,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order = 80
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_description),
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val ignorerUsers = matrixClient.ignoredUsersFlow.value
if (ignorerUsers.isEmpty()) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_result_none),
status = NotificationTroubleshootTestState.Status.Success,
)
} else {
delegate.updateState(
description = stringProvider.getQuantityString(
R.plurals.troubleshoot_notifications_test_blocked_users_result_some,
ignorerUsers.size,
ignorerUsers.size
),
status = NotificationTroubleshootTestState.Status.Failure(
hasQuickFix = true,
isCritical = false,
quickFixButtonString = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_quick_fix),
),
)
}
}
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
navigator.openIgnoredUsers()
}
override suspend fun reset() = delegate.reset()
}

View File

@@ -54,7 +54,7 @@ class NotificationTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
}
@@ -81,7 +81,7 @@ class NotificationTest(
notificationDisplayer.dismissDiagnosticNotification()
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
)

View File

@@ -13,6 +13,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.impl.R
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.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@@ -56,7 +57,7 @@ class PushLoopbackTest(
val hasQuickFix = pushService.getCurrentPushProvider()?.canRotateToken() == true
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1),
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix)
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)
)
job.cancel()
return
@@ -64,7 +65,7 @@ class PushLoopbackTest(
Timber.e(e, "Failed to test push")
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
job.cancel()
return
@@ -72,7 +73,7 @@ class PushLoopbackTest(
if (!testPushResult) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
job.cancel()
return
@@ -93,13 +94,16 @@ class PushLoopbackTest(
job.cancel()
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
)
}
override suspend fun quickFix(coroutineScope: CoroutineScope) {
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
delegate.start()
pushService.getCurrentPushProvider()?.rotateToken()
run(coroutineScope)

View File

@@ -48,7 +48,7 @@ class PushProvidersTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
}

View File

@@ -54,6 +54,14 @@
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
<string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
<string name="troubleshoot_notifications_test_blocked_users_description">"Checking blocked users"</string>
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"View blocked users"</string>
<string name="troubleshoot_notifications_test_blocked_users_result_none">"No users are blocked."</string>
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
<item quantity="one">"You blocked %1$d user. You will not receive notifications for this user."</item>
<item quantity="other">"You blocked %1$d users. You will not receive notifications for these users."</item>
</plurals>
<string name="troubleshoot_notifications_test_blocked_users_title">"Blocked users"</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Get the name of the current provider."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string>

View File

@@ -7,12 +7,11 @@
package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -23,10 +22,7 @@ class CurrentPushProviderTestTest {
getCurrentPushProvider = FakeGetCurrentPushProvider("foo"),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -41,14 +37,11 @@ class CurrentPushProviderTestTest {
getCurrentPushProvider = FakeGetCurrentPushProvider(null),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
}

View File

@@ -0,0 +1,86 @@
/*
* 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.push.impl.troubleshoot
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
class IgnoredUsersTestTest {
@Test
fun `test IgnoredUsersTest order`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(),
stringProvider = FakeStringProvider(),
)
assertThat(sut.order).isEqualTo(80)
}
@Test
fun `test IgnoredUsersTest quick fix`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(),
stringProvider = FakeStringProvider(),
)
val openIgnoredUsersResult = lambdaRecorder<Unit> {}
val navigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
}
sut.quickFix(
coroutineScope = backgroundScope,
navigator = navigator,
)
openIgnoredUsersResult.assertions().isCalledOnce()
}
@Test
fun `test IgnoredUsersTest with no blocked users`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf())
),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test IgnoredUsersTest with blocked users`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2))
),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
val lastStatus = lastItem.status as NotificationTroubleshootTestState.Status.Failure
assertThat(lastStatus.hasQuickFix).isTrue()
assertThat(lastStatus.isCritical).isFalse()
assertThat(lastStatus.quickFixButtonString).isNotNull()
assertThat(lastItem.description).contains("2")
}
}
}

View File

@@ -7,14 +7,13 @@
package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -31,10 +30,7 @@ class NotificationTestTest {
fun `test NotificationTest notification cannot be displayed`() = runTest {
fakeNotificationDisplayer.displayDiagnosticNotificationResult = lambdaRecorder { _ -> false }
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java)
@@ -44,10 +40,7 @@ class NotificationTestTest {
@Test
fun `test NotificationTest user does not click on notification`() = runTest {
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)
@@ -60,10 +53,7 @@ class NotificationTestTest {
@Test
fun `test NotificationTest user clicks on notification`() = runTest {
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)

View File

@@ -7,7 +7,6 @@
package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
@@ -15,10 +14,11 @@ import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -32,14 +32,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
}
}
@@ -56,14 +53,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
}
@@ -89,17 +83,14 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
sut.quickFix(this)
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
rotateTokenLambda.assertions().isCalledOnce()
}
}
@@ -115,14 +106,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
}
}
@@ -139,14 +127,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
assertThat(lastItem.description).contains(A_FAILURE_REASON)
}
}
@@ -163,10 +148,7 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()

View File

@@ -7,12 +7,11 @@
package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -23,14 +22,11 @@ class PushProvidersTestTest {
pushProviders = emptySet(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
}
@@ -45,10 +41,7 @@ class PushProvidersTestTest {
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()

View File

@@ -76,5 +76,6 @@ dependencies {
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test)
}

View File

@@ -51,7 +51,7 @@ class FirebaseAvailabilityTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
}

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.FirebaseStore
import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.R
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.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@@ -62,7 +63,7 @@ class FirebaseTokenTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure),
status = NotificationTroubleshootTestState.Status.Failure(true)
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
)
}
}
@@ -71,7 +72,10 @@ class FirebaseTokenTest(
override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
delegate.start()
firebaseTroubleshooter.troubleshoot()
run(coroutineScope)

View File

@@ -7,14 +7,13 @@
package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -25,10 +24,7 @@ class FirebaseAvailabilityTestTest {
isPlayServiceAvailable = FakeIsPlayServiceAvailable(true),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -42,14 +38,11 @@ class FirebaseAvailabilityTestTest {
isPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
}

View File

@@ -7,15 +7,15 @@
package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -27,10 +27,7 @@ class FirebaseTokenTestTest {
firebaseTroubleshooter = FakeFirebaseTroubleshooter(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -53,16 +50,13 @@ class FirebaseTokenTestTest {
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix
sut.quickFix(this)
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
@@ -81,13 +75,10 @@ class FirebaseTokenTestTest {
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
}

View File

@@ -51,6 +51,7 @@ dependencies {
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.services.appnavstate.test)
}

View File

@@ -48,7 +48,7 @@ class UnifiedPushMatrixGatewayTest(
if (config == null) {
delegate.updateState(
description = "No current push provider",
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
} else {
val gatewayBaseUrl = config.url.removeSuffix("/_matrix/push/v1/notify")
@@ -65,13 +65,13 @@ class UnifiedPushMatrixGatewayTest(
} else {
delegate.updateState(
description = "${config.url} is not a Matrix gateway.",
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
} catch (throwable: Throwable) {
delegate.updateState(
description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}",
status = NotificationTroubleshootTestState.Status.Failure(false)
status = NotificationTroubleshootTestState.Status.Failure()
)
}
}

View File

@@ -13,6 +13,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.pushproviders.unifiedpush.R
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
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.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@@ -57,14 +58,17 @@ class UnifiedPushTest(
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure),
status = NotificationTroubleshootTestState.Status.Failure(true)
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
)
}
}
override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
openDistributorWebPageAction.execute()
}
}

View File

@@ -7,7 +7,6 @@
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig
@@ -18,8 +17,8 @@ import io.element.android.libraries.pushproviders.unifiedpush.matrixDiscoveryRes
import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -31,10 +30,7 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = matrixDiscoveryResponse,
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -48,14 +44,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = null,
discoveryResponse = matrixDiscoveryResponse,
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
}
}
@@ -65,14 +58,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = invalidDiscoveryResponse,
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
// Reset the error
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
@@ -85,14 +75,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = { error("Network error") },
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
}
}

View File

@@ -7,12 +7,13 @@
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
@@ -30,10 +31,7 @@ class UnifiedPushTestTest {
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
@@ -57,17 +55,14 @@ class UnifiedPushTestTest {
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix
launch {
sut.quickFix(this)
backgroundScope.launch {
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
sut.run(this)
}
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
@@ -91,14 +86,11 @@ class UnifiedPushTestTest {
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
}

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

@@ -60,7 +60,7 @@ class NotificationTroubleshootTestDelegate(
if (isSuccess) {
NotificationTroubleshootTestState.Status.Success
} else {
NotificationTroubleshootTestState.Status.Failure(hasQuickFix)
NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)
}
)
}

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 = false,
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

@@ -7,15 +7,15 @@
package io.element.android.libraries.troubleshoot.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
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 io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -23,9 +23,7 @@ class TroubleshootNotificationsPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createTroubleshootNotificationsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
assertThat(initialState.testSuiteState.tests).isEmpty()
assertThat(initialState.testSuiteState.mainState).isEqualTo(AsyncAction.Uninitialized)
@@ -40,9 +38,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
initialState.eventSink(TroubleshootNotificationsEvents.StartTests)
skipItems(1)
@@ -63,9 +59,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
val initialState = awaitItem()
initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests)
skipItems(1)
@@ -74,6 +68,80 @@ class TroubleshootNotificationsPresenterTest {
}
}
@Test
fun `present - critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true)
)
),
expectedIsCritical = true,
expectedMainState = AsyncAction.Failure::class.java,
)
}
@Test
fun `present - success and critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Success
),
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true)
),
),
expectedIsCritical = true,
expectedMainState = AsyncAction.Failure::class.java,
)
}
@Test
fun `present - non critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = false)
)
),
expectedIsCritical = false,
expectedMainState = AsyncAction.Success::class.java,
)
}
@Test
fun `present - waiting for user`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.WaitingForUser
)
),
expectedIsCritical = false,
expectedMainState = AsyncAction.ConfirmingNoParams::class.java,
)
}
private fun `present - check main state`(
tests: Set<NotificationTroubleshootTest>,
expectedIsCritical: Boolean,
expectedMainState: Class<out AsyncAction<*>>,
) = runTest {
val troubleshootTestSuite = createTroubleshootTestSuite(
tests = tests
)
val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite,
)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasFailedTests).isEqualTo(expectedIsCritical)
assertThat(initialState.testSuiteState.mainState).isInstanceOf(expectedMainState)
}
}
@Test
fun `present - quick fix test`() = runTest {
val troubleshootTestSuite = createTroubleshootTestSuite(
@@ -86,9 +154,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java)
@@ -111,9 +177,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,31 @@
/*
* 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)
implementation(libs.coroutines.test)
implementation(libs.test.core)
implementation(libs.test.turbine)
}
ktlint {
filter {
exclude { element ->
val path = element.file.path
// Exclude this file, that ktlint cannot parse.
path.contains("libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.kt")
}
}
}

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()
}

View File

@@ -0,0 +1,27 @@
/*
* 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.
*/
@file:Suppress("UnusedImports")
package io.element.android.libraries.troubleshoot.test
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
context(testScope: TestScope)
suspend fun NotificationTroubleshootTest.runAndTestState(
validate: suspend TurbineTestContext<NotificationTroubleshootTestState>.() -> Unit,
) {
testScope.backgroundScope.launch {
run(this)
}
state.test(validate = validate)
}

View File

@@ -123,6 +123,7 @@
"includeRegex" : [
"push_.*",
"notification_.*",
"troubleshoot_notifications\\.test_blocked_users\\..*",
"troubleshoot_notifications_test_current_push_provider.*",
"troubleshoot_notifications_test_detect_push_provider.*",
"troubleshoot_notifications_test_display_notification_.*",