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..5becaf2 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,35 @@ 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 + profile: Nexus 6 + 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/outputs/androidTest-results/connected/debug/flavors/githubConfig + retention-days: 1 + overwrite: true + include-hidden-files: true - 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..6143904 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,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..1b323ed --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -0,0 +1,22 @@ +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://10.0.2.2:8888") + 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..ea036fa --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt @@ -0,0 +1,151 @@ +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.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) + + @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()))) + } + + @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")) + } + +} \ 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..371579d --- /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("http://10.0.2.2:8888") + 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/helpers.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt new file mode 100644 index 0000000..f7ae993 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt @@ -0,0 +1,69 @@ +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(@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 + } + 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)) + ) + ) + ) +} \ 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/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