From aad93ef7227f05c740efa87560acd6e3b7ff23a7 Mon Sep 17 00:00:00 2001 From: aminecmi Date: Sat, 21 Dec 2024 20:32:58 +0100 Subject: [PATCH 1/3] chore: update and use multiplatform datetime --- build.gradle.kts | 10 +-- gradle.properties | 1 - iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- shared/build.gradle.kts | 11 +--- .../readerforselfossv2/utils/DateUtils.kt | 27 +------- .../repository/RepositoryImpl.kt | 2 +- .../readerforselfossv2/utils/DateUtils.kt | 30 ++++++++- shared/src/commonTest/kotlin/DatesTest.kt | 66 +++++++++++++++++++ .../utils/DateUtils.iosSimulatorArm64.kt | 9 +++ 9 files changed, 114 insertions(+), 44 deletions(-) create mode 100644 shared/src/commonTest/kotlin/DatesTest.kt create mode 100644 shared/src/iosSimulatorArm64Main/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.iosSimulatorArm64.kt diff --git a/build.gradle.kts b/build.gradle.kts index ca581f3..34eff79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,10 +7,10 @@ buildscript { plugins { //trick: for the same plugin versions in all sub-modules - id("com.android.application").version("8.7.2").apply(false) - id("com.android.library").version("8.7.2").apply(false) - id("org.jetbrains.kotlin.android").version("1.9.10").apply(false) - kotlin("multiplatform").version("1.9.10").apply(false) + id("com.android.application").version("8.7.3").apply(false) + id("com.android.library").version("8.7.3").apply(false) + id("org.jetbrains.kotlin.android").version("2.1.0").apply(false) + kotlin("multiplatform").version("2.1.0").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("org.jetbrains.kotlinx.kover").version("0.6.1").apply(true) } @@ -25,7 +25,7 @@ allprojects { tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + delete(layout.buildDirectory) } koverMerged { diff --git a/gradle.properties b/gradle.properties index d40bc34..28fdf63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,6 @@ kotlin.code.style=official #Android android.useAndroidX=true #android.nonTransitiveRClass=true -android.enableJetifier=true android.nonTransitiveRClass=false #MPP kotlin.mpp.enableCInteropCommonization=true diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 279f7f1..1e703a0 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -173,7 +173,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "export JAVA_HOME=/Users/amine/.sdkman/candidates/java/17.0.8.1-jbr\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode --stacktrace\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index a019f19..0af0d6f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -52,6 +52,9 @@ kotlin { // Sql implementation(SqlDelight.runtime) + + // Sql + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") } } val commonTest by getting { @@ -80,10 +83,6 @@ kotlin { val iosArm64Main by getting // val iosSimulatorArm64Main by getting val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - // iosSimulatorArm64Main.dependsOn(this) dependencies { implementation(SqlDelight.native) @@ -94,10 +93,6 @@ kotlin { val iosArm64Test by getting // val iosSimulatorArm64Test by getting val iosTest by creating { - dependsOn(commonTest) - iosX64Test.dependsOn(this) - iosArm64Test.dependsOn(this) - // iosSimulatorArm64Test.dependsOn(this) } } } diff --git a/shared/src/androidMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt b/shared/src/androidMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt index 8c2b8e0..e5664fb 100644 --- a/shared/src/androidMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt +++ b/shared/src/androidMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt @@ -6,33 +6,8 @@ import kotlinx.datetime.* actual class DateUtils { 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 { - val date = parseDate(dateString) + val date = dateString.toParsedDate() return " " + DateUtils.getRelativeTimeSpanString( diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index 5a7433c..f84df1f 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -74,7 +74,7 @@ class Repository( dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title } } val itemsList = ArrayList(dbItems.map { it.toView() }) - itemsList.sortByDescending { DateUtils.parseDate(it.datetime) } + itemsList.sortByDescending { it.datetime.toParsedDate() } fetchedItems = StatusAndData.succes( itemsList, diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt index 0645a66..7cbcbbd 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt @@ -1,9 +1,35 @@ 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() { companion object { - fun parseDate(dateString: String): Long - fun parseRelativeDate(dateString: String): String } } diff --git a/shared/src/commonTest/kotlin/DatesTest.kt b/shared/src/commonTest/kotlin/DatesTest.kt new file mode 100644 index 0000000..e074d04 --- /dev/null +++ b/shared/src/commonTest/kotlin/DatesTest.kt @@ -0,0 +1,66 @@ +package bou.amine.apps.readerforselfossv2.repository + +import bou.amine.apps.readerforselfossv2.utils.toParsedDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlin.test.Test +import kotlin.test.assertEquals + +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 = newVersionDate.toParsedDate() + 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 = newVersionDate2.toParsedDate() + 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 = oldVersionDate.toParsedDate() + 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 = oldVersionDateVariant.toParsedDate() + 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 = newVersionDateVariant.toParsedDate() + val expected = + LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() + + assertEquals(expected, date) + } +} diff --git a/shared/src/iosSimulatorArm64Main/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.iosSimulatorArm64.kt b/shared/src/iosSimulatorArm64Main/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.iosSimulatorArm64.kt new file mode 100644 index 0000000..ee78316 --- /dev/null +++ b/shared/src/iosSimulatorArm64Main/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.iosSimulatorArm64.kt @@ -0,0 +1,9 @@ +package bou.amine.apps.readerforselfossv2.utils + +actual class DateUtils actual constructor() { + actual companion object { + actual fun parseRelativeDate(dateString: String): String { + TODO("Not yet implemented") + } + } +} \ No newline at end of file -- 2.34.1 From f54fcc3ba19b888cb01c137011cd17d3fa4abbf3 Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 29 Dec 2024 21:35:12 +0100 Subject: [PATCH 2/3] test: coverage --- .gitea/workflows/assets/docker-compose.yml | 10 + .gitea/workflows/common_build.yml | 4 +- .gitea/workflows/on_push_coverage.yml | 44 +++++ .gitignore | 5 +- androidApp/.gitignore | 1 + androidApp/build.gradle.kts | 17 ++ .../readerforselfossv2/android/CommonTests.kt | 90 +++++++++ .../android/HomeActivityTest.kt | 137 ++++++++++++++ .../android/LoginActivityTest.kt | 83 +++++++++ .../android/SettingsActivityGeneralTest.kt | 172 ++++++++++++++++++ .../android/SettingsActivityOfflineTest.kt | 169 +++++++++++++++++ .../android/SettingsActivityReaderTest.kt | 86 +++++++++ .../android/SettingsActivityTest.kt | 104 +++++++++++ .../android/SourcesActivityTest.kt | 65 +++++++ .../readerforselfossv2/android/helpers.kt | 101 ++++++++++ .../android/HomeActivity.kt | 58 +++++- .../android/LoginActivity.kt | 23 ++- .../apps/readerforselfossv2/android/MyApp.kt | 27 +-- .../android/SourcesActivity.kt | 8 +- .../CountingIdlingResourceSingleton.kt | 21 +++ .../android/testing/TestingHelper.kt | 19 ++ androidApp/src/test/kotlin/DatesTest.kt | 66 ------- .../tests/robolectric/LoginActivityTest.kt | 77 ++++++++ .../robolectric/RobotElectriqueRunner.kt | 12 ++ .../android/tests/robolectric/helpers.kt | 23 +++ .../tests/repository}/RepositoryTest.kt | 168 ++++++++++------- .../tests/repository}/TestUtils.kt | 4 +- build.gradle.kts | 7 +- gradle.properties | 2 + .../service/ACRASettings.kt | 63 ++++--- 30 files changed, 1474 insertions(+), 192 deletions(-) create mode 100644 .gitea/workflows/assets/docker-compose.yml create mode 100644 .gitea/workflows/on_push_coverage.yml create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt create mode 100644 androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt create mode 100644 androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt create mode 100644 androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/TestingHelper.kt delete mode 100644 androidApp/src/test/kotlin/DatesTest.kt create mode 100644 androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt create mode 100644 androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/RobotElectriqueRunner.kt create mode 100644 androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/helpers.kt rename androidApp/src/test/kotlin/{ => bou/amine/apps/readerforselfossv2/tests/repository}/RepositoryTest.kt (89%) rename androidApp/src/test/kotlin/{ => bou/amine/apps/readerforselfossv2/tests/repository}/TestUtils.kt (98%) diff --git a/.gitea/workflows/assets/docker-compose.yml b/.gitea/workflows/assets/docker-compose.yml new file mode 100644 index 0000000..6a21eee --- /dev/null +++ b/.gitea/workflows/assets/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' +services: + selfoss: + container_name: selfoss + image: rsprta/selfoss + network_mode: "host" + ports: + - "8888:8888" + + diff --git a/.gitea/workflows/common_build.yml b/.gitea/workflows/common_build.yml index 8db1abc..d899be6 100644 --- a/.gitea/workflows/common_build.yml +++ b/.gitea/workflows/common_build.yml @@ -19,6 +19,6 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Configure gradle... - run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties + run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties - name: Build and test - run: ./gradlew build --stacktrace + run: ./gradlew build --stacktrace \ No newline at end of file diff --git a/.gitea/workflows/on_push_coverage.yml b/.gitea/workflows/on_push_coverage.yml new file mode 100644 index 0000000..aa326e6 --- /dev/null +++ b/.gitea/workflows/on_push_coverage.yml @@ -0,0 +1,44 @@ +name: Check master code +on: + push: + branches: + - master + +jobs: + coverage: + 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: coverage + run: | + ./gradlew :koverHtmlReport + - uses: actions/upload-artifact@v3 + with: + name: coverage + path: build/reports/kover/html + retention-days: 1 + overwrite: true + include-hidden-files: true + - name: Clean + if: always() + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml stop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4bf20a4..40e8241 100644 --- a/.gitignore +++ b/.gitignore @@ -320,4 +320,7 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift -crowdin.properties \ No newline at end of file +crowdin.properties + +.kotlin/ +build-cache/ \ No newline at end of file diff --git a/androidApp/.gitignore b/androidApp/.gitignore index 796b96d..248e305 100644 --- a/androidApp/.gitignore +++ b/androidApp/.gitignore @@ -1 +1,2 @@ /build +.kotlin/ \ No newline at end of file diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index c9349bd..dc12389 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -84,6 +84,7 @@ android { // tests testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" } packaging { resources { @@ -107,6 +108,13 @@ android { } } namespace = "bou.amine.apps.readerforselfossv2.android" + testOptions { + animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" + unitTests { + isIncludeAndroidResources = true + } + } } @@ -184,9 +192,18 @@ dependencies { testImplementation("io.mockk:mockk:1.12.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + implementation("androidx.test.espresso:espresso-idling-resource:3.6.1") + androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestUtil("androidx.test:orchestrator:1.5.1") + testImplementation("org.robolectric:robolectric:4.14.1") + testImplementation("androidx.test:core-ktx:1.6.1") implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion") + } tasks.withType { diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt new file mode 100644 index 0000000..09ac1fd --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -0,0 +1,90 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.content.Context +import androidx.annotation.ArrayRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.allOf + +fun performLogin(someUrl: String? = null) { + onView(withId(R.id.urlView)).perform(click()).perform( + typeTextIntoFocusedView( + if (!someUrl.isNullOrEmpty()) someUrl else "http://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()))) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt new file mode 100644 index 0000000..e95b84c --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt @@ -0,0 +1,137 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isFocused +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.isSelected +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class HomeActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + @Before + fun init() { + loginAndInitHome() + } + + @Test + fun testMenu() { + onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( + matches( + isClickable() + ) + ) + onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check( + matches( + isClickable() + ) + ) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.readAll)).check(matches(isDisplayed())) + onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed())) + onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed())) + onView(withText(R.string.menu_home_refresh)).check(matches(isDisplayed())) + onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed())) + onView(withText(R.string.action_disconnect)).check(matches(isDisplayed())) + } + + @Test + fun testMenuActions() { + onView(withId(R.id.action_search)).perform(click()) + onView( + withId(R.id.search_src_text) + ).check(matches(isFocused())) + onView(isRoot()).perform(ViewActions.pressBack()) + + onView(withId(R.id.action_filter)).perform(click()) + onView( + withText(R.string.filter_item_sources) + ).check(matches(isDisplayed())) + onView( + withText(R.string.filter_item_tags) + ).check(matches(isDisplayed())) + onView( + withId(R.id.floatingActionButton2) + ).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.readAll)).perform(click()) + onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.menu_home_sources)).perform(click()) + onView(withId(R.id.fab)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.title_activity_settings)).perform(click()) + onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + onView(withText(R.string.menu_home_refresh)).perform(click()) + onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + + /*onView(withText(R.string.issue_tracker_link)).perform(click()) + onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) + onView(isRoot()).perform(ViewActions.pressBack()) + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + )*/ + + onView(withText(R.string.action_disconnect)).perform(click()) + onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed())) + } + + @Test + fun testEmptyView() { + onView(withId(R.id.emptyText)).check(matches(isDisplayed())) + onView( + hasBottombarItemText(R.string.tab_new) + ).check(matches(isDisplayed())).check(matches(isSelected())) + onView( + hasBottombarItemText(R.string.tab_read) + ).check(matches(isDisplayed())).check(matches(not(isSelected()))) + onView( + hasBottombarItemText(R.string.tab_favs) + ).check(matches(isDisplayed())).check(matches(not(isSelected()))) + } + +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt new file mode 100644 index 0000000..06c554d --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt @@ -0,0 +1,83 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.app.Activity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class LoginActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + private fun getActivity(): Activity? { + var activity: Activity? = null + activityRule.scenario.onActivity { + activity = it + } + return activity + } + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance() + .unregister(CountingIdlingResourceSingleton.countingIdlingResource) + } + + @Test + fun viewIsInitialized() { + onView(withId(R.id.urlView)).check(matches(isDisplayed())) + onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked())) + .check( + matches(isClickable()) + ) + onView(withId(R.id.withLogin)).check(matches(isDisplayed())) + .check(matches(isNotChecked())).check( + matches(isClickable()) + ) + } + + @Test + fun urlError() { + performLogin("172.17.0.1:8888") + onView(withId(R.id.urlView)).perform(click()) + onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) + } + + @Test + fun multiError() { + onView(withId(R.id.signInButton)).perform(click()) + onView(withId(R.id.signInButton)).perform(click()) + onView(withId(R.id.signInButton)).perform(click()) + onView(withText(R.string.warning_wrong_url)).check(matches(isDisplayed())) + } + + @Test + fun connect() { + performLogin() + onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt new file mode 100644 index 0000000..1b8b7e1 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt @@ -0,0 +1,172 @@ +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())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt new file mode 100644 index 0000000..c2f192c --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt @@ -0,0 +1,169 @@ +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()) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt new file mode 100644 index 0000000..88e1f27 --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt @@ -0,0 +1,86 @@ +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()) + } + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt new file mode 100644 index 0000000..202301d --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt @@ -0,0 +1,104 @@ +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() + ) + onView(withText(R.string.title_activity_settings)).perform(click()) + } + + + @Test + fun testAllSettings() { + + onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_theme)).check(matches(isDisplayed())) + onView(withText(R.string.pref_header_links)).check(matches(isDisplayed())) + onView(withText(R.string.pref_switch_disable_acra)).check( + matches( + allOf( + isDisplayed(), + not(isSelected()) + ) + ) + ) + onView(withText(R.string.action_about)).check(matches(isDisplayed())) + } + + + @Test + fun testThemes() { + testPreferencesFromArray(context, R.array.ModeTitles) { + onView(withText(R.string.pref_header_theme)).perform(click()) + } + } + + + @Test + fun testExperimentail() { + onView(withText(R.string.pref_header_experimental)).perform(click()) + changeAndCancelSetting("", "10") { + onView(withText(R.string.pref_api_timeout)).perform(click()) + } + changeAndSaveSetting("", "10") { + onView(withText(R.string.pref_api_timeout)).perform(click()) + } + } + + + @Test + fun testBugReports() { + onView(withText(R.string.pref_switch_disable_acra)).perform(click()) + } + + + @Test + fun testLinks() { + onView(withText(R.string.pref_header_links)).perform(click()) + onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed())) + onView(withText(R.string.issue_tracker_summary)).check(matches(isDisplayed())) + onView(withText(R.string.source_code)).check(matches(isDisplayed())) + onView(withText(R.string.translation)).check(matches(isDisplayed())) + } + + + @Test + fun testAbout() { + onView(withText(R.string.action_about)).perform(click()) + onView(withText("ACRA")).check(matches(isDisplayed())) + } +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt new file mode 100644 index 0000000..8a3088b --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt @@ -0,0 +1,65 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollCompletelyTo +import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +@LargeTest +class SourcesActivityTest { + + @get:Rule + val activityRule = ActivityScenarioRule(LoginActivity::class.java) + + lateinit var sourceName: String + + @Before + fun init() { + sourceName = UUID.randomUUID().toString().substring(0, 15) + + loginAndInitHome() + openActionBarOverflowOrOptionsMenu( + ApplicationProvider.getApplicationContext() + ) + onView(withText(R.string.menu_home_sources)) + .perform(click()) + } + + @Test + fun addSource() { + onView(withId(R.id.fab)) + .perform(click()) + onView(withId(R.id.nameInput)) + .perform(click()).perform(typeTextIntoFocusedView(sourceName)) + onView(withId(R.id.sourceUri)) + .perform(click()) + .perform(typeTextIntoFocusedView("https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10")) + onView(withId(R.id.tags)) + .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3")) + onView(withId(R.id.spoutsSpinner)) + .perform(click()) + onView(withText("RSS Feed")) + .perform(scrollCompletelyTo()) + .perform(click()) + onView(withId(R.id.saveBtn)) + .perform(click()) + onView(withText(sourceName)).check(matches(isDisplayed())) + } + +} \ No newline at end of file diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt new file mode 100644 index 0000000..60c300f --- /dev/null +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/helpers.kt @@ -0,0 +1,101 @@ +package bou.amine.apps.readerforselfossv2.android + +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import android.widget.RelativeLayout +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.graphics.drawable.toBitmap +import androidx.test.espresso.Root +import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withClassName +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withResourceName +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.hamcrest.TypeSafeMatcher + + +fun withError(@StringRes id: Int): TypeSafeMatcher { + return object : TypeSafeMatcher() { + override fun matchesSafely(view: View?): Boolean { + if (view == null) { + return false + } + val context = view.context + if (view !is EditText) { + return false + } + if (view.error == null) { + return false + } + + return view.error.toString() == context.getString(id) + } + + override fun describeTo(description: Description?) { + } + } +} + +fun isPopupWindow(): Matcher { + return isPlatformPopup() +} + +fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("ImageView with drawable same as drawable with id $id") + } + + override fun matchesSafely(view: View): Boolean { + val context = view.context + val expectedBitmap = context.getDrawable(id)!!.toBitmap() + try { + return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap) + } catch (e: Exception) { + return false + } + } +} + +fun hasBottombarItemText(@StringRes id: Int): Matcher? { + return allOf( + withResourceName("fixed_bottom_navigation_icon"), + withParent( + allOf( + withResourceName("fixed_bottom_navigation_icon_container"), + hasSibling(withText(id)) + ) + ) + ) +} + +fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher? { + return allOf( + withId(android.R.id.switch_widget), + withParent( + withSettingsCheckboxFrame(id) + ) + ) +} + +fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher? { + return allOf( + withId(android.R.id.widget_frame), + hasSibling( + allOf( + withClassName(Matchers.equalTo(RelativeLayout::class.java.name)), + withChild( + withText(id) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index a60904e..5b63a33 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -29,6 +29,7 @@ import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.model.SelfossModel @@ -84,7 +85,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar repository.offlineOverride = intent.getBooleanExtra("startOffline", false) if (fromTabShortcut) { - elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) + elementsShown = + ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) } setContentView(view) @@ -96,8 +98,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar handleSwipeRefreshLayout() if (appSettingsService.isItemCachingEnabled()) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.tryToCacheItemsAndGetNewOnes() + CountingIdlingResourceSingleton.decrement() } } } @@ -111,9 +115,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar binding.swipeRefreshLayout.setOnRefreshListener { repository.offlineOverride = false lastFetchDone = false + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { getElementsAccordingToTab() binding.swipeRefreshLayout.isRefreshing = false + CountingIdlingResourceSingleton.decrement() } } @@ -274,9 +280,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleRecurringTask() - + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.handleDBActions() + CountingIdlingResourceSingleton.decrement() } getElementsAccordingToTab() @@ -315,6 +322,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar ) binding.recyclerView.layoutManager = layoutManager } + is GridLayoutManager -> if (appSettingsService.isCardViewEnabled()) { layoutManager = @@ -326,6 +334,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS binding.recyclerView.layoutManager = layoutManager } + else -> if (currentManager == null) { if (!appSettingsService.isCardViewEnabled()) { @@ -362,12 +371,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } else { layoutManager.scrollToPositionWithOffset(0, 0) } + is GridLayoutManager -> if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { getElementsAccordingToTab() } else { layoutManager.scrollToPositionWithOffset(0, 0) } + else -> Unit } } @@ -420,6 +431,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar manager.findLastCompletelyVisibleItemPositions( null, ).last() + is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() else -> 0 } @@ -448,6 +460,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar appendResults: Boolean, itemType: ItemType, ) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { binding.swipeRefreshLayout.isRefreshing = true repository.displayedItems = itemType @@ -459,6 +472,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } binding.swipeRefreshLayout.isRefreshing = false handleListResult() + CountingIdlingResourceSingleton.decrement() } } @@ -469,8 +483,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar when (oldManager) { is StaggeredGridLayoutManager -> oldManager.findFirstCompletelyVisibleItemPositions(null).last() + is GridLayoutManager -> oldManager.findFirstCompletelyVisibleItemPosition() + else -> 0 } } @@ -511,8 +527,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar private fun reloadBadges() { if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.IO).launch { repository.reloadBadges() + CountingIdlingResourceSingleton.decrement() } } } @@ -548,7 +566,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.getActionView() as SearchView + val searchView = searchItem.actionView as SearchView searchView.setOnQueryTextListener(this) return true @@ -571,18 +589,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.issue_tracker -> { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) + val browserIntent = + Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) startActivity(browserIntent) return true } + R.id.action_filter -> { val filterSheetFragment = FilterSheetFragment() filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG) return true } + R.id.refresh -> { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val updatedRemote = repository.updateRemote() if (updatedRemote) { @@ -599,15 +621,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar Toast.LENGTH_SHORT, ).show() } + CountingIdlingResourceSingleton.decrement() } } return true } + R.id.readAll -> { if (elementsShown == ItemType.UNREAD) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { binding.swipeRefreshLayout.isRefreshing = true - + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val success = repository.markAllAsRead(items) if (success) { @@ -628,13 +652,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } handleListResult() binding.swipeRefreshLayout.isRefreshing = false + CountingIdlingResourceSingleton.decrement() + } } } return true } + R.id.action_disconnect -> { - needsConfirmation(R.string.confirm_disconnect_title, R.string.confirm_disconnect_description) { + needsConfirmation( + R.string.confirm_disconnect_title, + R.string.confirm_disconnect_description + ) { runBlocking { repository.logout() } @@ -644,14 +674,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } return true } + R.id.action_settings -> { settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) return true } + R.id.action_sources -> { startActivity(Intent(this, SourcesActivity::class.java)) return true } + else -> return super.onOptionsItemSelected(item) } } @@ -678,14 +711,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar .build() val backgroundWork = - PeriodicWorkRequestBuilder(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES) + PeriodicWorkRequestBuilder( + appSettingsService.getRefreshMinutes(), + TimeUnit.MINUTES + ) .setConstraints(myConstraints) .addTag("selfoss-loading") .build() WorkManager.getInstance( baseContext, - ).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork) + ).enqueueUniquePeriodicWork( + "selfoss-loading", + ExistingPeriodicWorkPolicy.KEEP, + backgroundWork + ) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index dd78a4b..81b0b5a 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.service.AppSettingsService @@ -102,9 +103,14 @@ class LoginActivity : AppCompatActivity(), DIAware { } private fun goToMain() { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { repository.updateApiInformation() - ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) + ACRA.errorReporter.putCustomData( + "SELFOSS_API_VERSION", + appSettingsService.getApiVersion().toString() + ) + CountingIdlingResourceSingleton.decrement() } val intent = Intent(this, HomeActivity::class.java) startActivity(intent) @@ -139,6 +145,7 @@ class LoginActivity : AppCompatActivity(), DIAware { repository.refreshLoginInformation(url, login, password) + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { try { repository.updateApiInformation() @@ -165,6 +172,7 @@ class LoginActivity : AppCompatActivity(), DIAware { preferenceError() } showProgress(false) + CountingIdlingResourceSingleton.decrement() } } @@ -261,20 +269,25 @@ class LoginActivity : AppCompatActivity(), DIAware { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.issue_tracker -> { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) + val browserIntent = + Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) startActivity(browserIntent) return true } + R.id.about -> { LibsBuilder() .withAboutIconShown(true) .withAboutVersionShown(true) - .withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl) - .withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl) + .withAboutSpecial2("Bug reports") + .withAboutSpecial2Description(AppSettingsService.trackerUrl) + .withAboutSpecial1("Project Page") + .withAboutSpecial1Description(AppSettingsService.sourceUrl) .start(this) true } + else -> super.onOptionsItemSelected(item) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index f1204f5..b23b3ba 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication 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.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB @@ -29,23 +30,27 @@ import org.acra.config.toast import org.acra.data.StringFormat import org.acra.ktx.initAcra import org.acra.sender.HttpSender -import org.kodein.di.* +import org.kodein.di.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 { override val di by DI.lazy { - bind() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) } + bind() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) } import(networkModule) bind() with singleton { DriverFactory(applicationContext) } bind() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind() with - singleton { - Repository( - instance(), - instance(), - isConnectionAvailable, - instance(), - ) - } + singleton { + Repository( + instance(), + instance(), + isConnectionAvailable, + instance(), + ) + } bind() with singleton { ConnectivityStatus(applicationContext) } bind() with singleton { AppViewModel(repository = instance()) } } @@ -194,4 +199,4 @@ class MyApp : MultiDexApplication(), DIAware { super.onPause(owner) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index b655848..f322627 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository import kotlinx.coroutines.CoroutineScope @@ -36,7 +37,8 @@ class SourcesActivity : AppCompatActivity(), DIAware { supportActionBar?.setDisplayShowHomeEnabled(true) binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark) - binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) + binding.fab.backgroundTintList = + ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) } override fun onStop() { @@ -53,6 +55,7 @@ class SourcesActivity : AppCompatActivity(), DIAware { binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = mLayoutManager + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { val response = repository.getSourcesDetails() if (response.isNotEmpty()) { @@ -71,10 +74,11 @@ class SourcesActivity : AppCompatActivity(), DIAware { Toast.LENGTH_SHORT, ).show() } + CountingIdlingResourceSingleton.decrement() } binding.fab.setOnClickListener { startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java)) } } -} +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt new file mode 100644 index 0000000..089ba63 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/CountingIdlingResourceSingleton.kt @@ -0,0 +1,21 @@ +package bou.amine.apps.readerforselfossv2.android.testing + +import androidx.test.espresso.idling.CountingIdlingResource + +object CountingIdlingResourceSingleton { + + private const val RESOURCE = "GLOBAL" + + @JvmField + val countingIdlingResource = CountingIdlingResource(RESOURCE) + + fun increment() { + countingIdlingResource.increment() + } + + fun decrement() { + if (!countingIdlingResource.isIdleNow) { + countingIdlingResource.decrement() + } + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/TestingHelper.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/TestingHelper.kt new file mode 100644 index 0000000..897465c --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/testing/TestingHelper.kt @@ -0,0 +1,19 @@ +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" + } +} \ No newline at end of file diff --git a/androidApp/src/test/kotlin/DatesTest.kt b/androidApp/src/test/kotlin/DatesTest.kt deleted file mode 100644 index f525801..0000000 --- a/androidApp/src/test/kotlin/DatesTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package bou.amine.apps.readerforselfossv2.repository - -import bou.amine.apps.readerforselfossv2.utils.DateUtils -import junit.framework.TestCase.assertEquals -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import org.junit.Test - -class DatesTest { - private val newVersionDateVariant = "2022-12-24T17:00:08+00" - private val newVersionDate = "2013-04-07T13:43:00+01:00" - private val newVersionDate2 = "2013-04-07T13:43:00-01:00" - private val oldVersionDate = "2013-05-07 13:46:00" - private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" - - - @Test - fun new_version_date_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDate) - val expected = - LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - @Test - fun new_version_date2_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDate2) - val expected = - LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun old_version_date_should_be_parsed() { - val date = DateUtils.parseDate(oldVersionDate) - val expected = - LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun old_version_variant_date_should_be_parsed() { - val date = DateUtils.parseDate(oldVersionDateVariant) - val expected = - LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } - - @Test - fun new_version_variant_date_should_be_parsed() { - val date = DateUtils.parseDate(newVersionDateVariant) - val expected = - LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - - assertEquals(expected, date) - } -} diff --git a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt new file mode 100644 index 0000000..2dd513c --- /dev/null +++ b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt @@ -0,0 +1,77 @@ +package bou.amine.apps.readerforselfossv2.android.tests.robolectric + +import android.widget.Button +import android.widget.EditText +import androidx.core.view.isVisible +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 + + +@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(R.id.urlView).isVisible) + assert(activity.findViewById(R.id.selfSigned).isVisible) + assert(activity.findViewById(R.id.selfSigned).isChecked.not()) + assert(activity.findViewById(R.id.withLogin).isVisible) + assert(activity.findViewById(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(R.id.urlView) + urlView.setText("172.17.0.1:8888") + + activity.findViewById