diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__OneTimeDonations.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__OneTimeDonations.kt index 45acd4724a..90c7bb3123 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__OneTimeDonations.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__OneTimeDonations.kt @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.subscription.permits.DonationPermits import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.testing.DisableAnimationsRule import org.thoughtcrime.securesms.testing.GooglePayTestRule import org.thoughtcrime.securesms.testing.InAppPaymentsRule import org.thoughtcrime.securesms.testing.RxTestSchedulerRule @@ -54,6 +55,9 @@ class CheckoutFlowActivityTest__OneTimeDonations { @get:Rule val composeRule = createEmptyComposeRule() + @get:Rule + val animationsRule = DisableAnimationsRule() + private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION) @Before diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt index 803e703916..a6bb9f7737 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.testing.DisableAnimationsRule import org.thoughtcrime.securesms.testing.GooglePayTestRule import org.thoughtcrime.securesms.testing.InAppPaymentsRule import org.thoughtcrime.securesms.testing.RxTestSchedulerRule @@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__RecurringDonations { @get:Rule val composeRule = createEmptyComposeRule() + @get:Rule + val animationsRule = DisableAnimationsRule() + private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION) @Before diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__Redemption.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__Redemption.kt index ac7f58f5f0..7df01ce4f4 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__Redemption.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__Redemption.kt @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord +import org.thoughtcrime.securesms.testing.DisableAnimationsRule import org.thoughtcrime.securesms.testing.GooglePayTestRule import org.thoughtcrime.securesms.testing.InAppPaymentsRule import org.thoughtcrime.securesms.testing.RxTestSchedulerRule @@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__Redemption { @get:Rule val composeRule = createEmptyComposeRule() + @get:Rule + val animationsRule = DisableAnimationsRule() + private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION) @Before diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationsCheckoutSpecTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationsCheckoutSpecTest.kt index e407ca6c62..27605d04ea 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationsCheckoutSpecTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationsCheckoutSpecTest.kt @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord +import org.thoughtcrime.securesms.testing.DisableAnimationsRule import org.thoughtcrime.securesms.testing.GooglePayTestRule import org.thoughtcrime.securesms.testing.InAppPaymentsRule import org.thoughtcrime.securesms.testing.RawFlag @@ -69,6 +70,9 @@ class DonationsCheckoutSpecTest(private val spec: DonationsCheckoutTestSpec) { @get:Rule val composeRule = createEmptyComposeRule() + @get:Rule + val animationsRule = DisableAnimationsRule() + @Before fun setUp() { SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/DisableAnimationsRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/DisableAnimationsRule.kt new file mode 100644 index 0000000000..0fb966d066 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/DisableAnimationsRule.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.testing + +import android.provider.Settings +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.ExternalResource +import java.io.FileInputStream + +/** + * Disables system animation scales for the duration of a test and restores them afterward. + * + * Espresso and [android.app.Instrumentation.waitForIdleSync] only make progress once the main looper is idle. An + * on-screen indeterminate animation (e.g. the checkout's `CircularProgressIndicator`) posts frame callbacks forever, so + * the looper never idles and any idle-gated wait hangs indefinitely rather than timing out. Forcing the scales to 0 + * stops those animations so the looper can idle. + * + * Writes go through the shell (`UiAutomation`), which holds `WRITE_SECURE_SETTINGS`; the app process does not. + */ +class DisableAnimationsRule : ExternalResource() { + + private val scales = listOf( + Settings.Global.WINDOW_ANIMATION_SCALE, + Settings.Global.TRANSITION_ANIMATION_SCALE, + Settings.Global.ANIMATOR_DURATION_SCALE + ) + + private lateinit var previous: Map + + override fun before() { + val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver + previous = scales.associateWith { Settings.Global.getFloat(resolver, it, 1f) } + scales.forEach { putScale(it, 0f) } + } + + override fun after() { + if (::previous.isInitialized) { + previous.forEach { (key, value) -> putScale(key, value) } + } + } + + private fun putScale(key: String, value: Float) { + val stream = InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("settings put global $key $value") + FileInputStream(stream.fileDescriptor).use { it.readBytes() } + } +} diff --git a/app/src/testShared/org/thoughtcrime/securesms/testing/endpoints/ResponderWebSocketConnection.kt b/app/src/testShared/org/thoughtcrime/securesms/testing/endpoints/ResponderWebSocketConnection.kt index e76f544080..e5d8222ed3 100644 --- a/app/src/testShared/org/thoughtcrime/securesms/testing/endpoints/ResponderWebSocketConnection.kt +++ b/app/src/testShared/org/thoughtcrime/securesms/testing/endpoints/ResponderWebSocketConnection.kt @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.testing.endpoints import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.subjects.BehaviorSubject +import org.signal.libsignal.net.ChatConnection import org.signal.network.websocket.WebSocketRequestMessage import org.signal.network.websocket.WebSocketResponseMessage import org.signal.network.websocket.WebsocketResponse @@ -99,6 +100,10 @@ class ResponderWebSocketConnection( override fun sendResponse(response: WebSocketResponseMessage) = Unit + override suspend fun runWithChatConnection(callback: (ChatConnection) -> T): T { + throw IOException("Responder does not provide a libsignal chat connection") + } + private fun parseHeaders(headers: List): Map { return headers.mapNotNull { header -> val separator = header.indexOf(':')