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

395 e2e test #404

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
df4e839
#395 Add E2E test
stefan-niedermann Nov 4, 2021
9053036
#395 E2E test
stefan-niedermann Nov 5, 2021
9286b85
#395 Disable caching for AVD
stefan-niedermann Nov 5, 2021
d537150
Limit logs to com.nextcloud.* packages
stefan-niedermann Nov 5, 2021
ec1c8da
Enhance logging
stefan-niedermann Nov 5, 2021
ecf97ea
Merge import and verification tests
stefan-niedermann Nov 5, 2021
597d454
Limit logs to "E2E" tag
stefan-niedermann Nov 5, 2021
20883cd
Configure trusted domain in env variable
stefan-niedermann Nov 6, 2021
c2366ab
#395 Replace waitForWindowUpdate with Thread.sleep()
stefan-niedermann Nov 6, 2021
5a6eaa9
Add quite a lot of Thread.sleep()s...
stefan-niedermann Nov 6, 2021
e23f8f6
Use AVD API 26
stefan-niedermann Nov 8, 2021
d57337b
More logs
stefan-niedermann Nov 8, 2021
e505cc9
Parallel SDK 24 & 26 without failfast
stefan-niedermann Nov 8, 2021
75b0c65
Readd logs from e2e test
stefan-niedermann Nov 8, 2021
f160a52
Unify waits in e2e test
stefan-niedermann Nov 8, 2021
c5fbcae
Use Android 28 for emulated device
stefan-niedermann Nov 17, 2021
4dd6784
chore(e2e): Move setup from CI to gradle task for easier local run
stefan-niedermann Apr 20, 2023
2de4814
chore(e2e): Fail e2e.yml when e2e test fails
stefan-niedermann Apr 20, 2023
12175ed
chore(e2e): Add more log output
stefan-niedermann Apr 20, 2023
3043c62
chore(e2e): Run e2e test only for sample module
stefan-niedermann Apr 20, 2023
339cbaa
chore(e2e): Move emulator setup back to CI
stefan-niedermann Apr 20, 2023
eac73ab
chore(e2e): Enhance documentation
stefan-niedermann Apr 20, 2023
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
88 changes: 88 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
on: [push]

jobs:
setup_nextcloud:
runs-on: ubuntu-latest
name: Run e2e test
strategy:
fail-fast: false
matrix:
api-level: [ 28 ] #, 24, 25, 26, 27, 28, 29 ]
nextcloud-version: [ 'nextcloud:latest' ] #, 'nextcloud:stable', 'nextcloud:production' ]
services:
nextcloud:
image: ${{ matrix.nextcloud-version }}
env:
SQLITE_DATABASE: db.sqlite
NEXTCLOUD_ADMIN_USER: Test
NEXTCLOUD_ADMIN_PASSWORD: Test
NEXTCLOUD_TRUSTED_DOMAINS: 172.17.0.1
ports:
- 8080:80
options: >-
--health-cmd "curl GET 'http://Test:Test@localhost:80/ocs/v2.php/apps/serverinfo/api/v1/info' -f -H 'OCS-APIRequest: true' || exit 1"
--health-interval 1s
--health-timeout 2s
--health-retries 10
--health-start-period 3s
steps:
- name: Checkout
uses: actions/checkout@v2

# TODO 172.17.0.1 is the hard coded IP address of the docker container. Make this more generic.
- name: Verify Nextcloud being present
run: |
curl -v -X GET 'http://Test:Test@172.17.0.1:8080/ocs/v2.php/cloud/capabilities?format=json' -H 'OCS-APIRequest: true' | jq

##########################
# AVD CACHING START #
##########################

# - name: Gradle cache
# uses: actions/cache@v2
# with:
# path: |
# ~/.gradle/caches
# ~/.gradle/wrapper
# key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
#
# - name: AVD cache
# uses: actions/cache@v2
# id: avd-cache
# with:
# path: |
# ~/.android/avd/*
# ~/.android/adb*
# 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 }}
# force-avd-creation: false
# sdcard-path-or-size: sdcard
# emulator-options: -gpu swiftshader_indirect -no-window -noaudio -no-boot-anim -camera-back none
# disable-animations: true
# script: echo "Generated AVD snapshot for caching."

##########################
# AVD CACHING END #
##########################

- name: Run e2e tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
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: |
adb shell pm uninstall -k --user 0 com.nextcloud.android.beta || true
wget -q https://download.nextcloud.com/android/dev/latest.apk
adb install latest.apk
adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE
adb logcat -c || true
adb logcat -s "E2E" -v color &
adb logcat *:I -v color &
./gradlew :sample:connectedDebugAndroidTest
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ buildscript {
}
}

//plugins {
// id "de.undercouch.download" version "5.4.0"
//}

