From df4e8394132fcd5ebcda5e6931004c6fa3bfade6 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 4 Nov 2021 10:20:13 +0100 Subject: [PATCH 01/22] #395 Add E2E test Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 91 +++++++++ sample/build.gradle | 6 +- .../nextcloud/android/sso/sample/E2ETest.java | 190 ++++++++++++++++++ .../sso/sample/ExampleInstrumentedTest.java | 33 --- .../android/sso/sample/ExampleUnitTest.java | 24 --- 5 files changed, 283 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java delete mode 100644 sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java delete mode 100644 sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..4b0af0e8 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,91 @@ +on: [push] + +jobs: + setup_nextcloud: + runs-on: ubuntu-latest + name: Run e2e test + strategy: + matrix: + api-level: [ 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 + 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 + + - name: Make Nextcloud accessible from AVD + run: | + docker exec `docker ps -f 'name=_nextcloud' -l -q` bash -c 'runuser -u www-data -- php occ config:system:set trusted_domains 2 --value=172.17.0.1' + + # TODO 172.17.0.1 is the hard coded IP address of the docker container. Make this more generic. + - name: Verify Nextcloud being present on 172.17.0.1 + 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 + # FIXME Execution of connectedDebugAndroidTest fails + # TODO latest.apk should be cached when matrix testing is used + 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 *:I -v color & + ./gradlew connectedDebugAndroidTest || true \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index b323e854..73bb2d58 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -45,8 +45,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' } \ No newline at end of file diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java new file mode 100644 index 00000000..78d0a6fe --- /dev/null +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -0,0 +1,190 @@ +package com.nextcloud.android.sso.sample; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static androidx.test.uiautomator.Until.findObject; +import static androidx.test.uiautomator.Until.hasObject; + +import android.content.Intent; +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.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * FIXME This does not yet work + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class E2ETest { + + private static final String TAG = E2ETest.class.getSimpleName(); + + private UiDevice mDevice; + + private static final int TIMEOUT = 60_000; + + private static final String APP_NEXTCLOUD = "com.nextcloud.android.beta"; + // TODO This should be passed as argument + private static final String APP_SAMPLE = BuildConfig.APPLICATION_ID; + private static final String SERVER_URL = "http://172.17.0.1:8080"; + private static final String SERVER_USERNAME = "Test"; + private static final String SERVER_PASSWORD = "Test"; + + @Before + public void before() { + mDevice = UiDevice.getInstance(getInstrumentation()); + mDevice.pressHome(); + } + + @After + public void after() { + mDevice.pressHome(); + } + + @Test + public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException { + launch(APP_NEXTCLOUD); + + final var loginButton = mDevice.findObject(new UiSelector().textContains("Log in")); + loginButton.waitForExists(TIMEOUT); + log("Login Button exists. Clicking on it..."); + loginButton.click(); + log("Login Button clicked."); + + final var urlInput = mDevice.findObject(new UiSelector().focused(true)); + urlInput.waitForExists(TIMEOUT); + log("URL input exists."); + log("Entering URL..."); + urlInput.setText(SERVER_URL); + log("URL entered."); + + log("Pressing enter..."); + mDevice.pressEnter(); + log("Enter pressed."); + + log("Waiting for WebView..."); + mDevice.wait(findObject(By.clazz(WebView.class)), TIMEOUT); + log("WebView exists."); + + final var webViewLoginButton = mDevice.findObject(new UiSelector() + .instance(0) + .className(Button.class)); + log("Waiting for WebView Login Button..."); + webViewLoginButton.waitForExists(TIMEOUT); + log("WebView Login Button exists. Clicking on it..."); + webViewLoginButton.click(); + + final var usernameInput = mDevice.findObject(new UiSelector() + .instance(0) + .className(EditText.class)); + log("Waiting for Username Input..."); + usernameInput.waitForExists(TIMEOUT); + log("Username Input exists. Setting text..."); + usernameInput.setText(SERVER_USERNAME); + log("Username has been set."); + + final var passwordInput = mDevice.findObject(new UiSelector() + .instance(1) + .className(EditText.class)); + log("Waiting for Password Input..."); + passwordInput.waitForExists(TIMEOUT); + log("Password Input exists. Setting text..."); + passwordInput.setText(SERVER_PASSWORD); + + final var webViewSubmitButton = mDevice.findObject(new UiSelector() + .instance(0) + .className(Button.class)); + log("Waiting for WebView Submit Button..."); + webViewSubmitButton.waitForExists(TIMEOUT); + log("WebView Submit Button exists. Clicking on it..."); + webViewSubmitButton.click(); + + final var webViewGrantAccessButton = mDevice.findObject(new UiSelector() + .instance(0) + .className(Button.class)); + log("Waiting for WebView Grant Access Button..."); + webViewGrantAccessButton.waitForExists(TIMEOUT); + log("WebView Grant Access Button exists. Clicking on it..."); + webViewGrantAccessButton.click(); + } + + @Test + public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException { + launch(APP_SAMPLE); + + 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(); + + mDevice.waitForWindowUpdate(null, TIMEOUT); + + Thread.sleep(10_000); + final var okButton = mDevice.findObject(new UiSelector() + .textContains("OK")); + log("Waiting for OK Button..."); + okButton.waitForExists(TIMEOUT); + log("OK Button exists. Clicking on it..."); + okButton.click(); + log("OK Button clicked"); + + mDevice.waitForWindowUpdate(null, TIMEOUT); + + final var allowButton = mDevice.findObject(new UiSelector() + .instance(1) + .className(Button.class)); + log("Waiting for Allow Button..."); + allowButton.waitForExists(TIMEOUT); + log("Allow Button exists. Clicking on it..."); + allowButton.click(); + log("Allow Button clicked"); + + log("Waiting for finished import..."); + final var welcomeText = mDevice.findObject(new UiSelector().description("Filter")); + welcomeText.waitForExists(TIMEOUT); + log("Import finished."); + } + + @Test + public void test_02_verifyResult() throws UiObjectNotFoundException { + launch(APP_SAMPLE); + + final var taskCard = mDevice.findObject(new UiSelector() + .textContains("Test on Nextcloud")); + taskCard.waitForExists(TIMEOUT); + System.out.println("Found: " + taskCard.getText()); + } + + private void log(@NonNull String message) { + Log.i(TAG, message); + } + + private void launch(@NonNull String packageName) { + 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); + } +} diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java deleted file mode 100644 index 5bb98ff0..00000000 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/ExampleInstrumentedTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Nextcloud Android SingleSignOn Library - * - * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2021 Stefan Niedermann - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.android.sso.sample; - -import static org.junit.Assert.assertEquals; - -import android.content.Context; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.nextcloud.android.sso.sample", appContext.getPackageName()); - } -} \ No newline at end of file diff --git a/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java b/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java deleted file mode 100644 index dbc01426..00000000 --- a/sample/src/test/java/com/nextcloud/android/sso/sample/ExampleUnitTest.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Nextcloud Android SingleSignOn Library - * - * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2021 Stefan Niedermann - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.android.sso.sample; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file From 9053036eb2c5461d64acf3eae3db4214a929af45 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 08:45:19 +0100 Subject: [PATCH 02/22] #395 E2E test Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../java/com/nextcloud/android/sso/sample/E2ETest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 78d0a6fe..5f7b50dc 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -169,10 +169,10 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio public void test_02_verifyResult() throws UiObjectNotFoundException { launch(APP_SAMPLE); - final var taskCard = mDevice.findObject(new UiSelector() + final var result = mDevice.findObject(new UiSelector() .textContains("Test on Nextcloud")); - taskCard.waitForExists(TIMEOUT); - System.out.println("Found: " + taskCard.getText()); + result.waitForExists(TIMEOUT); + System.out.println("Found: " + result.getText()); } private void log(@NonNull String message) { From 9286b85cda0ec3c8663b741c9518c6dd3e47c442 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 08:58:40 +0100 Subject: [PATCH 03/22] #395 Disable caching for AVD Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4b0af0e8..70fc64bd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -40,33 +40,33 @@ jobs: # 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." +# - 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 # From d537150db2b9bd3de8542ee7312d138c0bf51fa9 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 09:41:41 +0100 Subject: [PATCH 04/22] Limit logs to com.nextcloud.* packages Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 70fc64bd..89eca64d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -87,5 +87,5 @@ jobs: adb install latest.apk adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE adb logcat -c || true - adb logcat *:I -v color & + adb logcat com.nextcloud.*:I -v color & ./gradlew connectedDebugAndroidTest || true \ No newline at end of file From ec1c8da5fb63cbc701d749173b605b5644fca0a6 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 10:23:32 +0100 Subject: [PATCH 05/22] Enhance logging Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- .../nextcloud/android/sso/sample/E2ETest.java | 66 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 89eca64d..d0f7763a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -87,5 +87,5 @@ jobs: adb install latest.apk adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE adb logcat -c || true - adb logcat com.nextcloud.*:I -v color & + adb logcat E2E:V -v color & ./gradlew connectedDebugAndroidTest || true \ No newline at end of file diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 5f7b50dc..370619bc 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -28,7 +28,7 @@ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class E2ETest { - private static final String TAG = E2ETest.class.getSimpleName(); + private static final String TAG = "E2E"; private UiDevice mDevice; @@ -54,73 +54,75 @@ public void after() { @Test public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException { + Log.i(TAG, "Configure Nextcloud account"); launch(APP_NEXTCLOUD); final var loginButton = mDevice.findObject(new UiSelector().textContains("Log in")); loginButton.waitForExists(TIMEOUT); - log("Login Button exists. Clicking on it..."); + Log.d(TAG, "Login Button exists. Clicking on it..."); loginButton.click(); - log("Login Button clicked."); + Log.d(TAG, "Login Button clicked."); final var urlInput = mDevice.findObject(new UiSelector().focused(true)); urlInput.waitForExists(TIMEOUT); - log("URL input exists."); - log("Entering URL..."); + Log.d(TAG, "URL input exists."); + Log.d(TAG, "Entering URL..."); urlInput.setText(SERVER_URL); - log("URL entered."); + Log.d(TAG, "URL entered."); - log("Pressing enter..."); + Log.d(TAG, "Pressing enter..."); mDevice.pressEnter(); - log("Enter pressed."); + Log.d(TAG, "Enter pressed."); - log("Waiting for WebView..."); + Log.d(TAG, "Waiting for WebView..."); mDevice.wait(findObject(By.clazz(WebView.class)), TIMEOUT); - log("WebView exists."); + Log.d(TAG, "WebView exists."); final var webViewLoginButton = mDevice.findObject(new UiSelector() .instance(0) .className(Button.class)); - log("Waiting for WebView Login Button..."); + Log.d(TAG, "Waiting for WebView Login Button..."); webViewLoginButton.waitForExists(TIMEOUT); - log("WebView Login Button exists. Clicking on it..."); + Log.d(TAG, "WebView Login Button exists. Clicking on it..."); webViewLoginButton.click(); final var usernameInput = mDevice.findObject(new UiSelector() .instance(0) .className(EditText.class)); - log("Waiting for Username Input..."); + Log.d(TAG, "Waiting for Username Input..."); usernameInput.waitForExists(TIMEOUT); - log("Username Input exists. Setting text..."); + Log.d(TAG, "Username Input exists. Setting text..."); usernameInput.setText(SERVER_USERNAME); - log("Username has been set."); + Log.d(TAG, "Username has been set."); final var passwordInput = mDevice.findObject(new UiSelector() .instance(1) .className(EditText.class)); - log("Waiting for Password Input..."); + Log.d(TAG, "Waiting for Password Input..."); passwordInput.waitForExists(TIMEOUT); - log("Password Input exists. Setting text..."); + Log.d(TAG, "Password Input exists. Setting text..."); passwordInput.setText(SERVER_PASSWORD); final var webViewSubmitButton = mDevice.findObject(new UiSelector() .instance(0) .className(Button.class)); - log("Waiting for WebView Submit Button..."); + Log.d(TAG, "Waiting for WebView Submit Button..."); webViewSubmitButton.waitForExists(TIMEOUT); - log("WebView Submit Button exists. Clicking on it..."); + Log.d(TAG, "WebView Submit Button exists. Clicking on it..."); webViewSubmitButton.click(); final var webViewGrantAccessButton = mDevice.findObject(new UiSelector() .instance(0) .className(Button.class)); - log("Waiting for WebView Grant Access Button..."); + Log.d(TAG, "Waiting for WebView Grant Access Button..."); webViewGrantAccessButton.waitForExists(TIMEOUT); - log("WebView Grant Access Button exists. Clicking on it..."); + Log.d(TAG, "WebView Grant Access Button exists. Clicking on it..."); webViewGrantAccessButton.click(); } @Test public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException { + Log.i(TAG, "Import account into sample app"); launch(APP_SAMPLE); final var accountButton = mDevice.findObject(new UiSelector() @@ -142,31 +144,32 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio Thread.sleep(10_000); final var okButton = mDevice.findObject(new UiSelector() .textContains("OK")); - log("Waiting for OK Button..."); + Log.d(TAG, "Waiting for OK Button..."); okButton.waitForExists(TIMEOUT); - log("OK Button exists. Clicking on it..."); + Log.d(TAG, "OK Button exists. Clicking on it..."); okButton.click(); - log("OK Button clicked"); + Log.d(TAG, "OK Button clicked"); mDevice.waitForWindowUpdate(null, TIMEOUT); final var allowButton = mDevice.findObject(new UiSelector() .instance(1) .className(Button.class)); - log("Waiting for Allow Button..."); + Log.d(TAG, "Waiting for Allow Button..."); allowButton.waitForExists(TIMEOUT); - log("Allow Button exists. Clicking on it..."); + Log.d(TAG, "Allow Button exists. Clicking on it..."); allowButton.click(); - log("Allow Button clicked"); + Log.d(TAG, "Allow Button clicked"); - log("Waiting for finished import..."); + Log.d(TAG, "Waiting for finished import..."); final var welcomeText = mDevice.findObject(new UiSelector().description("Filter")); welcomeText.waitForExists(TIMEOUT); - log("Import finished."); + Log.d(TAG, "Import finished."); } @Test public void test_02_verifyResult() throws UiObjectNotFoundException { + Log.i(TAG, "Verify successful import"); launch(APP_SAMPLE); final var result = mDevice.findObject(new UiSelector() @@ -175,11 +178,8 @@ public void test_02_verifyResult() throws UiObjectNotFoundException { System.out.println("Found: " + result.getText()); } - private void log(@NonNull String message) { - Log.i(TAG, message); - } - private void launch(@NonNull String packageName) { + Log.d(TAG, "Launching " + packageName); final var context = getInstrumentation().getContext(); context.startActivity(context .getPackageManager() From ecf97ea9a0010bbdf7b8e78a3007f0359b30f35d Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 10:39:31 +0100 Subject: [PATCH 06/22] Merge import and verification tests Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../nextcloud/android/sso/sample/E2ETest.java | 69 ++++++++----------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 370619bc..05d8b246 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -16,7 +16,6 @@ import androidx.test.uiautomator.UiObjectNotFoundException; import androidx.test.uiautomator.UiSelector; -import org.junit.After; import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; @@ -29,14 +28,13 @@ public class E2ETest { private static final String TAG = "E2E"; + private static final int TIMEOUT = 60_000; private UiDevice mDevice; - private static final int TIMEOUT = 60_000; - - private static final String APP_NEXTCLOUD = "com.nextcloud.android.beta"; - // TODO This should be passed as argument 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"; private static final String SERVER_URL = "http://172.17.0.1:8080"; private static final String SERVER_USERNAME = "Test"; private static final String SERVER_PASSWORD = "Test"; @@ -44,12 +42,6 @@ public class E2ETest { @Before public void before() { mDevice = UiDevice.getInstance(getInstrumentation()); - mDevice.pressHome(); - } - - @After - public void after() { - mDevice.pressHome(); } @Test @@ -59,69 +51,69 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException final var loginButton = mDevice.findObject(new UiSelector().textContains("Log in")); loginButton.waitForExists(TIMEOUT); - Log.d(TAG, "Login Button exists. Clicking on it..."); + 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..."); + Log.d(TAG, "Entering URL…"); urlInput.setText(SERVER_URL); Log.d(TAG, "URL entered."); - Log.d(TAG, "Pressing enter..."); + Log.d(TAG, "Pressing enter…"); mDevice.pressEnter(); Log.d(TAG, "Enter pressed."); - Log.d(TAG, "Waiting for WebView..."); + Log.d(TAG, "Waiting for WebView…"); mDevice.wait(findObject(By.clazz(WebView.class)), 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..."); + Log.d(TAG, "Waiting for WebView Login Button…"); webViewLoginButton.waitForExists(TIMEOUT); - Log.d(TAG, "WebView Login Button exists. Clicking on it..."); + Log.d(TAG, "WebView Login Button exists. Clicking on it…"); webViewLoginButton.click(); final var usernameInput = mDevice.findObject(new UiSelector() .instance(0) .className(EditText.class)); - Log.d(TAG, "Waiting for Username Input..."); + Log.d(TAG, "Waiting for Username Input…"); usernameInput.waitForExists(TIMEOUT); - Log.d(TAG, "Username Input exists. Setting text..."); + Log.d(TAG, "Username Input exists. Setting text…"); usernameInput.setText(SERVER_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..."); + Log.d(TAG, "Waiting for Password Input…"); passwordInput.waitForExists(TIMEOUT); - Log.d(TAG, "Password Input exists. Setting text..."); + Log.d(TAG, "Password Input exists. Setting text…"); passwordInput.setText(SERVER_PASSWORD); final var webViewSubmitButton = mDevice.findObject(new UiSelector() .instance(0) .className(Button.class)); - Log.d(TAG, "Waiting for WebView Submit Button..."); + Log.d(TAG, "Waiting for WebView Submit Button…"); webViewSubmitButton.waitForExists(TIMEOUT); - Log.d(TAG, "WebView Submit Button exists. Clicking on it..."); + Log.d(TAG, "WebView Submit Button exists. Clicking on it…"); webViewSubmitButton.click(); final var webViewGrantAccessButton = mDevice.findObject(new UiSelector() .instance(0) .className(Button.class)); - Log.d(TAG, "Waiting for WebView Grant Access Button..."); + Log.d(TAG, "Waiting for WebView Grant Access Button…"); webViewGrantAccessButton.waitForExists(TIMEOUT); - Log.d(TAG, "WebView Grant Access Button exists. Clicking on it..."); + Log.d(TAG, "WebView Grant Access Button exists. Clicking on it…"); webViewGrantAccessButton.click(); } @Test - public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException { + public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException { Log.i(TAG, "Import account into sample app"); launch(APP_SAMPLE); @@ -141,12 +133,11 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio mDevice.waitForWindowUpdate(null, TIMEOUT); - Thread.sleep(10_000); final var okButton = mDevice.findObject(new UiSelector() .textContains("OK")); - Log.d(TAG, "Waiting for OK Button..."); + Log.d(TAG, "Waiting for OK Button…"); okButton.waitForExists(TIMEOUT); - Log.d(TAG, "OK Button exists. Clicking on it..."); + Log.d(TAG, "OK Button exists. Clicking on it…"); okButton.click(); Log.d(TAG, "OK Button clicked"); @@ -155,31 +146,27 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio final var allowButton = mDevice.findObject(new UiSelector() .instance(1) .className(Button.class)); - Log.d(TAG, "Waiting for Allow Button..."); + Log.d(TAG, "Waiting for Allow Button…"); allowButton.waitForExists(TIMEOUT); - Log.d(TAG, "Allow Button exists. Clicking on it..."); + Log.d(TAG, "Allow Button exists. Clicking on it…"); allowButton.click(); Log.d(TAG, "Allow Button clicked"); - Log.d(TAG, "Waiting for finished import..."); + Log.d(TAG, "Waiting for finished import…"); final var welcomeText = mDevice.findObject(new UiSelector().description("Filter")); welcomeText.waitForExists(TIMEOUT); Log.d(TAG, "Import finished."); - } - @Test - public void test_02_verifyResult() throws UiObjectNotFoundException { - Log.i(TAG, "Verify successful import"); - launch(APP_SAMPLE); - - final var result = mDevice.findObject(new UiSelector() - .textContains("Test on Nextcloud")); + Log.i(TAG, "Verify successful import…"); + final var expectedToContain = "Test on Nextcloud"; + final var result = mDevice.findObject(new UiSelector().textContains(expectedToContain)); result.waitForExists(TIMEOUT); - System.out.println("Found: " + result.getText()); + 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() From 597d4546c0f05267e73d0c58c5ee430222b261f2 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Fri, 5 Nov 2021 10:56:36 +0100 Subject: [PATCH 07/22] Limit logs to "E2E" tag Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d0f7763a..c0e52f9e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -87,5 +87,5 @@ jobs: adb install latest.apk adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE adb logcat -c || true - adb logcat E2E:V -v color & + adb logcat -s "E2E" -v color & ./gradlew connectedDebugAndroidTest || true \ No newline at end of file From 20883cd024ae0456fed0a516634367f469276776 Mon Sep 17 00:00:00 2001 From: Niedermann IT-Dienstleistungen Date: Sat, 6 Nov 2021 08:23:00 +0100 Subject: [PATCH 08/22] Configure trusted domain in env variable Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c0e52f9e..e21486e2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -15,6 +15,7 @@ jobs: SQLITE_DATABASE: db.sqlite NEXTCLOUD_ADMIN_USER: Test NEXTCLOUD_ADMIN_PASSWORD: Test + NEXTCLOUD_TRUSTED_DOMAINS: 172.17.0.1 ports: - 8080:80 options: >- @@ -27,12 +28,8 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Make Nextcloud accessible from AVD - run: | - docker exec `docker ps -f 'name=_nextcloud' -l -q` bash -c 'runuser -u www-data -- php occ config:system:set trusted_domains 2 --value=172.17.0.1' - # TODO 172.17.0.1 is the hard coded IP address of the docker container. Make this more generic. - - name: Verify Nextcloud being present on 172.17.0.1 + - 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 @@ -88,4 +85,4 @@ jobs: adb shell pm grant com.nextcloud.android.beta android.permission.READ_EXTERNAL_STORAGE adb logcat -c || true adb logcat -s "E2E" -v color & - ./gradlew connectedDebugAndroidTest || true \ No newline at end of file + ./gradlew connectedDebugAndroidTest || true From c2366abf503b0faebfca4b8c87f569de69f8c421 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sat, 6 Nov 2021 12:43:37 +0100 Subject: [PATCH 09/22] #395 Replace waitForWindowUpdate with Thread.sleep() As suggested in https://github.com/nextcloud/Android-SingleSignOn/pull/404#issuecomment-962433568 Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../java/com/nextcloud/android/sso/sample/E2ETest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 05d8b246..3f2c3da4 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -113,7 +113,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException } @Test - public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException { + public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundException, InterruptedException { Log.i(TAG, "Import account into sample app"); launch(APP_SAMPLE); @@ -131,7 +131,7 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio radioAccount.waitForExists(TIMEOUT); radioAccount.click(); - mDevice.waitForWindowUpdate(null, TIMEOUT); + Thread.sleep(5_000); final var okButton = mDevice.findObject(new UiSelector() .textContains("OK")); From 5a6eaa9cca216769d29253cd488a8632658bea79 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Sat, 6 Nov 2021 13:40:48 +0100 Subject: [PATCH 10/22] Add quite a lot of Thread.sleep()s... Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../java/com/nextcloud/android/sso/sample/E2ETest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 3f2c3da4..b4eae82d 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -131,17 +131,18 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio radioAccount.waitForExists(TIMEOUT); radioAccount.click(); - Thread.sleep(5_000); + Thread.sleep(15_000); final var okButton = mDevice.findObject(new UiSelector() .textContains("OK")); Log.d(TAG, "Waiting for OK Button…"); okButton.waitForExists(TIMEOUT); + Thread.sleep(15_000); Log.d(TAG, "OK Button exists. Clicking on it…"); okButton.click(); Log.d(TAG, "OK Button clicked"); - mDevice.waitForWindowUpdate(null, TIMEOUT); + Thread.sleep(15_000); final var allowButton = mDevice.findObject(new UiSelector() .instance(1) From e23f8f6ec2c6b44ee5f222d58ba4a11078c7b048 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 8 Nov 2021 07:57:53 +0100 Subject: [PATCH 11/22] Use AVD API 26 Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e21486e2..675aaa24 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,7 +6,7 @@ jobs: name: Run e2e test strategy: matrix: - api-level: [ 24 ] #, 25, 26, 27, 28, 29 ] + api-level: [ 26 ] #, 25, 26, 27, 28, 29 ] nextcloud-version: [ 'nextcloud:latest' ] #, 'nextcloud:stable', 'nextcloud:production' ] services: nextcloud: From d57337b28723287af5281e3ccbd30ca084ad0c97 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 8 Nov 2021 08:51:29 +0100 Subject: [PATCH 12/22] More logs Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 675aaa24..ab345976 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -84,5 +84,5 @@ jobs: 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 connectedDebugAndroidTest || true From e505cc93aa38b199cf2def1a86ae29dad8760e1a Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 8 Nov 2021 09:16:36 +0100 Subject: [PATCH 13/22] Parallel SDK 24 & 26 without failfast Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ab345976..c20e978b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -5,8 +5,9 @@ jobs: runs-on: ubuntu-latest name: Run e2e test strategy: + fail-fast: false matrix: - api-level: [ 26 ] #, 25, 26, 27, 28, 29 ] + api-level: [ 24, 26 ] #, 25, 27, 28, 29 ] nextcloud-version: [ 'nextcloud:latest' ] #, 'nextcloud:stable', 'nextcloud:production' ] services: nextcloud: From 75b0c6594bec570f6822f99893b678eda867d1bb Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 8 Nov 2021 09:34:20 +0100 Subject: [PATCH 14/22] Readd logs from e2e test Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c20e978b..990ea11d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -85,5 +85,6 @@ jobs: 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 connectedDebugAndroidTest || true From f160a529bf16a0f5672a905c05c54eacc43a506f Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 8 Nov 2021 10:59:37 +0100 Subject: [PATCH 15/22] Unify waits in e2e test Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../java/com/nextcloud/android/sso/sample/E2ETest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index b4eae82d..ee527b7a 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -116,6 +116,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException 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) @@ -131,18 +132,18 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio radioAccount.waitForExists(TIMEOUT); radioAccount.click(); - Thread.sleep(15_000); + Thread.sleep(WAIT); final var okButton = mDevice.findObject(new UiSelector() .textContains("OK")); Log.d(TAG, "Waiting for OK Button…"); okButton.waitForExists(TIMEOUT); - Thread.sleep(15_000); + Thread.sleep(WAIT); Log.d(TAG, "OK Button exists. Clicking on it…"); okButton.click(); Log.d(TAG, "OK Button clicked"); - Thread.sleep(15_000); + Thread.sleep(WAIT); final var allowButton = mDevice.findObject(new UiSelector() .instance(1) From c5fbcae9f62a8f5ae1595102f94cdbc069e0c8eb Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Wed, 17 Nov 2021 11:11:20 +0100 Subject: [PATCH 16/22] Use Android 28 for emulated device https://github.com/ReactiveCircus/android-emulator-runner/issues/49#issuecomment-614557488 Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 990ea11d..ebb81edf 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - api-level: [ 24, 26 ] #, 25, 27, 28, 29 ] + api-level: [ 28 ] #, 24, 25, 26, 27, 28, 29 ] nextcloud-version: [ 'nextcloud:latest' ] #, 'nextcloud:stable', 'nextcloud:production' ] services: nextcloud: From 4dd678476b2924412100c1bf6defea6d2a0f480c Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 13:05:14 +0200 Subject: [PATCH 17/22] chore(e2e): Move setup from CI to gradle task for easier local run Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 6 ---- build.gradle | 4 +++ sample/build.gradle | 33 +++++++++++++++++++ .../nextcloud/android/sso/sample/E2ETest.java | 27 ++++++++++++--- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ebb81edf..dc43ec44 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -77,13 +77,7 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - # FIXME Execution of connectedDebugAndroidTest fails - # TODO latest.apk should be cached when matrix testing is used 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 & diff --git a/build.gradle b/build.gradle index 3dfd1ec2..d0341713 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,10 @@ buildscript { } } +plugins { + id "de.undercouch.download" version "5.4.0" +} + allprojects { repositories { google() diff --git a/sample/build.gradle b/sample/build.gradle index 73bb2d58..e2295fd3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -5,6 +5,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import com.android.ddmlib.AndroidDebugBridge +import com.android.ddmlib.NullOutputReceiver + +import java.util.concurrent.TimeUnit + apply plugin: 'com.android.application' android { @@ -35,6 +40,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")) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index ee527b7a..a4a6aa29 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -1,7 +1,6 @@ package com.nextcloud.android.sso.sample; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static androidx.test.uiautomator.Until.findObject; import static androidx.test.uiautomator.Until.hasObject; import android.content.Intent; @@ -45,7 +44,7 @@ public void before() { } @Test - public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException { + public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException, InterruptedException { Log.i(TAG, "Configure Nextcloud account"); launch(APP_NEXTCLOUD); @@ -66,8 +65,10 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException 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); +// mDevice.wait(findObject(By.clazz(WebView.class)), TIMEOUT); + webView.waitForExists(TIMEOUT); Log.d(TAG, "WebView exists."); final var webViewLoginButton = mDevice.findObject(new UiSelector() @@ -76,6 +77,13 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException 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() @@ -95,14 +103,17 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException Log.d(TAG, "Password Input exists. Setting text…"); passwordInput.setText(SERVER_PASSWORD); + // mDevice.pressEnter(); final var webViewSubmitButton = mDevice.findObject(new UiSelector() - .instance(0) + .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)); @@ -110,6 +121,14 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException 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 From 2de4814db9795aaad78ddcab6a31b2a368cd372a Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 13:58:22 +0200 Subject: [PATCH 18/22] chore(e2e): Fail e2e.yml when e2e test fails Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dc43ec44..7d3692c6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -81,4 +81,4 @@ jobs: adb logcat -c || true adb logcat -s "E2E" -v color & adb logcat *:I -v color & - ./gradlew connectedDebugAndroidTest || true + ./gradlew connectedDebugAndroidTest From 12175ed1ec2d1c9f7fd46668a5c680cbb75e1279 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 14:01:27 +0200 Subject: [PATCH 19/22] chore(e2e): Add more log output Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../com/nextcloud/android/sso/sample/E2ETest.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index a4a6aa29..13d957d9 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -4,6 +4,7 @@ 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; @@ -46,6 +47,16 @@ public void before() { @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")); @@ -82,7 +93,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException // 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.dragTo(0, 0, 40); webViewLoginButton.click(); From 3043c6279b809e466b108f5a53f01a39520df26b Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 15:04:20 +0200 Subject: [PATCH 20/22] chore(e2e): Run e2e test only for sample module Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7d3692c6..426266a3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -80,5 +80,4 @@ jobs: script: | adb logcat -c || true adb logcat -s "E2E" -v color & - adb logcat *:I -v color & - ./gradlew connectedDebugAndroidTest + ./gradlew :sample:connectedDebugAndroidTest From 339cbaa10bd03d32a59b34fc64416eebf1285f3b Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 16:00:13 +0200 Subject: [PATCH 21/22] chore(e2e): Move emulator setup back to CI Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .github/workflows/e2e.yml | 5 ++++ build.gradle | 6 ++-- sample/build.gradle | 59 ++++++++++++++++++--------------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 426266a3..f138aaa1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -78,6 +78,11 @@ jobs: 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 diff --git a/build.gradle b/build.gradle index d0341713..065b46bb 100644 --- a/build.gradle +++ b/build.gradle @@ -22,9 +22,9 @@ buildscript { } } -plugins { - id "de.undercouch.download" version "5.4.0" -} +//plugins { +// id "de.undercouch.download" version "5.4.0" +//} allprojects { repositories { diff --git a/sample/build.gradle b/sample/build.gradle index e2295fd3..ef75b9e4 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -4,11 +4,6 @@ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later */ - -import com.android.ddmlib.AndroidDebugBridge -import com.android.ddmlib.NullOutputReceiver - -import java.util.concurrent.TimeUnit apply plugin: 'com.android.application' @@ -40,33 +35,33 @@ 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 - } -} +//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' From eac73abd6b7a8979e2771fef3575773c7e66b1fd Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Thu, 20 Apr 2023 16:11:50 +0200 Subject: [PATCH 22/22] chore(e2e): Enhance documentation Signed-off-by: Stefan Niedermann Signed-off-by: Andy Scherzinger --- .../nextcloud/android/sso/sample/E2ETest.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java index 13d957d9..f7e754ee 100644 --- a/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java +++ b/sample/src/androidTest/java/com/nextcloud/android/sso/sample/E2ETest.java @@ -22,10 +22,23 @@ import org.junit.runners.MethodSorters; /** - * FIXME This does not yet work + *

Setup

+ *

CI / CD

+ *

No manual configuration needs to be done because the setup already happens in the e2e.yml file.

+ *

Local

+ *
    + *
  1. Set {@link #CONFIG_SERVER_URL}, {@link #CONFIG_USERNAME}, {@link #CONFIG_PASSWORD} and {@link #CONFIG_DISPLAY_NAME}. The Nextcloud instance must exist and be reachable.
  2. + *
  3. Remove any existing installation of the Nextcloud files app
  4. + *
  5. Install the Dev-Version of the Nextcloud files app
  6. + *
  7. Grant the android.permission.READ_EXTERNAL_STORAGE permission to the Nextcloud files app
  8. + *
*/ @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; @@ -35,9 +48,6 @@ public class E2ETest { 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"; - private static final String SERVER_URL = "http://172.17.0.1:8080"; - private static final String SERVER_USERNAME = "Test"; - private static final String SERVER_PASSWORD = "Test"; @Before public void before() { @@ -69,7 +79,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException urlInput.waitForExists(TIMEOUT); Log.d(TAG, "URL input exists."); Log.d(TAG, "Entering URL…"); - urlInput.setText(SERVER_URL); + urlInput.setText(CONFIG_SERVER_URL); Log.d(TAG, "URL entered."); Log.d(TAG, "Pressing enter…"); @@ -103,7 +113,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException Log.d(TAG, "Waiting for Username Input…"); usernameInput.waitForExists(TIMEOUT); Log.d(TAG, "Username Input exists. Setting text…"); - usernameInput.setText(SERVER_USERNAME); + usernameInput.setText(CONFIG_USERNAME); Log.d(TAG, "Username has been set."); final var passwordInput = mDevice.findObject(new UiSelector() @@ -112,7 +122,7 @@ public void test_00_configureNextcloudAccount() throws UiObjectNotFoundException Log.d(TAG, "Waiting for Password Input…"); passwordInput.waitForExists(TIMEOUT); Log.d(TAG, "Password Input exists. Setting text…"); - passwordInput.setText(SERVER_PASSWORD); + passwordInput.setText(CONFIG_PASSWORD); // mDevice.pressEnter(); final var webViewSubmitButton = mDevice.findObject(new UiSelector() @@ -190,7 +200,7 @@ public void test_01_importAccountIntoSampleApp() throws UiObjectNotFoundExceptio Log.d(TAG, "Import finished."); Log.i(TAG, "Verify successful import…"); - final var expectedToContain = "Test on Nextcloud"; + 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() + "'.");