Issue#41 Copy full example into separate module with Hilt Integration
This commit is contained in:
parent
69e76dc0da
commit
52a99a82fc
229 changed files with 8416 additions and 11 deletions
1
hilt/hilt-app-shared-test/.gitignore
vendored
Normal file
1
hilt/hilt-app-shared-test/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
49
hilt/hilt-app-shared-test/build.gradle
Normal file
49
hilt/hilt-app-shared-test/build.gradle
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk 31
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
assets.srcDirs += files("$projectDir/../hilt-app/schemas".toString())
|
||||
resources.srcDirs += files("$projectDir/../hilt-app/schemas".toString())
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
// since it itself contains the Test it doesn't have tests of it's own
|
||||
disableTestTasks(this)
|
||||
|
||||
dependencies {
|
||||
implementation project(":hilt:hilt-app")
|
||||
implementation project(':test-util-android')
|
||||
implementation testFixtures(project(':hilt:hilt-core'))
|
||||
implementation "com.google.dagger:hilt-android-testing:$hilt_version"
|
||||
implementation project(':test-util-shared-robolectric')
|
||||
api project(':hilt:hilt-network-di-test-util')
|
||||
applyAppSharedTestDependenciesTo(this)
|
||||
}
|
||||
0
hilt/hilt-app-shared-test/consumer-rules.pro
Normal file
0
hilt/hilt-app-shared-test/consumer-rules.pro
Normal file
21
hilt/hilt-app-shared-test/proguard-rules.pro
vendored
Normal file
21
hilt/hilt-app-shared-test/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
5
hilt/hilt-app-shared-test/src/main/AndroidManifest.xml
Normal file
5
hilt/hilt-app-shared-test/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fnives.test.showcase.hilt.test.shared">
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.di
|
||||
|
||||
import org.fnives.test.showcase.hilt.BuildConfig
|
||||
|
||||
object TestBaseUrlHolder {
|
||||
|
||||
var url = BuildConfig.BASE_URL
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.storage.migration
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.android.testutil.SharedMigrationTestRule
|
||||
import org.fnives.test.showcase.hilt.storage.LocalDatabase
|
||||
import org.fnives.test.showcase.hilt.storage.favourite.FavouriteEntity
|
||||
import org.fnives.test.showcase.hilt.storage.migation.Migration1To2
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* reference:
|
||||
* https://medium.com/androiddevelopers/testing-room-migrations-be93cdb0d975
|
||||
* https://developer.android.com/training/data-storage/room/migrating-db-versions
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
open class MigrationToLatestInstrumentedSharedTest {
|
||||
|
||||
@get:Rule
|
||||
val helper = SharedMigrationTestRule<LocalDatabase>(instrumentation = InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
private fun getMigratedRoomDatabase(): LocalDatabase {
|
||||
val database: LocalDatabase = Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
LocalDatabase::class.java,
|
||||
TEST_DB
|
||||
)
|
||||
.addMigrations(Migration1To2())
|
||||
.build()
|
||||
// close the database and release any stream resources when the test finishes
|
||||
helper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
open fun migrate1To2() {
|
||||
val expectedEntities = setOf(
|
||||
FavouriteEntity("123"),
|
||||
FavouriteEntity("124"),
|
||||
FavouriteEntity("125")
|
||||
)
|
||||
val version1DB = helper.createDatabase(
|
||||
name = TEST_DB,
|
||||
version = 1
|
||||
)
|
||||
version1DB.run {
|
||||
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (\"123\")")
|
||||
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (124)")
|
||||
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (125)")
|
||||
}
|
||||
version1DB.close()
|
||||
|
||||
val version2DB = helper.runMigrationsAndValidate(
|
||||
name = TEST_DB,
|
||||
version = 2,
|
||||
validateDroppedTables = true,
|
||||
Migration1To2()
|
||||
)
|
||||
version2DB.close()
|
||||
|
||||
val favouriteDao = getMigratedRoomDatabase().favouriteDao
|
||||
|
||||
val entities = runBlocking { favouriteDao.get().first() }.toSet()
|
||||
|
||||
Assert.assertEquals(expectedEntities, entities)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEST_DB = "migration-test"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.testutil.HttpsConfigurationModuleTemplate
|
||||
import org.fnives.test.showcase.hilt.test.shared.di.TestBaseUrlHolder
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
class MockServerScenarioSetupTestRule : TestRule {
|
||||
|
||||
lateinit var mockServerScenarioSetup: MockServerScenarioSetup
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement = createStatement(base)
|
||||
|
||||
private fun createStatement(base: Statement) = object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
before()
|
||||
try {
|
||||
base.evaluate()
|
||||
} finally {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun before() {
|
||||
val (mockServerScenarioSetup, url) = HttpsConfigurationModuleTemplate.startWithHTTPSMockWebServer()
|
||||
TestBaseUrlHolder.url = url
|
||||
this.mockServerScenarioSetup = mockServerScenarioSetup
|
||||
}
|
||||
|
||||
private fun after() {
|
||||
mockServerScenarioSetup.stop()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.idling
|
||||
|
||||
import org.fnives.test.showcase.hilt.ui.shared.executor.AsyncTaskExecutor
|
||||
import org.fnives.test.showcase.hilt.ui.shared.executor.TaskExecutor
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* Similar Test Rule to InstantTaskExecutorRule just for the [AsyncTaskExecutor] to make AsyncDiffUtil synchronized.
|
||||
*/
|
||||
class AsyncDiffUtilInstantTestRule : TestRule {
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
AsyncTaskExecutor.delegate = object : TaskExecutor {
|
||||
override fun executeOnDiskIO(runnable: Runnable) {
|
||||
runnable.run()
|
||||
}
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
base.evaluate()
|
||||
|
||||
AsyncTaskExecutor.delegate = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.idling
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceNotIdle
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitIdlingResources
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.runOnUIAwaitOnCurrent
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.storage.TestDatabaseInitialization
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseDispatcherTestRule : TestRule {
|
||||
|
||||
lateinit var testDispatcher: TestDispatcher
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
val dispatcher = StandardTestDispatcher()
|
||||
testDispatcher = dispatcher
|
||||
TestDatabaseInitialization.dispatcher = dispatcher
|
||||
base.evaluate()
|
||||
}
|
||||
}
|
||||
|
||||
fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent {
|
||||
testDispatcher.advanceUntilIdleWithIdlingResources()
|
||||
}
|
||||
|
||||
fun advanceUntilIdle() = runOnUIAwaitOnCurrent {
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
|
||||
fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent {
|
||||
testDispatcher.scheduler.advanceTimeBy(delayInMillis)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun TestDispatcher.advanceUntilIdleWithIdlingResources() {
|
||||
scheduler.advanceUntilIdle() // advance until a request is sent
|
||||
while (anyResourceNotIdle()) { // check if any request is in progress
|
||||
awaitIdlingResources() // complete all requests and other idling resources
|
||||
scheduler.advanceUntilIdle() // run coroutines after request is finished
|
||||
}
|
||||
scheduler.advanceUntilIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.idling
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.storage.TestDatabaseInitialization
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule as LibMainDispatcherTestRule
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherTestRule(useStandard: Boolean = true) : LibMainDispatcherTestRule(useStandard) {
|
||||
|
||||
override fun onTestDispatcherInitialized(testDispatcher: TestDispatcher) {
|
||||
TestDatabaseInitialization.dispatcher = testDispatcher
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.idling
|
||||
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.IdlingResourceDisposable
|
||||
import org.fnives.test.showcase.hilt.network.testutil.NetworkSynchronization
|
||||
import javax.inject.Inject
|
||||
|
||||
class NetworkSynchronizationHelper @Inject constructor(private val networkSynchronization: NetworkSynchronization) {
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
fun setup() {
|
||||
networkSynchronization.networkIdlingResources().map {
|
||||
IdlingResourceDisposable(it)
|
||||
}.forEach {
|
||||
compositeDisposable.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.statesetup
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.runner.intent.IntentStubberRegistry
|
||||
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.MainDispatcherTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.auth.LoginRobot
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.home.HomeRobot
|
||||
import org.fnives.test.showcase.hilt.ui.auth.AuthActivity
|
||||
import org.fnives.test.showcase.hilt.ui.home.MainActivity
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||
import org.koin.test.KoinTest
|
||||
|
||||
object SetupAuthenticationState : KoinTest {
|
||||
|
||||
fun setupLogin(
|
||||
mainDispatcherTestRule: MainDispatcherTestRule,
|
||||
mockServerScenarioSetup: MockServerScenarioSetup,
|
||||
resetIntents: Boolean = true,
|
||||
) {
|
||||
resetIntentsIfNeeded(resetIntents) {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"))
|
||||
val activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
val loginRobot = LoginRobot()
|
||||
loginRobot.setupIntentResults()
|
||||
loginRobot
|
||||
.setPassword("b")
|
||||
.setUsername("a")
|
||||
.clickOnLogin()
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
activityScenario.safeClose()
|
||||
}
|
||||
}
|
||||
|
||||
fun setupLogout(
|
||||
mainDispatcherTestRule: MainDispatcherTestRule,
|
||||
resetIntents: Boolean = true,
|
||||
) {
|
||||
resetIntentsIfNeeded(resetIntents) {
|
||||
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
val homeRobot = HomeRobot()
|
||||
homeRobot.setupIntentResults()
|
||||
homeRobot.clickSignOut()
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
activityScenario.safeClose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetIntentsIfNeeded(resetIntents: Boolean, action: () -> Unit) {
|
||||
val wasInitialized = IntentStubberRegistry.isLoaded()
|
||||
if (!wasInitialized) {
|
||||
Intents.init()
|
||||
}
|
||||
action()
|
||||
Intents.release()
|
||||
if (resetIntents && wasInitialized) {
|
||||
Intents.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.testutils.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import org.fnives.test.showcase.hilt.di.StorageModule
|
||||
import org.fnives.test.showcase.hilt.storage.LocalDatabase
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Reloads the Database module, so it uses the inMemory database with the switched out Executors.
|
||||
*
|
||||
* This is needed so in AndroidTests not a real File based device is used.
|
||||
* This speeds tests up, and isolates them better, there will be no junk in the Database file from previous tests.
|
||||
*/
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [StorageModule::class]
|
||||
)
|
||||
object TestDatabaseInitialization {
|
||||
|
||||
var dispatcher: CoroutineDispatcher? = null
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private val _dispatcher: CoroutineDispatcher
|
||||
get() = dispatcher ?: throw IllegalStateException("TestDispatcher is not initialized")
|
||||
|
||||
fun create(context: Context, dispatcher: CoroutineDispatcher = this._dispatcher): LocalDatabase {
|
||||
val executor = dispatcher.asExecutor()
|
||||
return Room.inMemoryDatabaseBuilder(context, LocalDatabase::class.java)
|
||||
.setTransactionExecutor(executor)
|
||||
.setQueryExecutor(executor)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLocalDatabase(@ApplicationContext context: Context): LocalDatabase =
|
||||
create(context)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.NetworkSynchronizationHelper
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import javax.inject.Inject
|
||||
|
||||
open class NetworkSynchronizedActivityTest {
|
||||
|
||||
@Inject
|
||||
lateinit var networkSynchronizationHelper: NetworkSynchronizationHelper
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setupBeforeInjection()
|
||||
|
||||
hiltRule.inject()
|
||||
networkSynchronizationHelper.setup()
|
||||
setupAfterInjection()
|
||||
}
|
||||
|
||||
open fun setupBeforeInjection() {
|
||||
}
|
||||
|
||||
open fun setupAfterInjection() {
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
networkSynchronizationHelper.dispose()
|
||||
additionalTearDown()
|
||||
}
|
||||
|
||||
open fun additionalTearDown() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.auth
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
|
||||
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||
import org.fnives.test.showcase.hilt.R
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.MockServerScenarioSetupTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.MainDispatcherTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.NetworkSynchronizedActivityTest
|
||||
import org.fnives.test.showcase.hilt.ui.auth.AuthActivity
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
open class AuthActivityInstrumentedSharedTest : NetworkSynchronizedActivityTest() {
|
||||
|
||||
private lateinit var activityScenario: ActivityScenario<AuthActivity>
|
||||
|
||||
private val mainDispatcherTestRule = MainDispatcherTestRule()
|
||||
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
|
||||
private val mockServerScenarioSetup: MockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
|
||||
private lateinit var robot: LoginRobot
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||
.around(mockServerScenarioSetupTestRule)
|
||||
.around(mainDispatcherTestRule)
|
||||
.around(SafeCloseActivityRule { activityScenario })
|
||||
.around(ScreenshotRule("test-showcase"))
|
||||
|
||||
override fun setupAfterInjection() {
|
||||
Intents.init()
|
||||
robot = LoginRobot()
|
||||
}
|
||||
|
||||
override fun additionalTearDown() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
|
||||
@Test
|
||||
fun properLoginResultsInNavigationToHome() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
AuthScenario.Success(password = "alma", username = "banan")
|
||||
)
|
||||
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
robot
|
||||
.setPassword("alma")
|
||||
.setUsername("banan")
|
||||
.assertPassword("alma")
|
||||
.assertUsername("banan")
|
||||
.clickOnLogin()
|
||||
.assertLoadingBeforeRequests()
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.assertNavigatedToHome()
|
||||
}
|
||||
|
||||
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
|
||||
@Test
|
||||
fun emptyPasswordShowsProperErrorMessage() {
|
||||
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
robot
|
||||
.setUsername("banan")
|
||||
.assertUsername("banan")
|
||||
.clickOnLogin()
|
||||
.assertLoadingBeforeRequests()
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.assertErrorIsShown(R.string.password_is_invalid)
|
||||
.assertNotNavigatedToHome()
|
||||
.assertNotLoading()
|
||||
}
|
||||
|
||||
/** GIVEN password and empty username WHEN signIn THEN error username is shown */
|
||||
@Test
|
||||
fun emptyUserNameShowsProperErrorMessage() {
|
||||
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
robot
|
||||
.setPassword("banan")
|
||||
.assertPassword("banan")
|
||||
.clickOnLogin()
|
||||
.assertLoadingBeforeRequests()
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.assertErrorIsShown(R.string.username_is_invalid)
|
||||
.assertNotNavigatedToHome()
|
||||
.assertNotLoading()
|
||||
}
|
||||
|
||||
/** GIVEN password and username and invalid credentials response WHEN signIn THEN error invalid credentials is shown */
|
||||
@Test
|
||||
fun invalidCredentialsGivenShowsProperErrorMessage() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
|
||||
)
|
||||
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
robot
|
||||
.setUsername("alma")
|
||||
.setPassword("banan")
|
||||
.assertUsername("alma")
|
||||
.assertPassword("banan")
|
||||
.clickOnLogin()
|
||||
.assertLoadingBeforeRequests()
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.assertErrorIsShown(R.string.credentials_invalid)
|
||||
.assertNotNavigatedToHome()
|
||||
.assertNotLoading()
|
||||
}
|
||||
|
||||
/** GIVEN password and username and error response WHEN signIn THEN error invalid credentials is shown */
|
||||
@Test
|
||||
fun networkErrorShowsProperErrorMessage() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
AuthScenario.GenericError(username = "alma", password = "banan")
|
||||
)
|
||||
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
|
||||
robot
|
||||
.setUsername("alma")
|
||||
.setPassword("banan")
|
||||
.assertUsername("alma")
|
||||
.assertPassword("banan")
|
||||
.clickOnLogin()
|
||||
.assertLoadingBeforeRequests()
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.assertErrorIsShown(R.string.something_went_wrong)
|
||||
.assertNotNavigatedToHome()
|
||||
.assertNotLoading()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.auth
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import org.fnives.test.showcase.android.testutil.intent.notIntended
|
||||
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown
|
||||
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText
|
||||
import org.fnives.test.showcase.android.testutil.viewaction.progressbar.ReplaceProgressBarDrawableToStatic
|
||||
import org.fnives.test.showcase.hilt.R
|
||||
import org.fnives.test.showcase.hilt.ui.home.MainActivity
|
||||
import org.hamcrest.core.IsNot.not
|
||||
|
||||
class LoginRobot {
|
||||
|
||||
fun setupIntentResults() {
|
||||
Intents.intending(hasComponent(MainActivity::class.java.canonicalName))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed because Espresso idling waits until mainThread is idle.
|
||||
*
|
||||
* However, ProgressBar keeps the main thread active since it's animating.
|
||||
*
|
||||
* Another solution is described here: https://proandroiddev.com/progressbar-animations-with-espresso-57f826102187
|
||||
* In short they replace the inflater to remove animations, by using custom test runner.
|
||||
*/
|
||||
fun replaceProgressBar() = apply {
|
||||
onView(withId(R.id.loading_indicator)).perform(ReplaceProgressBarDrawableToStatic())
|
||||
}
|
||||
|
||||
fun setUsername(username: String): LoginRobot = apply {
|
||||
onView(withId(R.id.user_edit_text))
|
||||
.perform(ViewActions.replaceText(username), ViewActions.closeSoftKeyboard())
|
||||
}
|
||||
|
||||
fun setPassword(password: String): LoginRobot = apply {
|
||||
onView(withId(R.id.password_edit_text))
|
||||
.perform(ViewActions.replaceText(password), ViewActions.closeSoftKeyboard())
|
||||
}
|
||||
|
||||
fun clickOnLogin() = apply {
|
||||
replaceProgressBar()
|
||||
onView(withId(R.id.login_cta))
|
||||
.perform(ViewActions.click())
|
||||
}
|
||||
|
||||
fun assertPassword(password: String) = apply {
|
||||
onView(withId((R.id.password_edit_text)))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(password)))
|
||||
}
|
||||
|
||||
fun assertUsername(username: String) = apply {
|
||||
onView(withId((R.id.user_edit_text)))
|
||||
.check(ViewAssertions.matches(ViewMatchers.withText(username)))
|
||||
}
|
||||
|
||||
fun assertErrorIsShown(@StringRes stringResID: Int) = apply {
|
||||
assertSnackBarIsShownWithText(stringResID)
|
||||
}
|
||||
|
||||
fun assertLoadingBeforeRequests() = apply {
|
||||
onView(withId(R.id.loading_indicator))
|
||||
.check(ViewAssertions.matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun assertNotLoading() = apply {
|
||||
onView(withId(R.id.loading_indicator))
|
||||
.check(ViewAssertions.matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
fun assertErrorIsNotShown() = apply {
|
||||
assertSnackBarIsNotShown()
|
||||
}
|
||||
|
||||
fun assertNavigatedToHome() = apply {
|
||||
intended(hasComponent(MainActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun assertNotNavigatedToHome() = apply {
|
||||
notIntended(hasComponent(MainActivity::class.java.canonicalName))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.home
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withChild
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withParent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.fnives.test.showcase.android.testutil.intent.notIntended
|
||||
import org.fnives.test.showcase.android.testutil.viewaction.imageview.WithDrawable
|
||||
import org.fnives.test.showcase.android.testutil.viewaction.recycler.RemoveItemAnimations
|
||||
import org.fnives.test.showcase.android.testutil.viewaction.swiperefresh.PullToRefresh
|
||||
import org.fnives.test.showcase.hilt.R
|
||||
import org.fnives.test.showcase.hilt.ui.auth.AuthActivity
|
||||
import org.fnives.test.showcase.model.content.Content
|
||||
import org.fnives.test.showcase.model.content.FavouriteContent
|
||||
import org.hamcrest.Matchers.allOf
|
||||
|
||||
class HomeRobot {
|
||||
|
||||
/**
|
||||
* Needed because Espresso idling sometimes not in sync with RecyclerView's animation.
|
||||
* So we simply remove the item animations, the animations should be disabled anyway for test.
|
||||
*
|
||||
* Reference: https://github.com/android/android-test/issues/223
|
||||
*/
|
||||
fun removeItemAnimations() = apply {
|
||||
Espresso.onView(withId(R.id.recycler)).perform(RemoveItemAnimations())
|
||||
}
|
||||
|
||||
fun assertToolbarIsShown() = apply {
|
||||
Espresso.onView(withId(R.id.toolbar))
|
||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
fun setupIntentResults() {
|
||||
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||
}
|
||||
|
||||
fun assertNavigatedToAuth() = apply {
|
||||
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun assertDidNotNavigateToAuth() = apply {
|
||||
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun clickSignOut(setupIntentResults: Boolean = true) = apply {
|
||||
if (setupIntentResults) {
|
||||
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||
}
|
||||
|
||||
Espresso.onView(withId(R.id.logout_cta)).perform(click())
|
||||
}
|
||||
|
||||
fun assertContainsItem(index: Int, item: FavouriteContent) = apply {
|
||||
removeItemAnimations()
|
||||
val isFavouriteResourceId = if (item.isFavourite) {
|
||||
R.drawable.favorite_24
|
||||
} else {
|
||||
R.drawable.favorite_border_24
|
||||
}
|
||||
Espresso.onView(withId(R.id.recycler))
|
||||
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
|
||||
|
||||
Espresso.onView(
|
||||
allOf(
|
||||
withChild(allOf(withText(item.content.title), withId(R.id.title))),
|
||||
withChild(allOf(withText(item.content.description), withId(R.id.description))),
|
||||
withChild(allOf(withId(R.id.favourite_cta), WithDrawable(isFavouriteResourceId)))
|
||||
)
|
||||
)
|
||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||
}
|
||||
|
||||
fun clickOnContentItem(index: Int, item: Content) = apply {
|
||||
removeItemAnimations()
|
||||
Espresso.onView(withId(R.id.recycler))
|
||||
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
|
||||
|
||||
Espresso.onView(
|
||||
allOf(
|
||||
withId(R.id.favourite_cta),
|
||||
withParent(
|
||||
allOf(
|
||||
withChild(allOf(withText(item.title), withId(R.id.title))),
|
||||
withChild(allOf(withText(item.description), withId(R.id.description)))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
fun swipeRefresh() = apply {
|
||||
Espresso.onView(withId(R.id.swipe_refresh_layout)).perform(PullToRefresh())
|
||||
}
|
||||
|
||||
fun assertContainsNoItems() = apply {
|
||||
removeItemAnimations()
|
||||
Espresso.onView(withId(R.id.recycler))
|
||||
.check(matches(hasChildCount(0)))
|
||||
}
|
||||
|
||||
fun assertContainsError() = apply {
|
||||
Espresso.onView(withId(R.id.error_message))
|
||||
.check(matches(allOf(isDisplayed(), withText(R.string.something_went_wrong))))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.home
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
||||
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.synchronization.loopMainThreadFor
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.MockServerScenarioSetupTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.AsyncDiffUtilInstantTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.MainDispatcherTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.statesetup.SetupAuthenticationState.setupLogin
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.NetworkSynchronizedActivityTest
|
||||
import org.fnives.test.showcase.hilt.ui.home.MainActivity
|
||||
import org.fnives.test.showcase.model.content.FavouriteContent
|
||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
open class MainActivityInstrumentedSharedTest : NetworkSynchronizedActivityTest() {
|
||||
|
||||
private lateinit var activityScenario: ActivityScenario<MainActivity>
|
||||
|
||||
private val mainDispatcherTestRule = MainDispatcherTestRule()
|
||||
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
|
||||
private val mockServerScenarioSetup: MockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
|
||||
private lateinit var robot: HomeRobot
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||
.around(mockServerScenarioSetupTestRule)
|
||||
.around(mainDispatcherTestRule)
|
||||
.around(AsyncDiffUtilInstantTestRule())
|
||||
.around(SafeCloseActivityRule { activityScenario })
|
||||
.around(ScreenshotRule("test-showcase"))
|
||||
|
||||
override fun setupAfterInjection() {
|
||||
super.setupAfterInjection()
|
||||
robot = HomeRobot()
|
||||
setupLogin(mainDispatcherTestRule, mockServerScenarioSetup)
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
override fun additionalTearDown() {
|
||||
super.additionalTearDown()
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */
|
||||
@Test
|
||||
fun signOutClickedResultsInNavigation() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.clickSignOut()
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.assertNavigatedToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN success response WHEN data is returned THEN it is shown on the ui */
|
||||
@Test
|
||||
fun successfulDataLoadingShowsTheElementsOnTheUI() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
ContentData.contentSuccess.forEachIndexed { index, content ->
|
||||
robot.assertContainsItem(index, FavouriteContent(content, false))
|
||||
}
|
||||
robot.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN success response WHEN item is clicked THEN ui is updated */
|
||||
@Test
|
||||
fun clickingOnListElementUpdatesTheElementsFavouriteState() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
|
||||
robot.assertContainsItem(0, expectedItem)
|
||||
.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN success response WHEN item is clicked THEN ui is updated even if activity is recreated */
|
||||
@Test
|
||||
fun elementFavouritedIsKeptEvenIfActivityIsRecreated() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
|
||||
|
||||
activityScenario.safeClose()
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.assertContainsItem(0, expectedItem)
|
||||
.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN success response WHEN item is clicked then clicked again THEN ui is updated */
|
||||
@Test
|
||||
fun clickingAnElementMultipleTimesProperlyUpdatesIt() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false)
|
||||
robot.assertContainsItem(0, expectedItem)
|
||||
.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN error response WHEN loaded THEN error is Shown */
|
||||
@Test
|
||||
fun networkErrorResultsInUIErrorStateShown() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.assertContainsNoItems()
|
||||
.assertContainsError()
|
||||
.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN error response then success WHEN retried THEN success is shown */
|
||||
@Test
|
||||
fun retryingFromErrorStateAndSucceedingShowsTheData() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
ContentScenario.Error(usingRefreshedToken = false)
|
||||
.then(ContentScenario.Success(usingRefreshedToken = false))
|
||||
)
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.swipeRefresh()
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
loopMainThreadFor(2000L)
|
||||
|
||||
ContentData.contentSuccess.forEachIndexed { index, content ->
|
||||
robot.assertContainsItem(index, FavouriteContent(content, false))
|
||||
}
|
||||
robot.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN success then error WHEN retried THEN error is shown */
|
||||
@Test
|
||||
fun errorIsShownIfTheDataIsFetchedAndErrorIsReceived() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
ContentScenario.Success(usingRefreshedToken = false)
|
||||
.then(ContentScenario.Error(usingRefreshedToken = false))
|
||||
)
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.swipeRefresh()
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot
|
||||
.assertContainsError()
|
||||
.assertContainsNoItems()
|
||||
.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN unauthenticated then success WHEN loaded THEN success is shown */
|
||||
@Test
|
||||
fun authenticationIsHandledWithASingleLoading() {
|
||||
mockServerScenarioSetup.setScenario(
|
||||
ContentScenario.Unauthorized(usingRefreshedToken = false)
|
||||
.then(ContentScenario.Success(usingRefreshedToken = true))
|
||||
)
|
||||
.setScenario(RefreshTokenScenario.Success)
|
||||
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
ContentData.contentSuccess.forEachIndexed { index, content ->
|
||||
robot.assertContainsItem(index, FavouriteContent(content, false))
|
||||
}
|
||||
robot.assertDidNotNavigateToAuth()
|
||||
}
|
||||
|
||||
/** GIVEN unauthenticated then error WHEN loaded THEN navigated to auth */
|
||||
@Test
|
||||
fun sessionExpirationResultsInNavigation() {
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false))
|
||||
.setScenario(RefreshTokenScenario.Error)
|
||||
|
||||
activityScenario = ActivityScenario.launch(MainActivity::class.java)
|
||||
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
|
||||
|
||||
robot.assertNavigatedToAuth()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.splash
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
|
||||
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.MockServerScenarioSetupTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.MainDispatcherTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.statesetup.SetupAuthenticationState.setupLogin
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.statesetup.SetupAuthenticationState.setupLogout
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.NetworkSynchronizedActivityTest
|
||||
import org.fnives.test.showcase.hilt.ui.splash.SplashActivity
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
open class SplashActivityInstrumentedSharedTest : NetworkSynchronizedActivityTest() {
|
||||
|
||||
private lateinit var activityScenario: ActivityScenario<SplashActivity>
|
||||
|
||||
private val mainDispatcherTestRule = MainDispatcherTestRule()
|
||||
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
|
||||
private val mockServerScenarioSetup: MockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
|
||||
|
||||
private lateinit var robot: SplashRobot
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||
.around(mainDispatcherTestRule)
|
||||
.around(mockServerScenarioSetupTestRule)
|
||||
.around(SafeCloseActivityRule { activityScenario })
|
||||
.around(ScreenshotRule("test-showcase"))
|
||||
|
||||
override fun setupAfterInjection() {
|
||||
Intents.init()
|
||||
robot = SplashRobot()
|
||||
}
|
||||
|
||||
override fun additionalTearDown() {
|
||||
Intents.release()
|
||||
}
|
||||
|
||||
/** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */
|
||||
@Test
|
||||
fun loggedInStateNavigatesToHome() {
|
||||
setupLogin(mainDispatcherTestRule, mockServerScenarioSetup)
|
||||
|
||||
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
mainDispatcherTestRule.advanceTimeBy(501)
|
||||
|
||||
robot.assertHomeIsStarted()
|
||||
.assertAuthIsNotStarted()
|
||||
}
|
||||
|
||||
/** GIVEN loggedOffState WHEN opened after some time THEN AuthActivity is started */
|
||||
@Test
|
||||
fun loggedOutStatesNavigatesToAuthentication() {
|
||||
setupLogout(mainDispatcherTestRule)
|
||||
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
mainDispatcherTestRule.advanceTimeBy(501)
|
||||
|
||||
robot.assertAuthIsStarted()
|
||||
.assertHomeIsNotStarted()
|
||||
}
|
||||
|
||||
/** GIVEN loggedOffState and not enough time WHEN opened THEN no activity is started */
|
||||
@Test
|
||||
fun loggedOutStatesNotEnoughTime() {
|
||||
setupLogout(mainDispatcherTestRule)
|
||||
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
mainDispatcherTestRule.advanceTimeBy(500)
|
||||
|
||||
robot.assertAuthIsNotStarted()
|
||||
.assertHomeIsNotStarted()
|
||||
}
|
||||
|
||||
/** GIVEN loggedInState and not enough time WHEN opened THEN no activity is started */
|
||||
@Test
|
||||
fun loggedInStatesNotEnoughTime() {
|
||||
setupLogin(mainDispatcherTestRule, mockServerScenarioSetup)
|
||||
|
||||
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
mainDispatcherTestRule.advanceTimeBy(500)
|
||||
|
||||
robot.assertHomeIsNotStarted()
|
||||
.assertAuthIsNotStarted()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package org.fnives.test.showcase.hilt.test.shared.ui.splash
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import org.fnives.test.showcase.android.testutil.intent.notIntended
|
||||
import org.fnives.test.showcase.hilt.ui.auth.AuthActivity
|
||||
import org.fnives.test.showcase.hilt.ui.home.MainActivity
|
||||
|
||||
class SplashRobot {
|
||||
|
||||
fun setupIntentResults() {
|
||||
Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||
}
|
||||
|
||||
fun assertHomeIsStarted() = apply {
|
||||
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun assertHomeIsNotStarted() = apply {
|
||||
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun assertAuthIsStarted() = apply {
|
||||
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
}
|
||||
|
||||
fun assertAuthIsNotStarted() = apply {
|
||||
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue