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

Update UI tests #419

Merged
merged 1 commit into from
Apr 19, 2024
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
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
Loading