Skip to content

Commit

Permalink
Merge pull request #175 from ILIYANGERMANOV/ui-tests
Browse files Browse the repository at this point in the history
UI tests & minor issues closed
  • Loading branch information
ILIYANGERMANOV committed Nov 24, 2021
2 parents fe4350a + 32d7e37 commit a4c0900
Show file tree
Hide file tree
Showing 59 changed files with 2,034 additions and 136 deletions.
27 changes: 26 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,38 @@ Please check if your pull request fulfills the following requirements:
- [ ] I've read the **[Contribution Guidelines](https://github.com/ILIYANGERMANOV/ivy-wallet/blob/main/CONTRIBUTING.md)**.
- [ ] The code builds and is tested on an actual Android device.
- [ ] I confirm that I've run the code locally and everything works as expected.
- [ ] I confirm that I've run `bundle exec fastlane ui_tests` and all tests are passing
- [ ] I confirm that I've run Ivy Wallet's UI tests (`androidTest`) and all tests are passing
successfully.

_Important: Don't worry if you experience flaky UI tests. Just re-run the failed ones again and if they pass => it's all good!_

_Put an `x` in the boxes that apply._

- [x] Demo: Checking checkbox using `[x]`

### How to run Ivy Wallet's UI tests (`androidTest`)

**Connect Android Emulator**
- Pixel 5 API 29+ AVD emulator _(recommended)_
- Pixel 3XL API 29+ AVD emulator _(recommended)_
- Large screen physical device _(might also work)_

**Method 1: Android Studio UI**
- Find `com (androidTest)` package
- Right click
- `Run 'Tests in 'com''`

_Note: If you've checked "Compact Middle Packages" the option will appear as `com.ivy.wallet (androidTest)`._

**Method 2: Gradle Wrapper**
- `chmod +x gradlew` (Linux)
- `./gradlew connectedDebugAndroidTest`

**Method 3: Fastlane**
- Install Ruby 2.7
- `bundle install`
- `bundle exec fastlane ui_tests`

## Pull Request Type

Please check the type of change your PR introduces:
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/ui_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ jobs:
with:
ruby-version: '2.7'

- name: Install Fastlane
run: bundle install
# - name: Install up-to-date Bundler vesrion
# run: gem install bundler:2.2.30

# - name: Install Fastlane
# run: bundle install
#----------------------------------------------------

#Security
Expand All @@ -62,13 +65,15 @@ jobs:
#---------------------------------------------------

#Run UI tests
- name: Run UI Tests
- name: Run UI tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: bundle exec fastlane ui_tests
profile: pixel_3_xl
script: fastlane ui_tests

- name: Upload Android Tests report to GitHub
if: always()
uses: actions/upload-artifact@v2
with:
name: android-tests-report
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package com.ivy.wallet.compose
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
import androidx.compose.ui.test.printToString

const val COMPOSE_TEST_TAG = "compose_test"

