From 4605a80cb23c43c8ca0976ec4fb1819d8f369de6 Mon Sep 17 00:00:00 2001 From: aminecmi Date: Sun, 22 Dec 2024 21:05:47 +0100 Subject: [PATCH] test: test --- .gitea/workflows/common_build.yml | 4 +- .gitea/workflows/on_push_testing.yml | 56 ++++-- .gitignore | 5 +- androidApp/.gitignore | 1 + androidApp/build.gradle.kts | 11 ++ .../readerforselfossv2/android/CommonTests.kt | 23 +++ .../android/HomeActivityTest.kt | 162 ++++++++++++++++++ .../android/LoginActivityTest.kt | 70 ++++++++ .../readerforselfossv2/android/helpers.kt | 65 +++++++ androidApp/src/test/kotlin/DatesTest.kt | 66 ------- gradle.properties | 2 + 11 files changed, 379 insertions(+), 86 deletions(-) create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt delete mode 100644 androidApp/src/test/kotlin/DatesTest.kt diff --git a/.gitea/workflows/common_build.yml b/.gitea/workflows/common_build.yml index 8db1abc..d899be6 100644 --- a/.gitea/workflows/common_build.yml +++ b/.gitea/workflows/common_build.yml @@ -19,6 +19,6 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Configure gradle... - run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties + run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties - name: Build and test - run: ./gradlew build --stacktrace + run: ./gradlew build --stacktrace \ No newline at end of file diff --git a/.gitea/workflows/on_push_testing.yml b/.gitea/workflows/on_push_testing.yml index 9ca8aa2..93f354d 100644 --- a/.gitea/workflows/on_push_testing.yml +++ b/.gitea/workflows/on_push_testing.yml @@ -10,10 +10,10 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v4 -# with: -# fetch-depth: 0 -# - name: Fetch tags -# run: git fetch --tags -p + with: + fetch-depth: 0 + - name: Fetch tags + run: git fetch --tags -p - name: Init compose uses: KengoTODA/actions-setup-docker-compose@v1 with: @@ -21,20 +21,42 @@ jobs: - name: run selfoss run: | docker compose -f .gitea/workflows/assets/docker-compose.yml up -d + sleep 1m response=$(curl 172.17.0.1:8888/api/about) echo $response -# - uses: actions/setup-java@v4 -# with: -# distribution: 'temurin' -# java-version: '17' -# - name: Setup Android SDK -# uses: android-actions/setup-android@v3 -# - name: Configure gradle... -# run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties -# - name: Build and test -# run: ./gradlew build --stacktrace + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Configure gradle... + run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties + - name: ui tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 25 + force-avd-creation: false + disable-animations: true + script: adb shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS && ./gradlew connectedAndroidTest + - name: Artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: ui-tests + path: androidApp/build + if-no-files-found: error + retention-days: 5 + overwrite: true + include-hidden-files: true + - name: Test Summary + uses: test-summary/action@v2 + with: + paths: | + androidApp/build/outputs/androidTest-results/connected/debug/flavors/githubConfig/**/TEST-*.xml + androidApp/build/test-results/testGithubConfig*/*.xml + if: failure() - name: Clean - if: success() || failure() + if: always() run: | - docker compose -f .gitea/workflows/assets/docker-compose.yml stop - + docker compose -f .gitea/workflows/assets/docker-compose.yml stop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4bf20a4..40e8241 100644 --- a/.gitignore +++ b/.gitignore @@ -320,4 +320,7 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift -crowdin.properties \ No newline at end of file +crowdin.properties + +.kotlin/ +build-cache/ \ No newline at end of file diff --git a/androidApp/.gitignore b/androidApp/.gitignore index 796b96d..248e305 100644 --- a/androidApp/.gitignore +++ b/androidApp/.gitignore @@ -1 +1,2 @@ /build +.kotlin/ \ No newline at end of file diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index c9349bd..11bf753 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -84,6 +84,7 @@ android { // tests testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" } packaging { resources { @@ -107,6 +108,10 @@ android { } } namespace = "bou.amine.apps.readerforselfossv2.android" + testOptions { + animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } } @@ -184,6 +189,12 @@ dependencies { testImplementation("io.mockk:mockk:1.12.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestUtil("androidx.test:orchestrator:1.5.1") + implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion") diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt new file mode 100644 index 0000000..452a429 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -0,0 +1,23 @@ +package bou.amine.apps.readerforselfossv2.android + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText + +fun performLogin(url: String) { + onView(withId(R.id.urlView)).perform(click()).perform( + typeTextIntoFocusedView(url) + ) + onView(withId(R.id.signInButton)).perform(click()) +} + +fun loginAndInitHome() { + performLogin("http://172.17.0.1:8888") + Thread.sleep(30000) + onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) + onView(withText("OK")).perform(click()) +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt new file mode 100644 index 0000000..f82cd27 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt @@ -0,0 +1,162 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.app.Activity +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isFocused +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.isSelected +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class HomeActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + private fun getActivity(): Activity? { + var activity: Activity? = null + activityRule.scenario.onActivity { + activity = it + } + return activity + } + + @Before + fun init() { + loginAndInitHome() + } + + @Test + fun testMenu() { + onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( + matches( + isClickable() + ) + ) + onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check( + matches( + isClickable() + ) + ) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.readAll)).check(matches(isDisplayed())) + onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed())) + onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed())) + onView(withText(R.string.menu_home_refresh)).check(matches(isDisplayed())) + onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed())) + onView(withText(R.string.action_disconnect)).check(matches(isDisplayed())) + } + + @Test + fun testMenuActions() { + onView(withId(R.id.action_search)).perform(click()) + onView( + withId(R.id.search_src_text) + ).check(matches(isFocused())) + onView(isRoot()).perform(ViewActions.pressBack()) + + onView(withId(R.id.action_filter)).perform(click()) + Thread.sleep(30000) + onView( + withText(R.string.filter_item_sources) + ).check(matches(isDisplayed())) + onView( + withText(R.string.filter_item_tags) + ).check(matches(isDisplayed())) + onView( + withId(R.id.floatingActionButton2) + ).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.readAll)).perform(click()) + onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.menu_home_sources)).perform(click()) + onView(withId(R.id.fab)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.title_activity_settings)).perform(click()) + onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.menu_home_refresh)).perform(click()) + onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + /*onView(withText(R.string.issue_tracker_link)).perform(click()) + onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + )*/ + + onView(withText(R.string.action_disconnect)).perform(click()) + onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed())) + } + + @Test + fun testEmptyView() { + onView(withId(R.id.emptyText)).check(matches(isDisplayed())) + onView( + hasBottombarItemText(R.string.tab_new) + ).check(matches(isDisplayed())).check(matches(isSelected())) + onView( + hasBottombarItemText(R.string.tab_read) + ).check(matches(isDisplayed())).check(matches(not(isSelected()))) + onView( + hasBottombarItemText(R.string.tab_favs) + ).check(matches(isDisplayed())).check(matches(not(isSelected()))) + } + + @Test + fun addSource() { + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.menu_home_sources)) + .perform(click()) + onView(withId(R.id.fab)) + .perform(click()) + onView(withId(R.id.nameInput)) + .perform(click()).perform(typeTextIntoFocusedView("Source")) + Thread.sleep(20000) + } + +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt new file mode 100644 index 0000000..b26e83c --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt @@ -0,0 +1,70 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.app.Activity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.hamcrest.CoreMatchers.not +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class LoginActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + private fun getActivity(): Activity? { + var activity: Activity? = null + activityRule.scenario.onActivity { + activity = it + } + return activity + } + + @Test + fun viewIsInitialized() { + onView(withId(R.id.urlView)).check(matches(isDisplayed())) + onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked())) + .check( + matches(isClickable()) + ) + onView(withId(R.id.withLogin)).check(matches(isDisplayed())) + .check(matches(isNotChecked())).check( + matches(isClickable()) + ) + } + + @Test + fun urlError() { + performLogin("172.17.0.1:8888") + onView(withId(R.id.urlView)).perform(click()) + onView(withId(R.id.urlView)).check(matches(withError(getActivity()!!.getString(R.string.wrong_infos)))) + } + + @Test + fun multiError() { + onView(withId(R.id.signInButton)).perform(click()) + Thread.sleep(15000) + onView(withId(R.id.signInButton)).perform(click()) + Thread.sleep(15000) + onView(withId(R.id.signInButton)).perform(click()) + onView(withText(R.string.warning_wrong_url)).check(matches(isDisplayed())) + } + + @Test + fun connect() { + performLogin("http://172.17.0.1:8888") + onView(withId(R.id.loginProgress)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt new file mode 100644 index 0000000..f018513 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt @@ -0,0 +1,65 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.graphics.drawable.toBitmap +import androidx.test.espresso.Root +import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withResourceName +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + + +fun withError(expected: String): TypeSafeMatcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(view: View?): Boolean { + if (view !is EditText) { + return false + } + return view.error.toString() == expected + } + + override fun describeTo(description: Description?) { + } + } +} + +fun isPopupWindow(): Matcher { + return isPlatformPopup() +} + +fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("ImageView with drawable same as drawable with id $id") + } + + override fun matchesSafely(view: View): Boolean { + val context = view.context + val expectedBitmap = context.getDrawable(id)!!.toBitmap() + try { + return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap) + } catch (e: Exception) { + return false + } + } +} + +fun hasBottombarItemText(@StringRes id: Int): Matcher? { + return allOf( + withResourceName("fixed_bottom_navigation_icon"), + withParent( + allOf( + withResourceName("fixed_bottom_navigation_icon_container"), + hasSibling(withText(id)) + ) + ) + ) +} \ No newline at end of file diff --git a/androidApp/src/test/kotlin/DatesTest.kt b/androidApp/src/test/kotlin/DatesTest.kt deleted file mode 100644 index f525801..0000000 --- a/androidApp/src/test/kotlin/DatesTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package bou.amine.apps.readerforselfossv2.repository - -import bou.amine.apps.readerforselfossv2.utils.DateUtils -import junit.framework.TestCase.assertEquals -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import org.junit.Test - -class DatesTest { - private val newVersionDateVariant = "2022-12-24T17:00:08+00" - private val newVersionDate = "2013-04-07T13:43:00+01:00" - private val newVersionDate2 = "2013-04-07T13:43:00-01:00" - private val oldVersionDate = "2013-05-07 13:46:00" - private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" - - - @Test - fun new_version_date_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDate) - val expected = - LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - @Test - fun new_version_date2_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDate2) - val expected = - LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun old_version_date_should_be_parsed() { - val date = DateUtils.parseDate(oldVersionDate) - val expected = - LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun old_version_variant_date_should_be_parsed() { - val date = DateUtils.parseDate(oldVersionDateVariant) - val expected = - LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun new_version_variant_date_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDateVariant) - val expected = - LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } -} diff --git a/gradle.properties b/gradle.properties index 28fdf63..c0d0157 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ kotlin.code.style=official #Android android.useAndroidX=true #android.nonTransitiveRClass=true +android.enableJetifier=true android.nonTransitiveRClass=false #MPP kotlin.mpp.enableCInteropCommonization=true @@ -25,3 +26,4 @@ org.gradle.parallel=true org.gradle.caching=true ignoreGitVersion=false kotlin.native.cacheKind.iosX64=none +org.gradle.configureondemand=true \ No newline at end of file