test: test
Some checks failed
Check master code / checkout (push) Failing after 19m42s

This commit is contained in:
aminecmi 2024-12-22 21:05:47 +01:00
parent 6fb7c4a073
commit 6137ff3372
14 changed files with 466 additions and 100 deletions

View File

@ -19,6 +19,6 @@ jobs:
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Configure gradle... - 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 - name: Build and test
run: ./gradlew build --stacktrace run: ./gradlew build --stacktrace

View File

@ -10,10 +10,10 @@ jobs:
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
# with: with:
# fetch-depth: 0 fetch-depth: 0
# - name: Fetch tags - name: Fetch tags
# run: git fetch --tags -p run: git fetch --tags -p
- name: Init compose - name: Init compose
uses: KengoTODA/actions-setup-docker-compose@v1 uses: KengoTODA/actions-setup-docker-compose@v1
with: with:
@ -21,20 +21,35 @@ jobs:
- name: run selfoss - name: run selfoss
run: | run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
sleep 1m
response=$(curl 172.17.0.1:8888/api/about) response=$(curl 172.17.0.1:8888/api/about)
echo $response echo $response
# - uses: actions/setup-java@v4 - uses: actions/setup-java@v4
# with: with:
# distribution: 'temurin' distribution: 'temurin'
# java-version: '17' java-version: '17'
# - name: Setup Android SDK - name: Setup Android SDK
# uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
# - name: Configure gradle... - 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 - name: ui tests
# run: ./gradlew build --stacktrace 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 - name: Clean
if: success() || failure() if: always()
run: | run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop docker compose -f .gitea/workflows/assets/docker-compose.yml stop

5
.gitignore vendored
View File

@ -320,4 +320,7 @@ fabric.properties
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift # End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
crowdin.properties crowdin.properties
.kotlin/
build-cache/

View File

@ -1 +1,2 @@
/build /build
.kotlin/

View File

@ -84,6 +84,7 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
} }
packaging { packaging {
resources { resources {
@ -107,6 +108,10 @@ android {
} }
} }
namespace = "bou.amine.apps.readerforselfossv2.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("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") 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-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion")

View File

@ -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())
}

View File

@ -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<Context>()
)
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<Context>()
)
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<Context>()
)
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<Context>()
)
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<Context>()
)
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<Context>()
)
/*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<Context>()
)*/
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<Context>()
)
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"))
}
}

View File

@ -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()))
}
}

View File

@ -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<View?> {
return object : TypeSafeMatcher<View?>() {
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<Root> {
return isPlatformPopup()
}
fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
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<View>? {
return allOf(
withResourceName("fixed_bottom_navigation_icon"),
withParent(
allOf(
withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id))
)
)
)
}

View File

@ -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.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity 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.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
@ -84,7 +85,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
repository.offlineOverride = intent.getBooleanExtra("startOffline", false) repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) { if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) elementsShown =
ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
} }
setContentView(view) setContentView(view)
@ -96,8 +98,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleSwipeRefreshLayout() handleSwipeRefreshLayout()
if (appSettingsService.isItemCachingEnabled()) { if (appSettingsService.isItemCachingEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@ -111,9 +115,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false repository.offlineOverride = false
lastFetchDone = false lastFetchDone = false
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab() getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -274,9 +280,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask() handleRecurringTask()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.handleDBActions() repository.handleDBActions()
CountingIdlingResourceSingleton.decrement()
} }
getElementsAccordingToTab() getElementsAccordingToTab()
@ -315,6 +322,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
) )
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
is GridLayoutManager -> is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
layoutManager = layoutManager =
@ -326,6 +334,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
else -> else ->
if (currentManager == null) { if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
@ -362,12 +371,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} else { } else {
layoutManager.scrollToPositionWithOffset(0, 0) layoutManager.scrollToPositionWithOffset(0, 0)
} }
is GridLayoutManager -> is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
layoutManager.scrollToPositionWithOffset(0, 0) layoutManager.scrollToPositionWithOffset(0, 0)
} }
else -> Unit else -> Unit
} }
} }
@ -420,6 +431,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
manager.findLastCompletelyVisibleItemPositions( manager.findLastCompletelyVisibleItemPositions(
null, null,
).last() ).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
@ -448,6 +460,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
appendResults: Boolean, appendResults: Boolean,
itemType: ItemType, itemType: ItemType,
) { ) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType repository.displayedItems = itemType
@ -459,6 +472,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
handleListResult() handleListResult()
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -469,8 +483,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (oldManager) { when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last() oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager -> is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition() oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
} }
@ -511,8 +527,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() { private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges() repository.reloadBadges()
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@ -548,7 +566,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.getActionView() as SearchView val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(this) searchView.setOnQueryTextListener(this)
return true return true
@ -571,18 +589,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> { 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) startActivity(browserIntent)
return true return true
} }
R.id.action_filter -> { R.id.action_filter -> {
val filterSheetFragment = FilterSheetFragment() val filterSheetFragment = FilterSheetFragment()
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG) filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
return true return true
} }
R.id.refresh -> { R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
@ -599,15 +621,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
).show() ).show()
} }
CountingIdlingResourceSingleton.decrement()
} }
} }
return true return true
} }
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) { if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
@ -628,13 +652,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
handleListResult() handleListResult()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
return true return true
} }
R.id.action_disconnect -> { 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 { runBlocking {
repository.logout() repository.logout()
} }
@ -644,14 +674,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
return true return true
} }
R.id.action_settings -> { R.id.action_settings -> {
settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
return true return true
} }
R.id.action_sources -> { R.id.action_sources -> {
startActivity(Intent(this, SourcesActivity::class.java)) startActivity(Intent(this, SourcesActivity::class.java))
return true return true
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
} }
@ -678,14 +711,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.build() .build()
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES) PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES
)
.setConstraints(myConstraints) .setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance( WorkManager.getInstance(
baseContext, baseContext,
).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork) ).enqueueUniquePeriodicWork(
"selfoss-loading",
ExistingPeriodicWorkPolicy.KEEP,
backgroundWork
)
} }
} }
} }

View File

@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding 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.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@ -102,9 +103,14 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun goToMain() { private fun goToMain() {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation() 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) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)
@ -139,6 +145,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password) repository.refreshLoginInformation(url, login, password)
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
repository.updateApiInformation() repository.updateApiInformation()
@ -165,6 +172,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
preferenceError() preferenceError()
} }
showProgress(false) showProgress(false)
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -261,20 +269,25 @@ class LoginActivity : AppCompatActivity(), DIAware {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.issue_tracker -> { 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) startActivity(browserIntent)
return true return true
} }
R.id.about -> { R.id.about -> {
LibsBuilder() LibsBuilder()
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl) .withAboutSpecial2("Bug reports")
.withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl) .withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl)
.start(this) .start(this)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
} }

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -18,6 +18,7 @@ kotlin.code.style=official
#Android #Android
android.useAndroidX=true android.useAndroidX=true
#android.nonTransitiveRClass=true #android.nonTransitiveRClass=true
android.enableJetifier=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
#MPP #MPP
kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.enableCInteropCommonization=true
@ -25,3 +26,4 @@ org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none kotlin.native.cacheKind.iosX64=none
org.gradle.configureondemand=true