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..7f63f96 100644 --- a/.gitea/workflows/on_push_testing.yml +++ b/.gitea/workflows/on_push_testing.yml @@ -6,35 +6,49 @@ on: jobs: checkout: - runs-on: ubuntu-latest + runs-on: macos steps: - - name: Check out repository code - uses: actions/checkout@v4 -# with: -# fetch-depth: 0 -# - name: Fetch tags -# run: git fetch --tags -p - - name: Init compose - uses: KengoTODA/actions-setup-docker-compose@v1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch tags + run: git fetch --tags -p + - uses: KengoTODA/actions-setup-docker-compose@v1 with: version: "2.23.3" - name: run selfoss run: | docker compose -f .gitea/workflows/assets/docker-compose.yml up -d - 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 - - name: Clean + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + - uses: gradle/actions/setup-gradle@v3 + - 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 if: success() || failure() + env: + ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 1800 + with: + api-level: 25 + force-avd-creation: false + emulator-boot-timeout: 1800 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-metrics -camera-back none + disable-animations: true + script: adb shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS && ./gradlew connectedAndroidTest --stacktrace + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: ui-tests + path: androidApp/build/outputs/androidTest-results/connected/debug/flavors/githubConfig + retention-days: 1 + overwrite: true + include-hidden-files: true + - name: Clean + 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..c19fdfe 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,9 @@ android { } } namespace = "bou.amine.apps.readerforselfossv2.android" + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } } @@ -184,6 +188,13 @@ 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") + implementation("androidx.test.espresso:espresso-idling-resource: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..8fdd140 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -0,0 +1,90 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.content.Context +import androidx.annotation.ArrayRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +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 org.hamcrest.CoreMatchers.allOf + +fun performLogin(someUrl: String? = null) { + onView(withId(R.id.urlView)).perform(click()).perform( + typeTextIntoFocusedView( + if (!someUrl.isNullOrEmpty()) someUrl else "http://172.17.0.1:8888" + ) + ) + onView(withId(R.id.signInButton)).perform(click()) + Thread.sleep(10000) +} + +fun loginAndInitHome() { + + performLogin() + onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) + onView(withText("OK")).perform(click()) +} + +fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) { + openSettingItem() + onView( + withId(android.R.id.edit) + ).perform(replaceText(newValue)) + onView( + withId(android.R.id.button2) + ).perform(click()) + openSettingItem() + onView( + withId(android.R.id.edit) + ).check(matches(withText(oldValue))) + onView( + withText(newValue) + ).check(doesNotExist()) + onView( + withId(android.R.id.button2) + ).perform(click()) +} + +fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) { + openSettingItem() + onView( + withId(android.R.id.edit) + ).perform(replaceText(newValue)) + onView( + withId(android.R.id.button1) + ).perform(click()) + openSettingItem() + onView( + withId(android.R.id.edit) + ).check(matches(withText(newValue))) + if (oldValue.isNotEmpty()) { + onView( + withText(oldValue) + ).check(doesNotExist()) + } + onView( + withId(android.R.id.button2) + ).perform(click()) +} + +fun testPreferencesFromArray( + context: Context, + @ArrayRes arrayRes: Int, + openSettingItem: () -> Unit +) { + openSettingItem() + context.resources.getStringArray(arrayRes).forEach { res -> + onView(withText(res)).check(matches(allOf(isDisplayed(), isNotChecked()))) + onView(withText(res)).perform(click()) + onView(withText(res)).check(doesNotExist()) + openSettingItem() + onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked()))) + } +} \ 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..e95b84c --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt @@ -0,0 +1,137 @@ +package bou.amine.apps.readerforselfossv2.android + +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.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) + + @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()) + 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()))) + } + +} \ 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..06c554d --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt @@ -0,0 +1,83 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.app.Activity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +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 bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton +import org.junit.After +import org.junit.Before +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 + } + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance() + .unregister(CountingIdlingResourceSingleton.countingIdlingResource) + } + + @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(R.string.wrong_infos))) + } + + @Test + fun multiError() { + onView(withId(R.id.signInButton)).perform(click()) + onView(withId(R.id.signInButton)).perform(click()) + onView(withId(R.id.signInButton)).perform(click()) + onView(withText(R.string.warning_wrong_url)).check(matches(isDisplayed())) + } + + @Test + fun connect() { + performLogin() + onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt new file mode 100644 index 0000000..8a3088b --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt @@ -0,0 +1,65 @@ +package bou.amine.apps.readerforselfossv2.android + +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.click +import androidx.test.espresso.action.ViewActions.scrollCompletelyTo +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 +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +@LargeTest +class SourcesActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + lateinit var sourceName: String + + @Before + fun init() { + sourceName = UUID.randomUUID().toString().substring(0, 15) + + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.menu_home_sources)) + .perform(click()) + } + + @Test + fun addSource() { + onView(withId(R.id.fab)) + .perform(click()) + onView(withId(R.id.nameInput)) + .perform(click()).perform(typeTextIntoFocusedView(sourceName)) + onView(withId(R.id.sourceUri)) + .perform(click()) + .perform(typeTextIntoFocusedView("https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10")) + onView(withId(R.id.tags)) + .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3")) + onView(withId(R.id.spoutsSpinner)) + .perform(click()) + onView(withText("RSS Feed")) + .perform(scrollCompletelyTo()) + .perform(click()) + onView(withId(R.id.saveBtn)) + .perform(click()) + onView(withText(sourceName)).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..60c300f --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt @@ -0,0 +1,101 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.RelativeLayout +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.withChild +import androidx.test.espresso.matcher.ViewMatchers.withClassName +import androidx.test.espresso.matcher.ViewMatchers.withId +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.Matchers +import org.hamcrest.TypeSafeMatcher + + +fun withError(@StringRes id: Int): TypeSafeMatcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(view: View?): Boolean { + if (view == null) { + return false + } + val context = view.context + if (view !is EditText) { + return false + } + if (view.error == null) { + return false + } + + return view.error.toString() == context.getString(id) + } + + 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)) + ) + ) + ) +} + +fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher? { + return allOf( + withId(android.R.id.switch_widget), + withParent( + withSettingsCheckboxFrame(id) + ) + ) +} + +fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher? { + return allOf( + withId(android.R.id.widget_frame), + hasSibling( + allOf( + withClassName(Matchers.equalTo(RelativeLayout::class.java.name)), + withChild( + withText(id) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityGeneralTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityGeneralTest.kt new file mode 100644 index 0000000..532bd00 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityGeneralTest.kt @@ -0,0 +1,179 @@ +package bou.amine.apps.readerforselfossv2.android.settings + +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.replaceText +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.isFocused +import androidx.test.espresso.matcher.ViewMatchers.isRoot +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 bou.amine.apps.readerforselfossv2.android.LoginActivity +import bou.amine.apps.readerforselfossv2.android.R +import bou.amine.apps.readerforselfossv2.android.changeAndCancelSetting +import bou.amine.apps.readerforselfossv2.android.changeAndSaveSetting +import bou.amine.apps.readerforselfossv2.android.loginAndInitHome +import bou.amine.apps.readerforselfossv2.android.withSettingsCheckboxFrame +import bou.amine.apps.readerforselfossv2.android.withSettingsCheckboxWidget +import org.hamcrest.CoreMatchers.allOf +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 SettingsActivityGeneralTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + @Before + fun init() { + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.title_activity_settings)).perform(click()) + onView(withText(R.string.pref_header_general)).perform(click()) + } + + @Test + fun testGeneral() { + onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed())) + onView( + withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title) + ).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed())) + onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check( + matches( + allOf( + isDisplayed(), isChecked() + ) + ) + ) + onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( + matches( + isEnabled() + ) + ) + onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed())) + onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxWidget(R.string.card_height_title)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxFrame(R.string.card_height_title)).check( + matches( + not(isEnabled()) + ) + ) + onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check( + matches( + allOf( + isDisplayed(), isChecked() + ) + ) + ) + onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + } + + @Test + fun testGeneralActionsNumberItems() { + onView(withText(R.string.pref_api_items_number_title)).perform(click()) + onView(withId(android.R.id.edit)).check(matches(isFocused())) + + // Value check + onView( + withId(android.R.id.edit) + ).perform(replaceText("AVC")) + .check(matches(withText(""))) + // TODO: should check message error. Not working for api level 30+ + onView( + withId(android.R.id.edit) + ).perform(replaceText("-1")) + .check(matches(withText(""))) + // TODO: should check message error. Not working for api level 30+ + onView( + withId(android.R.id.edit) + ).perform(replaceText("300")) + .check(matches(withText(""))) + onView( + withId(android.R.id.edit) + ).perform(typeTextIntoFocusedView("300")) + .check(matches(withText("30"))) + onView( + withId(android.R.id.edit) + ).perform(replaceText("10")) + .check(matches(withText("10"))) + onView(isRoot()).perform(ViewActions.pressBack()) + + // Value saving + changeAndCancelSetting("20", "10") { + onView(withText(R.string.pref_api_items_number_title)).perform(click()) + } + changeAndSaveSetting("20", "10") { + onView(withText(R.string.pref_api_items_number_title)).perform(click()) + } + } + + @Test + fun testGeneralActionsCheckboxes() { + // article viewer settings + onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( + matches( + isEnabled() + ) + ) + onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click()) + onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( + matches( + not(isEnabled()) + ) + ) + + onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled()))) + onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click()) + onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityOfflineTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityOfflineTest.kt new file mode 100644 index 0000000..69bc5ea --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityOfflineTest.kt @@ -0,0 +1,176 @@ +package bou.amine.apps.readerforselfossv2.android.settings + +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.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled +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 bou.amine.apps.readerforselfossv2.android.LoginActivity +import bou.amine.apps.readerforselfossv2.android.R +import bou.amine.apps.readerforselfossv2.android.changeAndCancelSetting +import bou.amine.apps.readerforselfossv2.android.changeAndSaveSetting +import bou.amine.apps.readerforselfossv2.android.loginAndInitHome +import bou.amine.apps.readerforselfossv2.android.withSettingsCheckboxFrame +import bou.amine.apps.readerforselfossv2.android.withSettingsCheckboxWidget +import org.hamcrest.CoreMatchers.allOf +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 SettingsActivityOfflineTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + lateinit var context: Context + + @Before + fun init() { + activityRule.scenario.onActivity { activity -> + context = activity.window.context + } + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.title_activity_settings)).perform(click()) + onView(withText(R.string.pref_header_offline)).perform(click()) + } + + @Test + fun testOffline() { + onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( + matches( + isEnabled() + ) + ) + onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( + matches( + allOf(isNotEnabled(), isDisplayed()) + ) + ) + + onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( + matches( + isNotEnabled() + ) + ) + onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check( + matches( + allOf( + isDisplayed(), not(isChecked()) + ) + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( + matches( + isNotEnabled() + ) + ) + onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check( + matches( + allOf( + isDisplayed(), isChecked() + ) + ) + ) + } + + @Test + fun testOfflineActions() { + onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed())) + onView(withText(R.string.pref_switch_items_caching)).perform(click()) + onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed())) + onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( + matches( + isEnabled() + ) + ) + onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( + matches( + isNotEnabled() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( + matches( + isNotEnabled() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( + matches( + isNotEnabled() + ) + ) + + onView(withText(R.string.pref_switch_periodic_refresh_off)).check( + matches( + isDisplayed() + ) + ) + onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click()) + onView(withText(R.string.pref_switch_periodic_refresh_on)).check( + matches( + isDisplayed() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check( + matches( + isEnabled() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( + matches( + isEnabled() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( + matches( + isEnabled() + ) + ) + changeAndCancelSetting("360", "123") { + onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click()) + } + changeAndSaveSetting("360", "123") { + onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click()) + } + onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).perform(click()) + onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click()) + onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click()) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityReaderTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityReaderTest.kt new file mode 100644 index 0000000..5d66df9 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityReaderTest.kt @@ -0,0 +1,93 @@ +package bou.amine.apps.readerforselfossv2.android.settings + +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.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +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 bou.amine.apps.readerforselfossv2.android.LoginActivity +import bou.amine.apps.readerforselfossv2.android.R +import bou.amine.apps.readerforselfossv2.android.changeAndCancelSetting +import bou.amine.apps.readerforselfossv2.android.changeAndSaveSetting +import bou.amine.apps.readerforselfossv2.android.loginAndInitHome +import bou.amine.apps.readerforselfossv2.android.testPreferencesFromArray +import bou.amine.apps.readerforselfossv2.android.withSettingsCheckboxFrame +import org.hamcrest.CoreMatchers.allOf +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 SettingsActivityReaderTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + lateinit var context: Context + + @Before + fun init() { + activityRule.scenario.onActivity { activity -> + context = activity.window.context + } + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.title_activity_settings)).perform(click()) + onView(withText(R.string.pref_header_viewer)).perform(click()) + } + + @Test + fun testReader() { + onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check( + matches( + allOf( + isDisplayed(), not( + isChecked() + ) + ) + ) + ) + onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed())) + onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed())) + } + + @Test + fun testReaderActions() { + onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check( + matches( + isDisplayed() + ) + ) + onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click()) + onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check( + matches( + isDisplayed() + ) + ) + + onView(withText(R.string.pref_content_reader_font_size)).perform(click()) + changeAndCancelSetting("16", "10") { + onView(withText(R.string.pref_content_reader_font_size)).perform(click()) + } + changeAndSaveSetting("16", "10") { + onView(withText(R.string.pref_content_reader_font_size)).perform(click()) + } + + testPreferencesFromArray(context, R.array.preloaded_fonts_values) { + onView(withText(R.string.settings_reader_font)).perform(click()) + } + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityTest.kt new file mode 100644 index 0000000..7b673dd --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivityTest.kt @@ -0,0 +1,110 @@ +package bou.amine.apps.readerforselfossv2.android.settings + +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.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isSelected +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 bou.amine.apps.readerforselfossv2.android.LoginActivity +import bou.amine.apps.readerforselfossv2.android.R +import bou.amine.apps.readerforselfossv2.android.changeAndCancelSetting +import bou.amine.apps.readerforselfossv2.android.changeAndSaveSetting +import bou.amine.apps.readerforselfossv2.android.loginAndInitHome +import bou.amine.apps.readerforselfossv2.android.testPreferencesFromArray +import org.hamcrest.CoreMatchers.allOf +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 SettingsActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + lateinit var context: Context + + @Before + fun init() { + activityRule.scenario.onActivity { activity -> + context = activity.window.context + } + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.title_activity_settings)).perform(click()) + } + + + @Test + fun testAllSettings() { + + onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_theme)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_links)).check(matches(isDisplayed())) + onView(withText(R.string.pref_switch_disable_acra)).check( + matches( + allOf( + isDisplayed(), + not(isSelected()) + ) + ) + ) + onView(withText(R.string.action_about)).check(matches(isDisplayed())) + } + + + @Test + fun testThemes() { + testPreferencesFromArray(context, R.array.ModeTitles) { + onView(withText(R.string.pref_header_theme)).perform(click()) + } + } + + + @Test + fun testExperimentail() { + onView(withText(R.string.pref_header_experimental)).perform(click()) + changeAndCancelSetting("", "10") { + onView(withText(R.string.pref_api_timeout)).perform(click()) + } + changeAndSaveSetting("", "10") { + onView(withText(R.string.pref_api_timeout)).perform(click()) + } + } + + + @Test + fun testBugReports() { + onView(withText(R.string.pref_switch_disable_acra)).perform(click()) + } + + + @Test + fun testLinks() { + onView(withText(R.string.pref_header_links)).perform(click()) + onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed())) + onView(withText(R.string.issue_tracker_summary)).check(matches(isDisplayed())) + onView(withText(R.string.source_code)).check(matches(isDisplayed())) + onView(withText(R.string.translation)).check(matches(isDisplayed())) + } + + + @Test + fun testAbout() { + onView(withText(R.string.action_about)).perform(click()) + onView(withText("ACRA")).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index a60904e..5b63a33 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -29,6 +29,7 @@ import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.model.SelfossModel @@ -84,7 +85,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar repository.offlineOverride = intent.getBooleanExtra("startOffline", false) if (fromTabShortcut) { - elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) + elementsShown = + ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) } setContentView(view) @@ -96,8 +98,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar handleSwipeRefreshLayout() if (appSettingsService.isItemCachingEnabled()) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.tryToCacheItemsAndGetNewOnes() + CountingIdlingResourceSingleton.decrement() } } } @@ -111,9 +115,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar binding.swipeRefreshLayout.setOnRefreshListener { repository.offlineOverride = false lastFetchDone = false + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { getElementsAccordingToTab() binding.swipeRefreshLayout.isRefreshing = false + CountingIdlingResourceSingleton.decrement() } } @@ -274,9 +280,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleRecurringTask() - + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.handleDBActions() + CountingIdlingResourceSingleton.decrement() } getElementsAccordingToTab() @@ -315,6 +322,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar ) binding.recyclerView.layoutManager = layoutManager } + is GridLayoutManager -> if (appSettingsService.isCardViewEnabled()) { layoutManager = @@ -326,6 +334,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS binding.recyclerView.layoutManager = layoutManager } + else -> if (currentManager == null) { if (!appSettingsService.isCardViewEnabled()) { @@ -362,12 +371,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } else { layoutManager.scrollToPositionWithOffset(0, 0) } + is GridLayoutManager -> if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { getElementsAccordingToTab() } else { layoutManager.scrollToPositionWithOffset(0, 0) } + else -> Unit } } @@ -420,6 +431,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar manager.findLastCompletelyVisibleItemPositions( null, ).last() + is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() else -> 0 } @@ -448,6 +460,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar appendResults: Boolean, itemType: ItemType, ) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { binding.swipeRefreshLayout.isRefreshing = true repository.displayedItems = itemType @@ -459,6 +472,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } binding.swipeRefreshLayout.isRefreshing = false handleListResult() + CountingIdlingResourceSingleton.decrement() } } @@ -469,8 +483,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar when (oldManager) { is StaggeredGridLayoutManager -> oldManager.findFirstCompletelyVisibleItemPositions(null).last() + is GridLayoutManager -> oldManager.findFirstCompletelyVisibleItemPosition() + else -> 0 } } @@ -511,8 +527,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar private fun reloadBadges() { if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.IO).launch { repository.reloadBadges() + CountingIdlingResourceSingleton.decrement() } } } @@ -548,7 +566,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.getActionView() as SearchView + val searchView = searchItem.actionView as SearchView searchView.setOnQueryTextListener(this) return true @@ -571,18 +589,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.issue_tracker -> { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) + val browserIntent = + Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) startActivity(browserIntent) return true } + R.id.action_filter -> { val filterSheetFragment = FilterSheetFragment() filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG) return true } + R.id.refresh -> { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val updatedRemote = repository.updateRemote() if (updatedRemote) { @@ -599,15 +621,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar Toast.LENGTH_SHORT, ).show() } + CountingIdlingResourceSingleton.decrement() } } return true } + R.id.readAll -> { if (elementsShown == ItemType.UNREAD) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { binding.swipeRefreshLayout.isRefreshing = true - + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val success = repository.markAllAsRead(items) if (success) { @@ -628,13 +652,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } handleListResult() binding.swipeRefreshLayout.isRefreshing = false + CountingIdlingResourceSingleton.decrement() + } } } return true } + R.id.action_disconnect -> { - needsConfirmation(R.string.confirm_disconnect_title, R.string.confirm_disconnect_description) { + needsConfirmation( + R.string.confirm_disconnect_title, + R.string.confirm_disconnect_description + ) { runBlocking { repository.logout() } @@ -644,14 +674,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } return true } + R.id.action_settings -> { settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) return true } + R.id.action_sources -> { startActivity(Intent(this, SourcesActivity::class.java)) return true } + else -> return super.onOptionsItemSelected(item) } } @@ -678,14 +711,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar .build() val backgroundWork = - PeriodicWorkRequestBuilder(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES) + PeriodicWorkRequestBuilder( + appSettingsService.getRefreshMinutes(), + TimeUnit.MINUTES + ) .setConstraints(myConstraints) .addTag("selfoss-loading") .build() WorkManager.getInstance( baseContext, - ).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork) + ).enqueueUniquePeriodicWork( + "selfoss-loading", + ExistingPeriodicWorkPolicy.KEEP, + backgroundWork + ) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index dd78a4b..81b0b5a 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.service.AppSettingsService @@ -102,9 +103,14 @@ class LoginActivity : AppCompatActivity(), DIAware { } private fun goToMain() { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.updateApiInformation() - ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) + ACRA.errorReporter.putCustomData( + "SELFOSS_API_VERSION", + appSettingsService.getApiVersion().toString() + ) + CountingIdlingResourceSingleton.decrement() } val intent = Intent(this, HomeActivity::class.java) startActivity(intent) @@ -139,6 +145,7 @@ class LoginActivity : AppCompatActivity(), DIAware { repository.refreshLoginInformation(url, login, password) + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { try { repository.updateApiInformation() @@ -165,6 +172,7 @@ class LoginActivity : AppCompatActivity(), DIAware { preferenceError() } showProgress(false) + CountingIdlingResourceSingleton.decrement() } } @@ -261,20 +269,25 @@ class LoginActivity : AppCompatActivity(), DIAware { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.issue_tracker -> { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) + val browserIntent = + Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) startActivity(browserIntent) return true } + R.id.about -> { LibsBuilder() .withAboutIconShown(true) .withAboutVersionShown(true) - .withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl) - .withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl) + .withAboutSpecial2("Bug reports") + .withAboutSpecial2Description(AppSettingsService.trackerUrl) + .withAboutSpecial1("Project Page") + .withAboutSpecial1Description(AppSettingsService.sourceUrl) .start(this) true } + else -> super.onOptionsItemSelected(item) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index b655848..f322627 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository import kotlinx.coroutines.CoroutineScope @@ -36,7 +37,8 @@ class SourcesActivity : AppCompatActivity(), DIAware { supportActionBar?.setDisplayShowHomeEnabled(true) binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark) - binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) + binding.fab.backgroundTintList = + ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) } override fun onStop() { @@ -53,6 +55,7 @@ class SourcesActivity : AppCompatActivity(), DIAware { binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = mLayoutManager + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val response = repository.getSourcesDetails() if (response.isNotEmpty()) { @@ -71,10 +74,11 @@ class SourcesActivity : AppCompatActivity(), DIAware { Toast.LENGTH_SHORT, ).show() } + CountingIdlingResourceSingleton.decrement() } binding.fab.setOnClickListener { startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java)) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt new file mode 100644 index 0000000..089ba63 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt @@ -0,0 +1,21 @@ +package bou.amine.apps.readerforselfossv2.android.testing + +import androidx.test.espresso.idling.CountingIdlingResource + +object CountingIdlingResourceSingleton { + + private const val RESOURCE = "GLOBAL" + + @JvmField + val countingIdlingResource = CountingIdlingResource(RESOURCE) + + fun increment() { + countingIdlingResource.increment() + } + + fun decrement() { + if (!countingIdlingResource.isIdleNow) { + countingIdlingResource.decrement() + } + } +} \ 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