fun ComposeTestRule.printTree() {
this.onRoot(useUnmergedTree = false).printToLog(COMPOSE_TEST_TAG)
println(this.onRoot(useUnmergedTree = false).printToString(100))
fun ComposeTestRule.printTree(useUnmergedTree: Boolean = true) {
this.onRoot(useUnmergedTree = useUnmergedTree).printToLog(COMPOSE_TEST_TAG)
}
75 changes: 71 additions & 4 deletions app/src/androidTest/java/com/ivy/wallet/compose/IvyComposeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ package com.ivy.wallet.compose
import android.content.Context
import android.util.Log
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.ivy.wallet.base.TestIdlingResource
import com.ivy.wallet.base.TestingContext
import com.ivy.wallet.base.timeNowUTC
import com.ivy.wallet.base.toEpochSeconds
import com.ivy.wallet.base.*
import com.ivy.wallet.persistence.IvyRoomDatabase
import com.ivy.wallet.persistence.SharedPrefs
import com.ivy.wallet.session.IvySession
Expand All @@ -25,6 +24,7 @@ import org.junit.Before
import org.junit.Rule
import javax.inject.Inject


@HiltAndroidTest
abstract class IvyComposeTest {
@get:Rule
Expand Down Expand Up @@ -101,11 +101,78 @@ abstract class IvyComposeTest {
private fun context(): Context {
return InstrumentationRegistry.getInstrumentation().targetContext
}

protected fun testWithRetry(attempt: Int = 0, test: () -> Unit) {
try {
test()
} catch (e: AssertionError) {
if (attempt == 0) {
//reset state && retry test
resetApp()

composeTestRule.waitMillis(500)

//Restart IvyActivity
val intent = composeTestRule.activity.intent
composeTestRule.activity.finish()
composeTestRule.activity.startActivity(intent)

composeTestRule.waitMillis(500)

testWithRetry(
attempt = attempt + 1,
test = test
)
} else {
//propagate exception
throw e
}
}
}
}

fun ComposeTestRule.waitSeconds(secondsToWait: Long) {
val secondsStart = timeNowUTC().toEpochSeconds()
this.waitUntil(timeoutMillis = (secondsToWait + 5) * 1000) {
secondsStart - timeNowUTC().toEpochSeconds() < -secondsToWait
}
}

fun ComposeTestRule.waitMillis(waitMs: Long) {
val startMs = timeNowUTC().toEpochMilli()
this.waitUntil(timeoutMillis = waitMs + 5000) {
startMs - timeNowUTC().toEpochMilli() < -waitMs
}
}

fun SemanticsNodeInteraction.performClickWithRetry(
composeTestRule: ComposeTestRule
) {
composeTestRule.clickWithRetry(
node = this,
maxRetries = 3
)
}

fun ComposeTestRule.clickWithRetry(
node: SemanticsNodeInteraction,
retryAttempt: Int = 0,
maxRetries: Int = 15,
waitBetweenRetriesMs: Long = 100,
) {
try {
node.assertExists()
.performClick()
} catch (e: AssertionError) {
if (retryAttempt < maxRetries) {
waitMillis(waitBetweenRetriesMs)

clickWithRetry(
node = node,
retryAttempt = retryAttempt + 1,
maxRetries = maxRetries,
waitBetweenRetriesMs = waitBetweenRetriesMs
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.ivy.wallet.compose.printTree

class AccountModal<A : ComponentActivity>(
private val composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>
Expand All @@ -16,10 +15,8 @@ class AccountModal<A : ComponentActivity>(
fun enterTitle(
title: String
) {
composeTestRule.printTree()

composeTestRule.onNodeWithTag("base_input")
.performTextInput(title)
.performTextReplacement(title)
}

fun clickBalance() {
Expand All @@ -44,4 +41,9 @@ class AccountModal<A : ComponentActivity>(
.onNode(hasText("Add"))
.performClick()
}

fun tapIncludeInBalance() {
composeTestRule.onNodeWithText("Include in balance")
.performClick()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class AccountsTab<A : ComponentActivity>(
if (currency != null) {
chooseCurrency()
currencyPicker.searchAndSelect(Currency.getInstance(currency))
currencyPicker.modalSave()
}


Expand All @@ -85,4 +86,9 @@ class AccountsTab<A : ComponentActivity>(
clickAdd()
}
}

fun clickReorder() {
composeTestRule.onNodeWithTag("reorder_button")
.performClick()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.ivy.wallet.compose.helpers

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.rules.ActivityScenarioRule

class BudgetModal<A : ComponentActivity>(
private val composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>
) {
private val amountInput = AmountInput(composeTestRule)

fun enterName(budgetName: String) {
composeTestRule.onNodeWithTag("base_input")
.performTextReplacement(budgetName)
}

fun enterAmount(amount: String) {
composeTestRule.onNodeWithTag("amount_balance")
.performClick()

amountInput.enterNumber(amount)
}

fun clickCategory(category: String) {
composeTestRule.onNode(
hasText(category)
.and(hasAnyAncestor(hasTestTag("budget_categories_row"))),
useUnmergedTree = true
).performClick()
}

fun clickAdd() {
composeTestRule.onNodeWithText("Add")
.performClick()
}

fun clickSave() {
composeTestRule.onNodeWithText("Save")
.performClick()
}

fun clickDelete() {
composeTestRule.onNodeWithTag("modal_delete")
.performClick()
}

fun clickClose() {
composeTestRule.onNodeWithContentDescription("close")
.performClick()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.ivy.wallet.compose.helpers

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.rules.ActivityScenarioRule

class BudgetsScreen<A : ComponentActivity>(
private val composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<A>, A>
) {
fun clickAddBudget() {
composeTestRule.onNodeWithText("Add budget")
.performClick()
}

fun assertBudgetsInfo(
appBudget: String?,
categoryBudget: String?,
currency: String = "USD"
) {
val budgetInfoNode = composeTestRule.onNodeWithTag("budgets_info_text")

when {
appBudget != null && categoryBudget == null -> {
budgetInfoNode.assertTextEquals("Budget info: $appBudget $currency app budget")
}
appBudget == null && categoryBudget != null -> {
budgetInfoNode.assertTextEquals("Budget info: $categoryBudget $currency for categories")
}
appBudget != null && categoryBudget != null -> {
budgetInfoNode.assertTextEquals("Budget info: $categoryBudget $currency for categories / $appBudget $currency app budget")
}
appBudget == null && categoryBudget == null -> {
budgetInfoNode.assertDoesNotExist()
}
else -> error("Unexpected case")
}
}

fun clickBudget(
budgetName: String
) {
composeTestRule.onNodeWithText(budgetName)
.performScrollTo()
.performClick()
}

fun assertBudgetDoesNotExist(
budgetName: String
) {
composeTestRule.onNodeWithText(budgetName)
.assertDoesNotExist()
}

}
Loading

0 comments on commit a4c0900

Please sign in to comment.