Issue#41 Attempt to fix failing tests with compose + update test paths for artifact

This commit is contained in:
Gergely Hegedus 2022-09-28 15:47:43 +03:00
parent 60cfb46ccf
commit f03c9f7bf2
16 changed files with 227 additions and 159 deletions

View file

@ -75,7 +75,9 @@ jobs:
if: always()
with:
name: JVM Test Results
path: ./**/build/reports/tests/**/*.html
path: |
./**/build/reports/tests/**/*.html
./**/**/build/reports/tests/**/*.html
retention-days: 1
run-tests-on-emulator:
@ -126,7 +128,9 @@ jobs:
if: always()
with:
name: Emulator-Test-Results-${{ matrix.api-level }}
path: ./**/build/reports/androidTests/**/*.html
path: |
./**/build/reports/androidTests/**/*.html
./**/**/build/reports/androidTests/**/*.html
retention-days: 1
- name: Upload Test Screenshots
uses: actions/upload-artifact@v2

View file

@ -1,21 +1,18 @@
package org.fnives.test.showcase.ui
import androidx.compose.ui.test.MainTestClock
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.R
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
import org.fnives.test.showcase.android.testutil.viewaction.LoopMainThreadFor
import org.fnives.test.showcase.compose.screen.AppNavigation
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.DatabaseDispatcherTestRule
import org.fnives.test.showcase.ui.compose.idle.ComposeNetworkSynchronizationTestRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -29,7 +26,9 @@ class AuthComposeInstrumentedTest : KoinTest {
private val composeTestRule = createComposeRule()
private val stateRestorationTester = StateRestorationTester(composeTestRule)
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule(
networkSynchronizationTestRule = ComposeNetworkSynchronizationTestRule(composeTestRule)
)
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val dispatcherTestRule = DatabaseDispatcherTestRule()
private lateinit var robot: ComposeLoginRobot
@ -72,7 +71,7 @@ class AuthComposeInstrumentedTest : KoinTest {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
navigationRobot.assertHomeScreen()
}
@ -86,7 +85,7 @@ class AuthComposeInstrumentedTest : KoinTest {
.assertUsername("banan")
.clickOnLogin()
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -103,7 +102,7 @@ class AuthComposeInstrumentedTest : KoinTest {
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -129,7 +128,7 @@ class AuthComposeInstrumentedTest : KoinTest {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.credentials_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -155,7 +154,7 @@ class AuthComposeInstrumentedTest : KoinTest {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -181,15 +180,5 @@ class AuthComposeInstrumentedTest : KoinTest {
companion object {
private const val SPLASH_DELAY = 600L
// workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807
/**
* Await the idling resource on a different thread while looping main.
*/
fun MainTestClock.awaitIdlingResources() {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(100L))
advanceTimeByFrame()
}
}
}

View file

@ -1,10 +1,10 @@
package org.fnives.test.showcase.ui
import android.content.Context
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
@ -13,8 +13,8 @@ import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.compose.screen.auth.AuthScreenTag
class ComposeLoginRobot(
composeTestRule: ComposeTestRule,
) : ComposeTestRule by composeTestRule {
semanticsNodeInteractionsProvider: SemanticsNodeInteractionsProvider,
) : SemanticsNodeInteractionsProvider by semanticsNodeInteractionsProvider {
fun setUsername(username: String): ComposeLoginRobot = apply {
onNodeWithTag(AuthScreenTag.UsernameInput).performTextInput(username)

View file

@ -0,0 +1,22 @@
package org.fnives.test.showcase.ui.compose.idle
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.junit4.ComposeTestRule
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
class ComposeIdlingDisposable(
private val idlingResource: IdlingResource,
private val testRule: ComposeTestRule,
) : Disposable {
override var isDisposed: Boolean = false
private set
init {
testRule.registerIdlingResource(idlingResource)
}
override fun dispose() {
isDisposed = true
testRule.unregisterIdlingResource(idlingResource)
}
}

View file

@ -0,0 +1,50 @@
package org.fnives.test.showcase.ui.compose.idle
import android.util.Log
import androidx.annotation.CheckResult
import androidx.compose.ui.test.junit4.ComposeTestRule
import okhttp3.OkHttpClient
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource
import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.koin.test.KoinTest
class ComposeNetworkSynchronizationTestRule(private val composeTestRule: ComposeTestRule) : TestRule, KoinTest {
private var disposable: Disposable? = null
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
disposable = registerIdlingResources()
try {
base.evaluate()
} finally {
dispose()
}
}
}
}
fun dispose() {
if (disposable == null) {
Log.w("ComposeNetworkSynchronizationTestRule", "Was disposed, but registerIdlingResources was not called!")
}
disposable?.dispose()
}
@CheckResult
private fun registerIdlingResources(): Disposable = getOkHttpClients()
.associateBy(keySelector = { it.toString() })
.map { (key, client) -> OkHttp3IdlingResource.create(key, client) }
.map(::EspressoToComposeIdlingResourceAdapter)
.map { ComposeIdlingDisposable(it, composeTestRule) }
.let(::CompositeDisposable)
private fun getOkHttpClients(): List<OkHttpClient> =
NetworkTestConfigurationHelper.getOkHttpClients()
}

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.compose.idle
import androidx.test.espresso.IdlingResource
class EspressoToComposeIdlingResourceAdapter(private val idlingResource: IdlingResource) : androidx.compose.ui.test.IdlingResource {
override val isIdleNow: Boolean get() = idlingResource.isIdleNow
}

View file

@ -22,8 +22,8 @@ Here is a list of actions we want to do:
```kotlin
class ComposeLoginRobot(
composeTestRule: ComposeTestRule,
) : ComposeTestRule by composeTestRule {
semanticsNodeInteractionsProvider: SemanticsNodeInteractionsProvider,
) : SemanticsNodeInteractionsProvider by semanticsNodeInteractionsProvider {
fun setUsername(username: String): ComposeLoginRobot = apply {
onNodeWithTag(AuthScreenTag.UsernameInput).performTextInput(username)
@ -61,9 +61,11 @@ class ComposeLoginRobot(
```
While in the View system we're using Espresso to interact with views,
in Compose we need a reference to the `ComposeTestRule` that contains our UI,
in Compose we need a reference to the `SemanticsNodeInteractionsProvider` that contains our UI,
which we will pass as a constructor parameter to the robot.
> SemanticsNodeInteractionsProvider gives access to `onNode` actions. ComposeTestRule extends it.
To create a `ComposeTestRule` you simply need to:
```kotlin
@ -152,10 +154,14 @@ fun setup() {
Network synchronization and mocking is the same as for View.
```kotlin
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule(
networkSynchronizationTestRule = ComposeNetworkSynchronizationTestRule(composeTestRule)
)
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
```
> ComposeNetworkSynchronizationTestRule is an equivalent to NetworkSynchronizationTestRule just registering the IdlingResource to ComposeTestRule instead of Espresso
Coroutine setup is the same, except for `Dispatchers.setMain(dispatcher)`, which we don't need.
```kotlin
@ -214,12 +220,13 @@ composeTestRule.mainClock.autoAdvance = true // Let clock auto advance again
Lastly we check the navigation was correct, meaning we should be on the home screen:
```kotlin
composeTestRule.mainClock.awaitIdlingResources() // wait for login network call idling resource
composeTestRule.waitForIdle() // wait for login network call idling resource
navigationRobot.assertHomeScreen()
```
> `awaitIdlingResources` is an extension function to await all idling resources.
> Note: Considering what the docs say this shouldn't be necessarily if the idling resources are setup in Espresso, since the compose test rule is aware of espresso and it waits for idle before every finder. In practice it only works with the line above. Could be a bug somewhere.
> waitForIdle is necessary to wait for the Coroutine then the Network Call to finish. The Network call is running on OkHttps's own thread, so we use IdlingResources to synchronize with it. This is done in the ComposeNetworkSynchronizationTestRule.
> waitForIdle blocks the current thread while the Resources are busy. There is an alternative awaitIdle() which can be useful in runTest suspendable tests, feel free to look inside the Interface of ComposeTestRule.
> Basically since we have OkHttpIdlingResource as an EspressoIdlingResource we adapt that to Compose's IdlingResource class and register it with the ComposeTestRule and unregister it at the end.
### 2. `emptyPasswordShowsProperErrorMessage`
@ -240,7 +247,7 @@ robot.setUsername("banan")
Finally we let coroutines go and verify the error is shown and we have not navigated:
```kotlin
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -260,7 +267,7 @@ robot
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -293,7 +300,7 @@ composeTestRule.mainClock.autoAdvance = true
Now at the end verify the error is shown properly:
```kotlin
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.credentials_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -323,7 +330,7 @@ composeTestRule.mainClock.advanceTimeByFrame()
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotLoading()
navigationRobot.assertAuthScreen()

View file

@ -1,31 +1,31 @@
package org.fnives.test.showcase.hilt.ui.compose
import androidx.compose.ui.test.MainTestClock
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.espresso.Espresso
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
import org.fnives.test.showcase.android.testutil.viewaction.LoopMainThreadFor
import org.fnives.test.showcase.hilt.R
import org.fnives.test.showcase.hilt.compose.screen.AppNavigation
import org.fnives.test.showcase.hilt.core.integration.fake.FakeUserDataLocalStorage
import org.fnives.test.showcase.hilt.di.TestUserDataLocalStorageModule
import org.fnives.test.showcase.hilt.test.shared.testutils.MockServerScenarioSetupTestRule
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.DatabaseDispatcherTestRule
import org.fnives.test.showcase.hilt.test.shared.ui.NetworkSynchronizedActivityTest
import org.fnives.test.showcase.hilt.ui.compose.idle.ComposeNetworkSyncHelper
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
class AuthComposeInstrumentedTest {
private val composeTestRule = createComposeRule()
private val stateRestorationTester = StateRestorationTester(composeTestRule)
@ -36,6 +36,12 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
private lateinit var robot: ComposeLoginRobot
private lateinit var navigationRobot: ComposeNavigationRobot
@Inject
lateinit var composeNetworkSyncHelper: ComposeNetworkSyncHelper
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
@ -44,16 +50,22 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
.around(composeTestRule)
.around(ScreenshotRule("test-showcase-compose"))
override fun setupBeforeInjection() {
@Before
fun setup() {
TestUserDataLocalStorageModule.replacement = FakeUserDataLocalStorage()
}
hiltRule.inject()
override fun setupAfterInjection() {
stateRestorationTester.setContent {
AppNavigation()
}
robot = ComposeLoginRobot(composeTestRule)
navigationRobot = ComposeNavigationRobot(composeTestRule)
composeNetworkSyncHelper.setup(composeTestRule)
}
@After
fun tearDown() {
composeNetworkSyncHelper.tearDown()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@ -76,11 +88,10 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
navigationRobot.assertHomeScreen()
}
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
@Test
fun emptyPasswordShowsProperErrorMessage() {
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
@ -90,7 +101,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
.assertUsername("banan")
.clickOnLogin()
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -107,7 +118,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
.assertPassword("banan")
.clickOnLogin()
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -133,7 +144,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.credentials_invalid)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -159,7 +170,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
composeTestRule.mainClock.awaitIdlingResources()
composeTestRule.waitForIdle()
robot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotLoading()
navigationRobot.assertAuthScreen()
@ -185,15 +196,5 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
companion object {
private const val SPLASH_DELAY = 600L
// workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807
/**
* Await the idling resource on a different thread while looping main.
*/
fun MainTestClock.awaitIdlingResources() {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(100L))
advanceTimeByFrame()
}
}
}

View file

@ -1,10 +1,10 @@
package org.fnives.test.showcase.hilt.ui.compose
import android.content.Context
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
@ -13,8 +13,8 @@ import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.hilt.compose.screen.auth.AuthScreenTag
class ComposeLoginRobot(
composeTestRule: ComposeTestRule,
) : ComposeTestRule by composeTestRule {
semanticsNodeInteractionsProvider: SemanticsNodeInteractionsProvider,
) : SemanticsNodeInteractionsProvider by semanticsNodeInteractionsProvider {
fun setUsername(username: String): ComposeLoginRobot = apply {
onNodeWithTag(AuthScreenTag.UsernameInput).performTextInput(username)

View file

@ -0,0 +1,22 @@
package org.fnives.test.showcase.hilt.ui.compose.idle
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.junit4.ComposeTestRule
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
class ComposeIdlingDisposable(
private val idlingResource: IdlingResource,
private val testRule: ComposeTestRule,
) : Disposable {
override var isDisposed: Boolean = false
private set
init {
testRule.registerIdlingResource(idlingResource)
}
override fun dispose() {
isDisposed = true
testRule.unregisterIdlingResource(idlingResource)
}
}

View file

@ -0,0 +1,29 @@
package org.fnives.test.showcase.hilt.ui.compose.idle
import android.util.Log
import androidx.compose.ui.test.junit4.ComposeTestRule
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
import org.fnives.test.showcase.hilt.network.testutil.NetworkSynchronization
import javax.inject.Inject
class ComposeNetworkSyncHelper @Inject constructor(
private val networkSynchronization: NetworkSynchronization,
) {
private var disposable: Disposable? = null
fun setup(composeTestRule: ComposeTestRule) {
disposable = networkSynchronization.networkIdlingResources()
.map(::EspressoToComposeIdlingResourceAdapter)
.map { ComposeIdlingDisposable(it, composeTestRule) }
.let(::CompositeDisposable)
}
fun tearDown() {
if (disposable == null) {
Log.w("ComposeNetworkSyncHelper", "tearDown called, but setup wasn't!")
}
disposable?.dispose()
}
}

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.hilt.ui.compose.idle
import androidx.test.espresso.IdlingResource
class EspressoToComposeIdlingResourceAdapter(private val idlingResource: IdlingResource) : androidx.compose.ui.test.IdlingResource {
override val isIdleNow: Boolean get() = idlingResource.isIdleNow
}

View file

@ -40,4 +40,5 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation project(':mockserver')
implementation "androidx.test.espresso:espresso-core:$espresso_version"
implementation project(":test-util-android")
}

View file

@ -3,6 +3,7 @@ package org.fnives.test.showcase.hilt.network.testutil
import androidx.annotation.CheckResult
import androidx.test.espresso.IdlingResource
import okhttp3.OkHttpClient
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource
import org.fnives.test.showcase.hilt.network.di.SessionLessQualifier
import org.fnives.test.showcase.hilt.network.di.SessionQualifier
import javax.inject.Inject

View file

@ -1,76 +0,0 @@
package org.fnives.test.showcase.hilt.network.testutil
import androidx.annotation.CheckResult
import androidx.annotation.NonNull
import androidx.test.espresso.IdlingResource
import okhttp3.Dispatcher
import okhttp3.OkHttpClient
/**
* AndroidX version of Jake Wharton's OkHttp3IdlingResource.
*
* Reference: https://github.com/JakeWharton/okhttp-idling-resource/blob/master/src/main/java/com/jakewharton/espresso/OkHttp3IdlingResource.java
*/
class OkHttp3IdlingResource private constructor(
private val name: String,
private val dispatcher: Dispatcher
) : IdlingResource {
@Volatile
var callback: IdlingResource.ResourceCallback? = null
private var isIdleCallbackWasCalled: Boolean = true
init {
val currentCallback = dispatcher.idleCallback
dispatcher.idleCallback = Runnable {
sleepForDispatcherDefaultCallInRetrofitErrorState()
callback?.onTransitionToIdle()
currentCallback?.run()
isIdleCallbackWasCalled = true
}
}
override fun getName(): String = name
override fun isIdleNow(): Boolean {
val isIdle = dispatcher.runningCallsCount() == 0
if (isIdle) {
// sometime the callback is just not properly called it seems, or maybe sync error.
// if it isn't called Espresso crashes, so we add this here.
callback?.onTransitionToIdle()
}
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
companion object {
/**
* Create a new [IdlingResource] from `client` as `name`. You must register
* this instance using `Espresso.registerIdlingResources`.
*/
@CheckResult
@NonNull
fun create(@NonNull name: String?, @NonNull client: OkHttpClient?): OkHttp3IdlingResource {
if (name == null) throw NullPointerException("name == null")
if (client == null) throw NullPointerException("client == null")
return OkHttp3IdlingResource(name, client.dispatcher)
}
/**
* This is required, because in case of Errors Retrofit uses Dispatcher.Default to suspendThrow
* see: retrofit2.KotlinExtensions.kt Exception.suspendAndThrow
* Relevant code issue: https://github.com/square/retrofit/blob/6cd6f7d8287f73909614cb7300fcde05f5719750/retrofit/src/main/java/retrofit2/KotlinExtensions.kt#L121
* This is the current suggested approach to their problem with Unchecked Exceptions
*
* Sadly Dispatcher.Default cannot be replaced yet, so we can't swap it out in tests:
* https://github.com/Kotlin/kotlinx.coroutines/issues/1365
*
* This brings us to this sleep for now.
*/
fun sleepForDispatcherDefaultCallInRetrofitErrorState() {
Thread.sleep(200L)
}
}
}

View file

@ -17,28 +17,32 @@ class OkHttp3IdlingResource private constructor(
) : IdlingResource {
@Volatile
var callback: IdlingResource.ResourceCallback? = null
@Volatile
private var isIdleCallbackWasCalled: Boolean = true
private val idleSync = Any()
init {
val currentCallback = dispatcher.idleCallback
dispatcher.idleCallback = Runnable {
synchronized(idleSync) {
sleepForDispatcherDefaultCallInRetrofitErrorState()
callback?.onTransitionToIdle()
currentCallback?.run()
isIdleCallbackWasCalled = true
}
}
}
override fun getName(): String = name
override fun isIdleNow(): Boolean {
override fun isIdleNow(): Boolean =
synchronized(idleSync) {
val isIdle = dispatcher.runningCallsCount() == 0
if (isIdle) {
// sometime the callback is just not properly called it seems, or maybe sync error.
// if it isn't called Espresso crashes, so we add this here.
callback?.onTransitionToIdle()
if (!isIdle) {
isIdleCallbackWasCalled = false
}
return isIdle
return@synchronized isIdle && isIdleCallbackWasCalled
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {