Skip to content

Commit

Permalink
Merge pull request #419 from eduvpn/feature/ui_tests
Browse files Browse the repository at this point in the history
Update UI tests
  • Loading branch information
dzolnai authored Apr 19, 2024
2 parents bd87093 + ef7eded commit c5e53a3
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 131 deletions.
96 changes: 58 additions & 38 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on: [ pull_request ]
jobs:
build:
name: build
runs-on: macos-latest # Needs to be macos for running AVD on Github
runs-on: ubuntu-latest
env:
SKIP_ICS_OPENVPN_BUILD: >
--build-cache
Expand All @@ -27,7 +27,7 @@ jobs:
-x :ics-openvpn-main:buildCMakeDebug[x86]
strategy:
matrix:
api-level: [ 31 ]
api-level: [ 34 ]
steps:
- name: Checkout repository and submodules
uses: actions/checkout@v4
Expand All @@ -39,17 +39,6 @@ jobs:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
# Based on https://github.com/actions/cache/blob/main/examples.md#java---gradle
- name: Cache Gradle caches and wrapper
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ matrix.api-level }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-${{ matrix.api-level }}-gradle-
- name: Cache build
uses: actions/cache@v3
with:
Expand Down Expand Up @@ -85,33 +74,64 @@ jobs:
./gradlew app:assembleBasicRelease app:assembleBasicDebugAndroidTest \
--build-cache --warning-mode all
# Based on https://github.com/marketplace/actions/android-emulator-runner
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
# Setup the runner in the KVM group to enable HW Accleration for the emulator.
# see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
- name: Enable KVM group perms
shell: bash
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# Get the AVD if it's already cached.
- name : AVD cache
uses : actions/cache/restore@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
id : restore-avd-cache
with :
path : |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
key : avd-${{ matrix.api-level }}

- 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 }}
arch: x86_64
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."
# If the AVD cache didn't exist, create an AVD
- name : create AVD and generate snapshot for caching
if : steps.restore-avd-cache.outputs.cache-hit != 'true'
uses : reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2
with :
api-level : ${{ matrix.api-level }}
arch : x86_64
disable-animations : false
emulator-boot-timeout : 12000
emulator-options : -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation : false
profile : Galaxy Nexus
ram-size : 4096M
script : echo "Generated AVD snapshot."

- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
# If we just created an AVD because there wasn't one in the cache, then cache that AVD.
- name : cache new AVD before tests
if : steps.restore-avd-cache.outputs.cache-hit != 'true'
id : save-avd-cache
uses : actions/cache/save@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
with :
path : |
~/.android/avd/*
~/.android/adb*
key : avd-${{ matrix.api-level }}

- name : Run tests
uses : reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2
with :
api-level : ${{ matrix.api-level }}
arch : x86_64
disable-animations : true
emulator-options : -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation : false
profile : Galaxy Nexus
script: ./gradlew :app:connectedBasicDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=nl.eduvpn.app.service $SKIP_ICS_OPENVPN_BUILD
- name : Upload results
if : ${{ always() }}
uses : actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
with :
name : instrumentation-test-results
path : ./**/build/reports/androidTests/connected/**
5 changes: 1 addition & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,6 @@ android {
}
}

def daggerVersion = "2.48"
def lifecycleVersion = "2.2.0"

dependencies {
// OpenVPN library
implementation project(path: ':ics-openvpn-main')
Expand Down Expand Up @@ -188,10 +185,10 @@ dependencies {
androidTestImplementation(eduvpnVersions.androidx.test.runner)
androidTestImplementation(eduvpnVersions.androidx.test.rules)
androidTestImplementation(eduvpnVersions.androidx.test.ext.junit)
androidTestImplementation(eduvpnVersions.androidx.test.orchestrator)
androidTestImplementation(eduvpnVersions.espresso)
androidTestImplementation(eduvpnVersions.uiautomator)
coreLibraryDesugaring(eduvpnVersions.desugar.jdk.libs)
androidTestUtil(eduvpnVersions.androidx.test.orchestrator)
}

// This will fail the build if there is a Kotlin compiler warning.
Expand Down
52 changes: 52 additions & 0 deletions app/src/androidTest/java/nl/eduvpn/app/ui_test/BrowserTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package nl.eduvpn.app.ui_test

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiSelector
import nl.eduvpn.app.utils.Log

abstract class BrowserTest {

companion object {
private val TAG = BrowserTest::class.java.name
}
fun prepareBrowser() {
// Switch over to UI Automator now, to control the browser
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// Wait for the browser to open and load
Thread.sleep(2_000L)
try {
// Newer Chrome versions ask if you want to log in
val acceptButton = device.findObject(UiSelector().text("Use without an account"))
acceptButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No Chrome user account shown, continuing", ex)
}
try {
// Chrome asks at first launch to accept data usage
val acceptButton = device.findObject(UiSelector().className("android.widget.Button").text("Accept & continue"))
acceptButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No Chrome accept window shown, continuing", ex)
}
try {
// Do not send all our web traffic to Google
val liteModeToggle = device.findObject(UiSelector().className("android.widget.Switch"))
if(liteModeToggle.isChecked) {
liteModeToggle.click()
}
val nextButton = device.findObject(UiSelector().className("android.widget.Button").text("Next"))
nextButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No lite mode window shown, continuing", ex)
}
try {
// Now it wants us to Sign in...
val noThanksButton = device.findObject(UiSelector().text("No thanks"))
noThanksButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No request for sign in, continung", ex)
}
}
}
55 changes: 18 additions & 37 deletions app/src/androidTest/java/nl/eduvpn/app/ui_test/ConnectVpnTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.*
import nl.eduvpn.app.BaseRobot
Expand All @@ -46,7 +47,7 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class ConnectVpnTest {
class ConnectVpnTest : BrowserTest() {

companion object {
private val TAG = ConnectVpnTest::class.java.name
Expand Down Expand Up @@ -82,48 +83,28 @@ class ConnectVpnTest {
allOf(withText(TEST_SERVER_URL), withClassName(containsString("TextView")))
).perform(click())
}
// Switch over to UI Automator now, to control the browser
val device = UiDevice.getInstance(getInstrumentation())
// Wait for the browser to open and load
Thread.sleep(2_000L)
try {
// Chrome asks at first launch to accept data usage
val acceptButton = device.findObject(UiSelector().className("android.widget.Button").text("Accept & continue"))
acceptButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No Chrome accept window shown, continuing", ex)
}
try {
// Do not send all our web traffic to Google
val liteModeToggle = device.findObject(UiSelector().className("android.widget.Switch"))
if(liteModeToggle.isChecked) {
liteModeToggle.click()
}
val nextButton = device.findObject(UiSelector().className("android.widget.Button").text("Next"))
nextButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No lite mode window shown, continuing", ex)
}
try {
// Now it wants us to Sign in...
val noThanksButton = device.findObject(UiSelector().text("No thanks"))
noThanksButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No request for sign in, continung", ex)
}
prepareBrowser()
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
try {
// We can't find objects based on hints here, so we do it on layout order instead.
Log.v(TAG, "Entering username.")
val userName = device.findObject(UiSelector().className("android.widget.EditText").instance(0))
userName.click()
userName.text = TEST_SERVER_USERNAME
Log.v(TAG, "Scrolling down to see password input")
val webView = UiScrollable(UiSelector().className("android.webkit.WebView").scrollable(true))
webView.scrollToEnd(1)
userName.setText(TEST_SERVER_USERNAME)
Log.v(TAG, "Entering password.")
val password = device.findObject(UiSelector().className("android.widget.EditText").instance(1))
password.click()
password.text = TEST_SERVER_PASSWORD
var password: UiObject?
try {
password = device.findObject(UiSelector().className("android.widget.EditText").instance(1))
password.click()
} catch (ex: Exception) {
Log.v(TAG, "Scrolling down to see password input")
val webView = UiScrollable(UiSelector().className("android.webkit.WebView").scrollable(true))
webView.flingToEnd(1)
Log.v(TAG, "Entering password again.")
password = device.findObject(UiSelector().className("android.widget.EditText").instance(1))
password.click()
}
password?.setText(TEST_SERVER_PASSWORD)
Log.v(TAG, "Hiding keyboard.")
device.pressBack() // Closes the keyboard
Log.v(TAG, "Signing in.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class InstituteAccessDemoTest {
class InstituteAccessDemoTest : BrowserTest() {

companion object {
private val TAG = InstituteAccessDemoTest::class.java.name
Expand Down Expand Up @@ -85,33 +85,7 @@ class InstituteAccessDemoTest {
// Switch over to UI Automator now, to control the browser
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val selector = UiSelector()
// Wait for the browser to open and load
Thread.sleep(2_000L)
try {
// Chrome asks at first launch to accept data usage
val acceptButton = device.findObject(UiSelector().className("android.widget.Button").text("Accept & continue"))
acceptButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No Chrome accept window shown, continuing", ex)
}
try {
// Do not send all our web traffic to Google
val liteModeToggle = device.findObject(UiSelector().className("android.widget.Switch"))
if(liteModeToggle.isChecked) {
liteModeToggle.click()
}
val nextButton = device.findObject(UiSelector().className("android.widget.Button").text("Next"))
nextButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No lite mode window shown, continuing", ex)
}
try {
// Now it wants us to Sign in...
val noThanksButton = device.findObject(UiSelector().text("No thanks"))
noThanksButton.click()
} catch (ex: UiObjectNotFoundException) {
Log.w(TAG, "No request for sign in, continuing", ex)
}
prepareBrowser()
try {
// Select eduID from the list
// "Login with" is hidden in the UI
Expand All @@ -129,20 +103,16 @@ class InstituteAccessDemoTest {
selector.className("android.widget.EditText").instance(0)
)
userName.click()
userName.text = DEMO_USER
userName.setText(DEMO_USER)
device.pressBack()
try {
Log.v(TAG, "Clicking 'type a password' link")
val typePasswordLink = device.findObject(selector.text("type a password."))
typePasswordLink.click()
Thread.sleep(500L)
} catch (ex: Exception) {
// Type a password preference is sometimes remembered.
}
Log.v(TAG, "Clicking 'Next' button")
val nextButton = device.findObject(selector.text("Next"))
nextButton.click()
Thread.sleep(1500L)
Log.v(TAG, "Entering password.")
val password = device.findObject(selector.className("android.widget.EditText").instance(1))
val password = device.findObject(selector.className("android.widget.EditText").instance(0))
password.click()
password.text = DEMO_PASSWORD
password.setText(DEMO_PASSWORD)
device.pressBack()
Log.v(TAG, "Logging in...")
val loginButton = device.findObject(selector.text("Login"))
Expand All @@ -165,11 +135,13 @@ class InstituteAccessDemoTest {
webView.scrollToEnd(2)
Log.v(TAG, "Approving VPN app.")
val approveButton = device.findObject(selector.text("Approve"))
approveButton.click()
try {
approveButton.click()
approveButton.click() // Sometimes it doesn't work :)
} catch (ex: Exception) {
// Unhandled
// It might be in dutch
val toestaanButton = device.findObject(selector.text("Toestaan"))
toestaanButton.click()
}
BaseRobot().waitForView(withText("Demo")).check(matches(isDisplayed()))
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/nl/eduvpn/app/service/BackendService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,14 @@ class BackendService(
}

@kotlin.jvm.Throws(CommonException::class)
suspend fun handleRedirection(redirectUri: Uri?): Boolean {
fun handleRedirection(redirectUri: Uri?): Boolean {
val cookie = pendingOAuthCookie
val urlString = redirectUri?.toString()
if (cookie == null || redirectUri == null || urlString.isNullOrEmpty()) {
return false
}
val error = goBackend.cookieReply(cookie, urlString)
pendingOAuthCookie = null
val error = goBackend.cookieReply(cookie, urlString)
if (!error.isNullOrEmpty()) {
throw CommonException(error)
}
Expand Down
Loading

0 comments on commit c5e53a3

Please sign in to comment.