Skip to content

Commit

Permalink
Merge pull request #56 from SecUSo/feat/add-automated-ui-tests
Browse files Browse the repository at this point in the history
Update android tests

- add basic UI tests
- update workflow configuration
  • Loading branch information
udenr authored Aug 29, 2023
2 parents a67459f + 5bf1882 commit e67bf2b
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 25 deletions.
49 changes: 25 additions & 24 deletions .github/workflows/android-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
api-level: [32]
target: [google_apis]
api-level: [29]
target: [default]
arch: [x86_64]
steps:
- name: Checkout
Expand All @@ -24,34 +24,35 @@ jobs:
- name: Gradle cache
uses: gradle/gradle-build-action@v2

- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }}
# - name: AVD cache
# uses: actions/cache@v3
# id: avd-cache
# with:
# path: |
# ~/.android/avd/*
# ~/.android/adb*
# key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }}

- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
# - name: create AVD and generate snapshot for caching
# if: steps.avd-cache.outputs.cache-hit != 'true'
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: ${{ matrix.api-level }}
# target: ${{ matrix.target }}
# arch: ${{ matrix.arch }}
# force-avd-creation: false
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: false
# script: echo "Generated AVD snapshot for caching."

- name: Run connected tests
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
profile: Nexus 6
# force-avd-creation: false
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew :app:connectedCheck
script: ./gradlew :app:connectedCheck --stacktrace
8 changes: 7 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@ dependencies {
implementation project(path: ':backup-api')
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.sqlite:sqlite:2.3.1'
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
androidTestImplementation('androidx.test.espresso:espresso-core:3.5.1', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation('com.android.support.test.espresso:espresso-contrib:3.0.2') {
exclude group: 'com.android.support', module: 'appcompat'
exclude group: 'com.android.support', module: 'support-v4'
exclude module: 'recyclerview-v7'
}
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.legacy:legacy-support-core-utils:1.0.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.secuso.privacyfriendlypasswordgenerator

import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.secuso.privacyfriendlypasswordgenerator.activities.MainActivity


@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityPasswordGenerationTest {

@get:Rule
var activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)

@Before
fun addEntry() {
//Remove entry if already present
removeEntry()
//Add new entry
onView(withId(R.id.add_fab)).perform(click())
onView(withId(R.id.editTextDomain))
.perform(typeText(DOMAIN), closeSoftKeyboard())
onView(withId(R.id.editTextUsername))
.perform(typeText(USERNAME), closeSoftKeyboard())
//Close dialog
onView(withId(android.R.id.button1)).perform(click())
}

@After
fun removeEntry() {
//Delete entry
try {
onView(withId(R.id.recycler_view))
.perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(DOMAIN)),
swipeLeft()
)
)
} catch (e: Exception) {

}
}

/**
*
*/
private fun checkEntry(domain: String, masterPassword: String, expectedPassword: String) {
//Select entry
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(hasDescendant(withText(domain)), click()))

//Click skip in the master password tutorial appears
try {
onView(withId(R.id.btn_skip)).perform(click())
} catch (e: NoMatchingViewException) {

}

//Enter master password and generate password
onView(withId(R.id.editTextMasterpassword))
.perform(typeText(masterPassword), closeSoftKeyboard())
onView(withId(R.id.generatorButton)).perform(click())

//Compare generated password to expected value
onView(withId(R.id.textViewPassword)).check(matches(withText(expectedPassword)))

//Close dialog
onView(withId(android.R.id.button1)).perform(click())
}

@Test
fun generatePassword() {
checkEntry(DOMAIN, MASTER_PASSWORD, PASSWORD_EXPECTED)
}

@Test
fun updateEntry() {
//Open update dialog
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(hasDescendant(withText(DOMAIN)), longClick()))
//Enter new username
onView(withId(R.id.editTextUsernameUpdate))
.perform(replaceText(USERNAME_AFTER_UPDATE), closeSoftKeyboard())
//Uncheck special characters
onView(withId(R.id.checkBoxSpecialCharacterUpdate)).perform(click())
//Confirm update
onView(withId(android.R.id.button1)).perform(click())
//Enter master password
onView(withId(R.id.editTextUpdateMasterpassword))
.perform(typeText(MASTER_PASSWORD), closeSoftKeyboard())
onView(withId(R.id.displayButton)).perform(click())
//Compare generated passwords to expected values
onView(withId(R.id.textViewOldPassword)).check(matches(withText(PASSWORD_EXPECTED)))
onView(withId(R.id.textViewNewPassword)).check(matches(withText(PASSWORD_EXPECTED_AFTER_UPDATE)))
//Close dialog
onView(withId(android.R.id.button1)).perform(click())


checkEntry(DOMAIN, MASTER_PASSWORD, PASSWORD_EXPECTED_AFTER_UPDATE)
}

companion object {
const val DOMAIN = "automated-test.test.com"
const val USERNAME = "hugo"
const val USERNAME_AFTER_UPDATE = "hugo2"
const val MASTER_PASSWORD = "12345678"
const val PASSWORD_EXPECTED = "zU5)}h(FNf"
const val PASSWORD_EXPECTED_AFTER_UPDATE = "fYt8wHnhsP"
}
}

0 comments on commit e67bf2b

Please sign in to comment.