Issue#41 Attempt to fix failing tests with compose + update test paths for artifact
This commit is contained in:
parent
60cfb46ccf
commit
f03c9f7bf2
16 changed files with 227 additions and 159 deletions
8
.github/workflows/pull-request-jobs.yml
vendored
8
.github/workflows/pull-request-jobs.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,29 +17,33 @@ 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 {
|
||||
sleepForDispatcherDefaultCallInRetrofitErrorState()
|
||||
callback?.onTransitionToIdle()
|
||||
currentCallback?.run()
|
||||
isIdleCallbackWasCalled = true
|
||||
synchronized(idleSync) {
|
||||
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()
|
||||
override fun isIdleNow(): Boolean =
|
||||
synchronized(idleSync) {
|
||||
val isIdle = dispatcher.runningCallsCount() == 0
|
||||
if (!isIdle) {
|
||||
isIdleCallbackWasCalled = false
|
||||
}
|
||||
|
||||
return@synchronized isIdle && isIdleCallbackWasCalled
|
||||
}
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.callback = callback
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue