Skip to content

Commit

Permalink
Add jetpack compose, replace sign in button with composable, and upda…
Browse files Browse the repository at this point in the history
…te tests
  • Loading branch information
plusmobileapps committed Feb 9, 2022
1 parent 86140f3 commit 5424173
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 45 deletions.
53 changes: 43 additions & 10 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ android {
testInstrumentationRunner "com.plusmobileapps.kotlinopenespresso.CustomTestRunner"
}

buildFeatures {
// Enables Jetpack Compose for this module
compose true
}

buildTypes {
release {
minifyEnabled false
Expand All @@ -35,9 +40,21 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

packagingOptions {
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.0'
exclude 'META-INF/LGPL2.1'
}

kotlinOptions {
jvmTarget = '1.8'
}

composeOptions {
kotlinCompilerExtensionVersion '1.0.5'
}

buildFeatures {
viewBinding true
}
Expand All @@ -46,16 +63,16 @@ android {
dependencies {

implementation(project(":model"))
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.annotation:annotation:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-rc01'
implementation "androidx.fragment:fragment-ktx:1.3.6"

testImplementation 'junit:junit:4.+'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha01'
implementation "androidx.fragment:fragment-ktx:1.4.1"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.test:core-ktx:1.4.0"
Expand All @@ -76,4 +93,20 @@ dependencies {
// Kotlinx Serialization - JSON
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"

// Integration with activities
implementation 'androidx.activity:activity-compose:1.4.0'
// Compose Material Design
implementation "androidx.compose.material:material:$compose_version"
// Animations
implementation "androidx.compose.animation:animation:$compose_version"
// Tooling support (Previews, etc.)
implementation "androidx.compose.ui:ui-tooling:$compose_version"
// Integration with ViewModels
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
// UI Tests
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
// Needed for createComposeRule, but not createAndroidComposeRule:
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.plusmobileapps.kotlinopenespresso.extensions

import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import androidx.test.espresso.ViewInteraction
import com.plusmobileapps.kotlinopenespresso.page.BasePage

Expand All @@ -9,6 +12,22 @@ import com.plusmobileapps.kotlinopenespresso.page.BasePage
*/
typealias PageScope <T> = T.() -> Unit

/**
* Will navigate to the selected screen by clicking on the provided [viewInteraction],
* then create a new instance of the screen being navigated to asserting that screen and applying the [block]
*/
inline fun <reified T : BasePage> BasePage.navigateToPageWithClick(
semanticsNodeInteraction: SemanticsNodeInteraction,
block: PageScope<T>
): T {
semanticsNodeInteraction.performClick()
return T::class.java.newInstance().apply {
this.composeTestRule = this@navigateToPageWithClick.composeTestRule
assertScreen()
block()
}
}

/**
* Will navigate to the selected screen by clicking on the provided [viewInteraction],
* then create a new instance of the screen being navigated to asserting that screen and applying the [block]
Expand All @@ -19,6 +38,7 @@ inline fun <reified T : BasePage> BasePage.navigateToPageWithClick(
): T {
viewInteraction.click()
return T::class.java.newInstance().apply {
this.composeTestRule = this@navigateToPageWithClick.composeTestRule
assertScreen()
block()
}
Expand All @@ -27,8 +47,9 @@ inline fun <reified T : BasePage> BasePage.navigateToPageWithClick(
/**
* Use at the start of a test to create a [PageScope] of the object and assert the screen
*/
inline fun <reified T : BasePage> startOnPage(block: PageScope<T> = {}): T =
inline fun <reified T : BasePage> ComposeTestRule.startOnPage(block: PageScope<T> = {}): T =
T::class.java.newInstance().apply {
this.composeTestRule = this@startOnPage
assertScreen()
block()
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.plusmobileapps.kotlinopenespresso.page

import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers.withId

interface BasePage {
abstract class BasePage {

fun assertScreen()
lateinit var composeTestRule: ComposeTestRule

abstract fun assertScreen()

fun Int.toViewInteraction(): ViewInteraction = onView(withId(this))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.plusmobileapps.kotlinopenespresso.extensions.PageScope
import com.plusmobileapps.kotlinopenespresso.extensions.navigateToPageWithClick
import com.plusmobileapps.kotlinopenespresso.extensions.verifyVisible

class LoggedInPage : BasePage {
class LoggedInPage: BasePage() {

override fun assertScreen() {
onWelcomeGreeting().verifyVisible()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package com.plusmobileapps.kotlinopenespresso.page

import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.espresso.ViewInteraction
import androidx.test.platform.app.InstrumentationRegistry
import com.plusmobileapps.kotlinopenespresso.R
import com.plusmobileapps.kotlinopenespresso.extensions.PageScope
import com.plusmobileapps.kotlinopenespresso.extensions.navigateToPageWithClick
import com.plusmobileapps.kotlinopenespresso.extensions.typeText
import com.plusmobileapps.kotlinopenespresso.extensions.verifyVisible

class LoginPage : BasePage {
class LoginPage : BasePage() {

override fun assertScreen() {
onEmail().verifyVisible()
onPassword().verifyVisible()
onSignInOrRegisterButton().verifyVisible()
onSignInOrRegisterButton().assertIsDisplayed()
}

fun enterInfo(email: String, password: String) {
Expand All @@ -22,7 +27,12 @@ class LoginPage : BasePage {

fun onEmail(): ViewInteraction = R.id.username.toViewInteraction()
fun onPassword(): ViewInteraction = R.id.password.toViewInteraction()
fun onSignInOrRegisterButton(): ViewInteraction = R.id.login.toViewInteraction()
fun onSignInOrRegisterButton(): SemanticsNodeInteraction {
val text =
InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.action_sign_in)
return composeTestRule.onNodeWithText(text = text)
}

fun onErrorMessage(): ViewInteraction = R.id.error_message.toViewInteraction()

fun goToLoggedInPage(block: PageScope<LoggedInPage>): LoggedInPage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import androidx.test.espresso.ViewInteraction
import com.plusmobileapps.kotlinopenespresso.R
import com.plusmobileapps.kotlinopenespresso.extensions.verifyVisible

class SettingsPage : BasePage {
class SettingsPage: BasePage() {

override fun assertScreen() {
onTitle().verifyVisible()
}

fun onTitle(): ViewInteraction = R.id.settings_title.toViewInteraction()

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.plusmobileapps.kotlinopenespresso.test

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.core.app.launchActivity
import androidx.test.espresso.IdlingRegistry
import com.plusmobileapps.model.LoginResponse
import com.plusmobileapps.kotlinopenespresso.di.EspressoModule
import com.plusmobileapps.kotlinopenespresso.di.NetworkModule
import com.plusmobileapps.kotlinopenespresso.extensions.click
import com.plusmobileapps.kotlinopenespresso.extensions.startOnPage
import com.plusmobileapps.kotlinopenespresso.extensions.verifyText
import com.plusmobileapps.kotlinopenespresso.extensions.verifyVisible
Expand Down Expand Up @@ -35,6 +36,9 @@ class MockNetworkLoginTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)

@get:Rule
val composeTestRule = createAndroidComposeRule<LoginActivity>()

private val networkHelper = MockNetworkTestHelper()
private val _idlingResource = TestCountingIdlingResource()

Expand Down Expand Up @@ -73,7 +77,7 @@ class MockNetworkLoginTest {

val activityScenario = launchActivity<LoginActivity>()

startOnPage<LoginPage> {
composeTestRule.startOnPage<LoginPage> {
enterInfo(username, password)
}.goToLoggedInPage {
onWelcomeGreeting().verifyText("Welcome $displayName!")
Expand All @@ -94,9 +98,9 @@ class MockNetworkLoginTest {

val activityScenario = launchActivity<LoginActivity>()

startOnPage<LoginPage> {
composeTestRule.startOnPage<LoginPage> {
enterInfo(username, password)
onSignInOrRegisterButton().click()
onSignInOrRegisterButton().performClick()
onErrorMessage().verifyText(expectedError).verifyVisible()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.plusmobileapps.kotlinopenespresso.test

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.core.app.launchActivity
import com.plusmobileapps.kotlinopenespresso.R
import com.plusmobileapps.kotlinopenespresso.data.LoginDataSource
Expand All @@ -12,7 +14,6 @@ import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import org.junit.Rule
import org.junit.Test
Expand All @@ -23,6 +24,9 @@ class MockkLoginTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)

@get:Rule
val composeTestRule = createAndroidComposeRule<LoginActivity>()

@BindValue
@JvmField
val loginDataSource: LoginDataSource = mockk(relaxed = true)
Expand All @@ -36,7 +40,7 @@ class MockkLoginTest {
everyLoginReturns { Result.Success(LoggedInUser("some-user-id", displayName)) }
val scenario = launchActivity<LoginActivity>()

startOnPage<LoginPage> {
composeTestRule.startOnPage<LoginPage> {
enterInfo(email, password)
}.goToLoggedInPage {
onWelcomeGreeting().verifyText("Welcome $displayName!")
Expand All @@ -52,9 +56,9 @@ class MockkLoginTest {
everyLoginReturns { Result.Error(IllegalArgumentException(expectedError)) }
val scenario = launchActivity<LoginActivity>()

startOnPage<LoginPage> {
composeTestRule.startOnPage<LoginPage> {
enterInfo(email, password);
onSignInOrRegisterButton().click()
onSignInOrRegisterButton().performClick()
onErrorMessage().verifyText(expectedError).verifyVisible()
}

Expand All @@ -65,7 +69,7 @@ class MockkLoginTest {
fun tooShortOfPasswordError() {
val scenario = launchActivity<LoginActivity>()

startOnPage<LoginPage> {
composeTestRule.startOnPage<LoginPage> {
onEmail().typeText("1")
onPassword().typeText("4")
onEmail().typeText("2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.res.stringResource
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import com.plusmobileapps.kotlinopenespresso.R
Expand All @@ -30,19 +34,26 @@ class LoginActivity : AppCompatActivity() {

val username = binding.username
val password = binding.password
val login = binding.login
val loginButton = binding.loginButton
val loading = binding.loading

binding.settingsButton.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}

loginButton.setContent {
val state = loginViewModel.loginFormState.observeAsState()
Button(onClick = {
loading.visibility = View.VISIBLE
loginViewModel.login(username.text.toString(), password.text.toString())
}, enabled = state.value?.isDataValid ?: false) {
Text(text = stringResource(id = R.string.action_sign_in))
}
}

loginViewModel.loginFormState.observe(this@LoginActivity, Observer {
val loginState = it ?: return@Observer

// disable login button unless both username / password is valid
login.isEnabled = loginState.isDataValid

if (loginState.usernameError != null) {
username.error = getString(loginState.usernameError)
}
Expand Down Expand Up @@ -88,11 +99,6 @@ class LoginActivity : AppCompatActivity() {
}
false
}

login.setOnClickListener {
loading.visibility = View.VISIBLE
loginViewModel.login(username.text.toString(), password.text.toString())
}
}
}

Expand Down

0 comments on commit 5424173

Please sign in to comment.