Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic UI tests #56

Merged
merged 3 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
}