Compare commits

..

No commits in common. "testing" and "master" have entirely different histories.

35 changed files with 81 additions and 1388 deletions

View File

@ -1,10 +0,0 @@
version: '3'
services:
selfoss:
container_name: selfoss
image: rsprta/selfoss
network_mode: "host"
ports:
- "8888:8888"

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" >> ~/.gradle/gradle.properties 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 - name: Build and test
run: ./gradlew build --stacktrace run: ./gradlew build --stacktrace

View File

@ -1,45 +0,0 @@
name: Check master code
on:
push:
branches:
- testing
jobs:
checkout:
runs-on: ubuntu-latest
steps:
- 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
- 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: Robolectric tests
run: |
./gradlew :androidApp:testGithubConfigDebugUnitTest --tests "bou.amine.apps.readerforselfossv2.android.tests.robolectric.*"
- uses: actions/upload-artifact@v3
if: failure()
with:
name: robot-tests
path: androidApp/build/test-results/testGithubConfigDebugUnitTest
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop

3
.gitignore vendored
View File

@ -321,6 +321,3 @@ fabric.properties
crowdin.properties crowdin.properties
.kotlin/
build-cache/

View File

@ -1,10 +1,3 @@
**v124123421
- fix: Trying to fix the serialization issue.
- Changelog for v124113311
--------------------------------------------------------------------
**v124113311 **v124113311
- chore: update versions. (#165) - chore: update versions. (#165)

View File

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

View File

@ -84,7 +84,6 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
} }
packaging { packaging {
resources { resources {
@ -108,16 +107,6 @@ android {
} }
} }
namespace = "bou.amine.apps.readerforselfossv2.android" namespace = "bou.amine.apps.readerforselfossv2.android"
testOptions {
animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
unitTests {
isIncludeAndroidResources = true
all {
it.systemProperty("robolectric.logging.enabled", true)
}
}
}
} }
@ -195,15 +184,6 @@ 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")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("org.mockito:mockito-core:5.14.2")
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

@ -1,90 +0,0 @@
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://10.0.2.2: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())))
}
}

View File

@ -1,137 +0,0 @@
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<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())))
}
}

View File

@ -1,83 +0,0 @@
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()))
}
}

View File

@ -1,172 +0,0 @@
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.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 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()))
}
}

View File

@ -1,169 +0,0 @@
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.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 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())
}
}

View File

@ -1,86 +0,0 @@
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.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 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())
}
}
}

View File

@ -1,104 +0,0 @@
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.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 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<Context>()
)
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()))
}
}

View File

@ -1,65 +0,0 @@
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<Context>()
)
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()))
}
}

View File

@ -1,101 +0,0 @@
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<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
}
if (view.error == null) {
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))
)
)
)
}
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
return allOf(
withId(android.R.id.switch_widget),
withParent(
withSettingsCheckboxFrame(id)
)
)
}
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? {
return allOf(
withId(android.R.id.widget_frame),
hasSibling(
allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild(
withText(id)
)
)
)
)
}

View File

@ -29,7 +29,6 @@ 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
@ -85,8 +84,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
repository.offlineOverride = intent.getBooleanExtra("startOffline", false) repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) { if (fromTabShortcut) {
elementsShown = elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
} }
setContentView(view) setContentView(view)
@ -98,10 +96,8 @@ 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()
} }
} }
} }
@ -115,11 +111,9 @@ 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()
} }
} }
@ -280,10 +274,9 @@ 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()
@ -322,7 +315,6 @@ 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 =
@ -334,7 +326,6 @@ 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()) {
@ -371,14 +362,12 @@ 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
} }
} }
@ -431,7 +420,6 @@ 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
} }
@ -460,7 +448,6 @@ 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
@ -472,7 +459,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
handleListResult() handleListResult()
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -483,10 +469,8 @@ 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
} }
} }
@ -527,10 +511,8 @@ 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()
} }
} }
} }
@ -566,7 +548,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.actionView as SearchView val searchView = searchItem.getActionView() as SearchView
searchView.setOnQueryTextListener(this) searchView.setOnQueryTextListener(this)
return true return true
@ -589,22 +571,18 @@ 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 = val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
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) {
@ -621,17 +599,15 @@ 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) {
@ -652,19 +628,13 @@ 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( needsConfirmation(R.string.confirm_disconnect_title, R.string.confirm_disconnect_description) {
R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description
) {
runBlocking { runBlocking {
repository.logout() repository.logout()
} }
@ -674,17 +644,14 @@ 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)
} }
} }
@ -711,21 +678,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.build() .build()
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>( PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES)
appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES
)
.setConstraints(myConstraints) .setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance( WorkManager.getInstance(
baseContext, baseContext,
).enqueueUniquePeriodicWork( ).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
"selfoss-loading",
ExistingPeriodicWorkPolicy.KEEP,
backgroundWork
)
} }
} }
} }

