diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml index 57a1b31..3b46f4c 100644 --- a/.gitea/workflows/on_pr.yml +++ b/.gitea/workflows/on_pr.yml @@ -48,7 +48,7 @@ jobs: adb logcat -G 16M ./gradlew JacocoDebugCodeCoverage || true ./gradlew androidApp:fetchScreenshots - adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt + adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt - uses: actions/upload-artifact@v3 if: always() with: diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt index 2dea4a4..746d3bb 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt @@ -93,5 +93,6 @@ class `1-LoginActivityTest` : WithANRException() { performLogin() onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText("OK")).perform(click()) + checkHomeLoadingDone() } } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt index b5f40ea..14d468e 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt @@ -1,6 +1,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -14,7 +15,9 @@ 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.hamcrest.CoreMatchers.not +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test @@ -29,6 +32,14 @@ class `2-HomeActivityTest` : WithANRException() { @get:Rule val activityRule = ActivityScenarioRule(HomeActivity::class.java) + @Before + fun registerIdlingResource() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) + checkHomeLoadingDone() + } + @Test fun testMenu() { onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt index d9a28e9..e2d0267 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context 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.isDisplayed @@ -10,6 +11,7 @@ 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.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.Before @@ -31,6 +33,9 @@ class `3-SettingsActivityTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) openMenu() onView(withText(R.string.title_activity_settings)).perform(click()) } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt index f26bbc7..7953e93 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt @@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText @@ -19,6 +20,7 @@ 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.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.Before @@ -38,6 +40,9 @@ class `4-SettingsActivityGeneralTest` : WithANRException() { @Before fun init() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext(), ) diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt index 3cf615e..0b20fff 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -13,6 +14,7 @@ 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.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.After @@ -35,6 +37,9 @@ class `5-SettingsActivityReaderTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) onView(withText(R.string.pref_header_viewer)).perform(click()) } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt index e7a66ae..e785bc3 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -15,6 +16,7 @@ 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.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.After @@ -37,6 +39,9 @@ class `6-SettingsActivityOfflineTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) onView(withText(R.string.pref_header_offline)).perform(click()) } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt index f751ab2..5adbd7c 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.swipeDown @@ -14,6 +15,7 @@ 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 @@ -32,6 +34,9 @@ class `7-SourcesActivityTest` : WithANRException() { @Before fun init() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) sourceName = UUID.randomUUID().toString().substring(0, 15) goToSources() 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 index 9f19d07..c3351f7 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -15,6 +15,7 @@ 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.base.DefaultFailureHandler +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isNotChecked @@ -24,6 +25,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.not import org.hamcrest.Matchers.hasToString import org.junit.BeforeClass import java.io.BufferedOutputStream @@ -139,6 +141,10 @@ fun testAddSourceWithUrl( onView(withText(sourceName)).check(matches(isDisplayed())) } +fun checkHomeLoadingDone() { + onView(withId(R.id.swipeRefreshLayout)).inRoot(not(isDialog())).perform(waitUntilNotLoading(300000)) +} + @Suppress("detekt:UtilityClassWithPublicConstructor") open class WithANRException { companion object { 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 index 207afcf..6b2b1fd 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt @@ -8,9 +8,14 @@ import android.widget.RelativeLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.graphics.drawable.toBitmap +import androidx.core.view.isVisible +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.PerformException import androidx.test.espresso.Root +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.withChild @@ -19,11 +24,15 @@ 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 androidx.test.espresso.util.HumanReadables +import androidx.test.espresso.util.TreeIterables import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.any import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.Matchers import org.hamcrest.TypeSafeMatcher +import java.util.concurrent.TimeoutException fun withError( @StringRes id: Int, @@ -44,6 +53,47 @@ fun withError( } } +fun waitUntilNotLoading(millis: Long): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = any(View::class.java) + + override fun getDescription(): String = "wait for a specific view is not loading during $millis millis." + + override fun perform( + uiController: UiController, + view: View?, + ) { + uiController.loopMainThreadUntilIdle() + val startTime = System.currentTimeMillis() + val endTime = startTime + millis + + do { + // either the empty view is displayed + for (child in TreeIterables.breadthFirstViewTraversal(view)) { + // found view with required ID + if (withId(R.id.emptyText).matches(child) && child.isVisible) { + return + } + } + + // or the refresh layout is refreshing + if (view is SwipeRefreshLayout && !view.isRefreshing) { + return + } + uiController.loopMainThreadForAtLeast(100) + } while (System.currentTimeMillis() < endTime) + + // timeout happens + throw PerformException + .Builder() + .withActionDescription(this.description) + .withViewDescription(HumanReadables.describe(view)) + .withCause(TimeoutException()) + .build() + } + } +} + fun isPopupWindow(): Matcher = isPlatformPopup() fun withDrawable(