allprojects {
repositories {
google()
Expand Down
36 changes: 31 additions & 5 deletions sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

apply plugin: 'com.android.application'

android {
Expand Down Expand Up @@ -35,6 +35,34 @@ android {
}
}

//task downloadNextcloudApk(type: Download) {
// src 'https://download.nextcloud.com/android/dev/latest.apk'
// dest new File(buildDir, 'latest.apk')
// overwrite true
//}
//
//task setupNextcloudEnvironment(dependsOn: downloadNextcloudApk) {
// def bridge = AndroidDebugBridge.createBridge(android.adbExecutable.path, false, 10, TimeUnit.SECONDS)
// doLast {
// bridge.devices.each { device ->
// println "Uninstall Nextcloud apk from ${device.name}"
// device.uninstallPackage("com.nextcloud.android.beta")
//
// println "Install Nextcloud apk on ${device.name}"
// device.installPackage(new File(buildDir, 'latest.apk').getAbsolutePath(), true)
//
// println "Grant permissions to Nextcloud"
// device.executeShellCommand("pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE", NullOutputReceiver.receiver, 3, TimeUnit.SECONDS)
// }
// }
//}
//
//tasks.whenTaskAdded { taskItem ->
// if (taskItem.name.contains("connected") && taskItem.name.endsWith("AndroidTest")) {
// taskItem.dependsOn setupNextcloudEnvironment
// }
//}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.22"))
Expand All @@ -45,8 +73,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'

testImplementation 'junit:junit:4.13.2'

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package com.nextcloud.android.sso.sample;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static androidx.test.uiautomator.Until.hasObject;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.util.Log;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;

import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

/**
* <h1>Setup</h1>
* <h2>CI / CD</h2>
* <p>No manual configuration needs to be done because the setup already happens in the <code>e2e.yml</code> file.</p>
* <h2>Local</h2>
* <ol>
* <li>Set {@link #CONFIG_SERVER_URL}, {@link #CONFIG_USERNAME}, {@link #CONFIG_PASSWORD} and {@link #CONFIG_DISPLAY_NAME}. The Nextcloud instance must exist and be reachable.</li>
* <li>Remove any existing installation of the Nextcloud files app</li>
* <li>Install the <a href="https://download.nextcloud.com/android/dev/latest.apk">Dev-Version of the Nextcloud files app</a></li>
* <li>Grant the <code>android.permission.READ_EXTERNAL_STORAGE</code> permission to the Nextcloud files app</li>
* </ol>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class E2ETest {
private static final String CONFIG_SERVER_URL = "http://172.17.0.1:8080";
private static final String CONFIG_USERNAME = "Test";
private static final String CONFIG_DISPLAY_NAME = "Test";
private static final String CONFIG_PASSWORD = "Test";

private static final String TAG = "E2E";
private static final int TIMEOUT = 60_000;

private UiDevice mDevice;

private static final String APP_SAMPLE = BuildConfig.APPLICATION_ID;
// TODO This should be passed as argument
private static final String APP_NEXTCLOUD = "com.nextcloud.android.beta";

@Before
public void before() {
mDevice = UiDevice.getInstance(getInstrumentation());
}

@Test
public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException, InterruptedException {
Log.i(TAG, "Configure Nextcloud account");

final var context = getInstrumentation().getContext();
final var packageManager = context.getPackageManager();
try {
packageManager.getPackageInfo(APP_NEXTCLOUD, 0);
Log.i(TAG, "Nextcloud APK is installed (checking on runtime)");
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Nextcloud APK is NOT installed (checking on runtime");
}

launch(APP_NEXTCLOUD);

final var loginButton = mDevice.findObject(new UiSelector().textContains("Log in"));
loginButton.waitForExists(TIMEOUT);
Log.d(TAG, "Login Button exists. Clicking on it…");
loginButton.click();
Log.d(TAG, "Login Button clicked.");

final var urlInput = mDevice.findObject(new UiSelector().focused(true));
urlInput.waitForExists(TIMEOUT);
Log.d(TAG, "URL input exists.");
Log.d(TAG, "Entering URL…");
urlInput.setText(CONFIG_SERVER_URL);
Log.d(TAG, "URL entered.");

Log.d(TAG, "Pressing enter…");
mDevice.pressEnter();
Log.d(TAG, "Enter pressed.");

final var webView = mDevice.findObject(new UiSelector().instance(0).className(WebView.class));
Log.d(TAG, "Waiting for WebView…");
// mDevice.wait(findObject(By.clazz(WebView.class)), TIMEOUT);
webView.waitForExists(TIMEOUT);
Log.d(TAG, "WebView exists.");

final var webViewLoginButton = mDevice.findObject(new UiSelector()
.instance(0)
.className(Button.class));
Log.d(TAG, "Waiting for WebView Login Button…");
webViewLoginButton.waitForExists(TIMEOUT);
Log.d(TAG, "WebView Login Button exists. Clicking on it…");

// TODO Find better way to scroll the Login button to the visible area
// Log.d(TAG, "Scroll to bottom of WebView…");
// mDevice.findObject(By.clazz(WebView.class)).swipe(Direction.UP, 1f);
// Log.d(TAG, "Finished scrolling");
webViewLoginButton.dragTo(0, 0, 40);

webViewLoginButton.click();

final var usernameInput = mDevice.findObject(new UiSelector()
.instance(0)
.className(EditText.class));
Log.d(TAG, "Waiting for Username Input…");
usernameInput.waitForExists(TIMEOUT);
Log.d(TAG, "Username Input exists. Setting text…");
usernameInput.setText(CONFIG_USERNAME);
Log.d(TAG, "Username has been set.");

final var passwordInput = mDevice.findObject(new UiSelector()
.instance(1)
.className(EditText.class));
Log.d(TAG, "Waiting for Password Input…");
passwordInput.waitForExists(TIMEOUT);
Log.d(TAG, "Password Input exists. Setting text…");
passwordInput.setText(CONFIG_PASSWORD);

// mDevice.pressEnter();
final var webViewSubmitButton = mDevice.findObject(new UiSelector()
.instance(1) // First button is password visibility toggle
.className(Button.class));
Log.d(TAG, "Waiting for WebView Submit Button…");
webViewSubmitButton.waitForExists(TIMEOUT);
Log.d(TAG, "WebView Submit Button exists. Clicking on it…");
webViewSubmitButton.click();

webViewSubmitButton.waitUntilGone(TIMEOUT);

final var webViewGrantAccessButton = mDevice.findObject(new UiSelector()
.instance(0)
.className(Button.class));
Log.d(TAG, "Waiting for WebView Grant Access Button…");
webViewGrantAccessButton.waitForExists(TIMEOUT);
Log.d(TAG, "WebView Grant Access Button exists. Clicking on it…");
webViewGrantAccessButton.click();

webView.waitUntilGone(TIMEOUT);

mDevice.waitForIdle(TIMEOUT);

Log.d(TAG, "Wait for Nextcloud files app…");
Thread.sleep(3_000);
Log.d(TAG, "Finishing setup…");
}

@Test
public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException {
Log.i(TAG, "Import account into sample app");
launch(APP_SAMPLE);
final var WAIT = 3_000;

final var accountButton = mDevice.findObject(new UiSelector()
.instance(0)
.className(Button.class));
accountButton.waitForExists(TIMEOUT);
accountButton.click();

mDevice.waitForWindowUpdate(null, TIMEOUT);

final var radioAccount = mDevice.findObject(new UiSelector()
.clickable(true)
.instance(0));
radioAccount.waitForExists(TIMEOUT);
radioAccount.click();

Thread.sleep(WAIT);

final var okButton = mDevice.findObject(new UiSelector()
.textContains("OK"));
Log.d(TAG, "Waiting for OK Button…");
okButton.waitForExists(TIMEOUT);
Thread.sleep(WAIT);
Log.d(TAG, "OK Button exists. Clicking on it…");
okButton.click();
Log.d(TAG, "OK Button clicked");

Thread.sleep(WAIT);

final var allowButton = mDevice.findObject(new UiSelector()
.instance(1)
.className(Button.class));
Log.d(TAG, "Waiting for Allow Button…");
allowButton.waitForExists(TIMEOUT);
Log.d(TAG, "Allow Button exists. Clicking on it…");
allowButton.click();
Log.d(TAG, "Allow Button clicked");

Log.d(TAG, "Waiting for finished import…");
final var welcomeText = mDevice.findObject(new UiSelector().description("Filter"));
welcomeText.waitForExists(TIMEOUT);
Log.d(TAG, "Import finished.");

Log.i(TAG, "Verify successful import…");
final var expectedToContain = CONFIG_DISPLAY_NAME + " on Nextcloud";
final var result = mDevice.findObject(new UiSelector().textContains(expectedToContain));
result.waitForExists(TIMEOUT);
Log.i(TAG, "Expected UI to display '" + expectedToContain + "'. Found: '" + result.getText() + "'.");
}

private void launch(@NonNull String packageName) {
Log.d(TAG, "Launching " + packageName);
mDevice.pressHome();
final var context = getInstrumentation().getContext();
context.startActivity(context
.getPackageManager()
.getLaunchIntentForPackage(packageName)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK));
mDevice.wait(hasObject(By.pkg(packageName).depth(0)), TIMEOUT);
}
}
Loading
Loading