View File

@ -17,7 +17,6 @@ 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
@ -103,14 +102,9 @@ 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( ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
"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)
@ -145,7 +139,6 @@ 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()
@ -172,7 +165,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
preferenceError() preferenceError()
} }
showProgress(false) showProgress(false)
CountingIdlingResourceSingleton.decrement()
} }
} }
@ -269,24 +261,19 @@ 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 = val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
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") .withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial2Description(AppSettingsService.trackerUrl) .withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl)
.withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl)
.start(this) .start(this)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@ -10,7 +10,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
@ -30,27 +29,23 @@ import org.acra.config.toast
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.kodein.di.DI import org.kodein.di.*
import org.kodein.di.DIAware
import org.kodein.di.bind
import org.kodein.di.instance
import org.kodein.di.singleton
class MyApp : MultiDexApplication(), DIAware { class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) } bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with bind<Repository>() with
singleton { singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, isConnectionAvailable,
instance(), instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }

View File

@ -8,7 +8,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding 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.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -37,8 +36,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark) binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
binding.fab.backgroundTintList = binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
} }
override fun onStop() { override fun onStop() {
@ -55,7 +53,6 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSourcesDetails() val response = repository.getSourcesDetails()
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
@ -74,7 +71,6 @@ class SourcesActivity : AppCompatActivity(), DIAware {
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
).show() ).show()
} }
CountingIdlingResourceSingleton.decrement()
} }
binding.fab.setOnClickListener { binding.fab.setOnClickListener {

View File

@ -1,21 +0,0 @@
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,19 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.testing
import android.os.Build
class TestingHelper {
fun isUnitTest(): Boolean {
var device = Build.DEVICE
var product = Build.PRODUCT
if (device == null) {
device = ""
}
if (product == null) {
product = ""
}
return device == "robolectric" && product == "robolectric"
}
}

View File

@ -1,11 +1,11 @@
package bou.amine.apps.readerforselfossv2.repository package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.utils.toParsedDate import bou.amine.apps.readerforselfossv2.utils.DateUtils
import junit.framework.TestCase.assertEquals
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlin.test.Test import org.junit.Test
import kotlin.test.assertEquals
class DatesTest { class DatesTest {
private val newVersionDateVariant = "2022-12-24T17:00:08+00" private val newVersionDateVariant = "2022-12-24T17:00:08+00"
@ -17,7 +17,7 @@ class DatesTest {
@Test @Test
fun new_version_date_should_be_parsed() { fun new_version_date_should_be_parsed() {
val date = newVersionDate.toParsedDate() val date = DateUtils.parseDate(newVersionDate)
val expected = val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
@ -26,7 +26,7 @@ class DatesTest {
} }
@Test @Test
fun new_version_date2_should_be_parsed() { fun new_version_date2_should_be_parsed() {
val date = newVersionDate2.toParsedDate() val date = DateUtils.parseDate(newVersionDate2)
val expected = val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
@ -36,7 +36,7 @@ class DatesTest {
@Test @Test
fun old_version_date_should_be_parsed() { fun old_version_date_should_be_parsed() {
val date = oldVersionDate.toParsedDate() val date = DateUtils.parseDate(oldVersionDate)
val expected = val expected =
LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
@ -46,7 +46,7 @@ class DatesTest {
@Test @Test
fun old_version_variant_date_should_be_parsed() { fun old_version_variant_date_should_be_parsed() {
val date = oldVersionDateVariant.toParsedDate() val date = DateUtils.parseDate(oldVersionDateVariant)
val expected = val expected =
LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
@ -56,7 +56,7 @@ class DatesTest {
@Test @Test
fun new_version_variant_date_should_be_parsed() { fun new_version_variant_date_should_be_parsed() {
val date = newVersionDateVariant.toParsedDate() val date = DateUtils.parseDate(newVersionDateVariant)
val expected = val expected =
LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()

View File

@ -1,81 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.content.Intent
import android.widget.Button
import android.widget.EditText
import androidx.core.view.isVisible
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.R
import com.google.android.material.switchmaterial.SwitchMaterial
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
@RunWith(RobotElectriqueRunnerclass::class)
class LoginActivityTest {
@Test
fun login_shouldDisplay() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state
val activity = controller.get()
assert(activity.findViewById<EditText>(R.id.urlView).isVisible)
assert(activity.findViewById<SwitchMaterial>(R.id.selfSigned).isVisible)
assert(activity.findViewById<SwitchMaterial>(R.id.selfSigned).isChecked.not())
assert(activity.findViewById<SwitchMaterial>(R.id.withLogin).isVisible)
assert(activity.findViewById<SwitchMaterial>(R.id.withLogin).isChecked.not())
}
}
@Test
fun urlError() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state
val activity = controller.get()
val urlView = activity.findViewById<EditText>(R.id.urlView)
urlView.setText("172.17.0.1:8888")
activity.findViewById<Button>(R.id.signInButton).performClick()
urlView.performClick()
assertEquals(activity.getString(R.string.login_url_problem), urlView.error)
}
}
@Test
fun multiError() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state
val activity = controller.get()
val signInButton = activity.findViewById<Button>(R.id.signInButton)
repeat(3) { signInButton.performClick() }
// Vérifie que l'avertissement est affiché
assertEquals(activity.getString(R.string.text_wrong_url), dialogMessage())
}
}
@Test
fun connect() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
controller.setup() // Moves the Activity to the RESUMED state
val activity = controller.get()
val signInButton = activity.findViewById<Button>(R.id.signInButton)
val urlView = activity.findViewById<EditText>(R.id.urlView)
urlView.setText("http://10.0.2.2:8888")
signInButton.performClick()
val expectedIntent = Intent(activity, HomeActivity::class.java)
val actual = shadowOf(RuntimeEnvironment.application).nextStartedActivity
assertEquals(expectedIntent.component, actual.component)
}
}
}

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
class RobotElectriqueRunnerclass(testClass: Class<*>?) :
RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config {
return Config.Builder().setSdk(25, 30, 33).build()
}
}

View File

@ -1,9 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import android.widget.TextView
import org.robolectric.shadows.ShadowDialog
fun dialogMessage(): String {
val latestDialog = ShadowDialog.getLatestDialog()
return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
}

View File

@ -7,10 +7,10 @@ buildscript {
plugins { plugins {
//trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("8.7.3").apply(false) id("com.android.application").version("8.7.2").apply(false)
id("com.android.library").version("8.7.3").apply(false) id("com.android.library").version("8.7.2").apply(false)
id("org.jetbrains.kotlin.android").version("2.1.0").apply(false) id("org.jetbrains.kotlin.android").version("1.9.10").apply(false)
kotlin("multiplatform").version("2.1.0").apply(false) kotlin("multiplatform").version("1.9.10").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
id("org.jetbrains.kotlinx.kover").version("0.6.1").apply(true) id("org.jetbrains.kotlinx.kover").version("0.6.1").apply(true)
} }
@ -25,7 +25,7 @@ allprojects {
tasks.register("clean", Delete::class) { tasks.register("clean", Delete::class) {
delete(layout.buildDirectory) delete(rootProject.buildDir)
} }
koverMerged { koverMerged {

View File

@ -1,4 +0,0 @@
**v124123421**
- fix: Trying to fix the serialization issue.
- Changelog for v124113311

View File

@ -26,4 +26,3 @@ 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

View File

@ -173,7 +173,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "export JAVA_HOME=/Users/amine/.sdkman/candidates/java/17.0.8.1-jbr\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode --stacktrace\n"; shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */

View File

@ -52,9 +52,6 @@ kotlin {
// Sql // Sql
implementation(SqlDelight.runtime) implementation(SqlDelight.runtime)
// Sql
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
} }
} }
val commonTest by getting { val commonTest by getting {
@ -83,6 +80,10 @@ kotlin {
val iosArm64Main by getting val iosArm64Main by getting
// val iosSimulatorArm64Main by getting // val iosSimulatorArm64Main by getting
val iosMain by creating { val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
// iosSimulatorArm64Main.dependsOn(this)
dependencies { dependencies {
implementation(SqlDelight.native) implementation(SqlDelight.native)
@ -93,6 +94,10 @@ kotlin {
val iosArm64Test by getting val iosArm64Test by getting
// val iosSimulatorArm64Test by getting // val iosSimulatorArm64Test by getting
val iosTest by creating { val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
// iosSimulatorArm64Test.dependsOn(this)
} }
} }
} }

View File

@ -6,8 +6,33 @@ import kotlinx.datetime.*
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
// Possible formats are
// yyyy-mm-dd hh:mm:ss format
private val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
// yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
private val newVersionFormat = "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})[+-](\\d{2}(:\\d{2})?)?".toRegex()
// TODO: do not fix any more issues here. Move everything to plateform specific code.
actual fun parseDate(dateString: String): Long {
var isoDateString: String =
try {
if (dateString.matches(oldVersionFormat)) {
dateString.replace(" ", "T")
} else if (dateString.matches(newVersionFormat)) {
newVersionFormat.find(dateString)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $dateString")
} else {
throw Exception("Unrecognized format for $dateString")
}
} catch (e: Exception) {
throw Exception("parseDate failed for $dateString", e)
}
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
}
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {
val date = dateString.toParsedDate() val date = parseDate(dateString)
return " " + return " " +
DateUtils.getRelativeTimeSpanString( DateUtils.getRelativeTimeSpanString(

View File

@ -74,7 +74,7 @@ class Repository(
dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title } dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
} }
val itemsList = ArrayList(dbItems.map { it.toView() }) val itemsList = ArrayList(dbItems.map { it.toView() })
itemsList.sortByDescending { it.datetime.toParsedDate() } itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
fetchedItems = fetchedItems =
StatusAndData.succes( StatusAndData.succes(
itemsList, itemsList,

View File

@ -1,35 +1,9 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
fun String.toParsedDate(): Long {
// Possible formats are
// yyyy-mm-dd hh:mm:ss format
val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
// yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
val newVersionFormat = "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})[+-](\\d{2}(:\\d{2})?)?".toRegex()
val isoDateString: String =
try {
if (this.matches(oldVersionFormat)) {
this.replace(" ", "T")
} else if (this.matches(newVersionFormat)) {
newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
} else {
throw Exception("Unrecognized format for $this")
}
} catch (e: Exception) {
throw Exception("parseDate failed for $this", e)
}
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
}
expect class DateUtils() { expect class DateUtils() {
companion object { companion object {
fun parseDate(dateString: String): Long
fun parseRelativeDate(dateString: String): String fun parseRelativeDate(dateString: String): String
} }
} }

View File

@ -1,9 +0,0 @@
package bou.amine.apps.readerforselfossv2.utils
actual class DateUtils actual constructor() {
actual companion object {
actual fun parseRelativeDate(dateString: String): String {
TODO("Not yet implemented")
}
}
}