Compare commits

..

1 Commits

Author SHA1 Message Date
9797468e31 ci: Instrumentation tests coverage in ci.
All checks were successful
Check PR code / BuildAndTestAndCoverage (pull_request) Successful in 32m56s
2025-03-20 22:01:56 +01:00
11 changed files with 91 additions and 72 deletions

View File

@ -46,7 +46,7 @@ jobs:
./gradlew androidApp:connectedAndroidTest || true ./gradlew androidApp:connectedAndroidTest || true
./gradlew androidApp:fetchScreenshots ./gradlew androidApp:fetchScreenshots
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure() if: always()
with: with:
name: failure-espresso name: failure-espresso
path: build/reports/androidTests/connected/screenshots path: build/reports/androidTests/connected/screenshots

View File

@ -95,7 +95,6 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
testInstrumentationRunnerArguments["useTestStorageService"] = "true" testInstrumentationRunnerArguments["useTestStorageService"] = "true"
} }
packaging { packaging {
@ -127,7 +126,6 @@ android {
namespace = "bou.amine.apps.readerforselfossv2.android" namespace = "bou.amine.apps.readerforselfossv2.android"
testOptions { testOptions {
animationsDisabled = true animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
unitTests { unitTests {
isIncludeAndroidResources = true isIncludeAndroidResources = true
} }
@ -208,7 +206,6 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
implementation("androidx.test.espresso:espresso-idling-resource:3.6.1") implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
androidTestUtil("androidx.test:orchestrator:1.6.0-alpha02")
androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02") androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02")
testImplementation("org.robolectric:robolectric:4.14.1") testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:core-ktx:1.7.0-alpha01") testImplementation("androidx.test:core-ktx:1.7.0-alpha01")

View File

@ -15,14 +15,18 @@ import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest @LargeTest
class LoginActivityTest : WithANRException() { class `1-LoginActivityTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -48,7 +52,7 @@ class LoginActivityTest : WithANRException() {
} }
@Test @Test
fun viewIsInitialized() { fun `1-viewIsInitialized`() {
onView(withId(R.id.urlView)).check(matches(isDisplayed())) onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)) onView(withId(R.id.selfSigned))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
@ -65,28 +69,28 @@ class LoginActivityTest : WithANRException() {
} }
@Test @Test
fun urlError() { fun `2-urlError`() {
performLogin("10.0.2.2:8888") performLogin("10.0.2.2:8888")
onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem))) onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
} }
@Test @Test
fun connectError() { fun `3-urlSlashError`() {
performLogin("http://10.0.2.2:8889")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun urlSlashError() {
performLogin("https://google.fr/toto") performLogin("https://google.fr/toto")
onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem))) onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
} }
@Test @Test
fun multiError() { fun `4-connectError`() {
performLogin("http://10.0.2.2:8889")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun `5-multiError`() {
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
@ -94,8 +98,9 @@ class LoginActivityTest : WithANRException() {
} }
@Test @Test
fun connect() { fun `6-connect`() {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
} }
} }

View File

@ -15,17 +15,20 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.junit.Before import org.junit.FixMethodOrder
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest @LargeTest
class HomeActivityTest : WithANRException() { class `2-HomeActivityTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -34,11 +37,6 @@ class HomeActivityTest : WithANRException() {
.outerRule(activityRule) .outerRule(activityRule)
.around(ScreenshotTakingRule()) .around(ScreenshotTakingRule())
@Before
fun init() {
loginAndInitHome()
}
@Test @Test
fun testMenu() { fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(

View File

@ -20,9 +20,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityTest : WithANRException() { @Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
class `3-SettingsActivityTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -38,7 +39,6 @@ class SettingsActivityTest : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context context = activity.window.context
} }
loginAndInitHome()
openMenu() openMenu()
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
} }
@ -77,6 +77,9 @@ class SettingsActivityTest : WithANRException() {
changeAndSaveSetting("", "10") { changeAndSaveSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click()) onView(withText(R.string.pref_api_timeout)).perform(click())
} }
changeAndSaveSetting("", "60") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
} }
@Test @Test

View File

@ -22,16 +22,20 @@ import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.junit.Before import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@LargeTest @LargeTest
class SettingsActivityGeneralTest : WithANRException() { class `4-SettingsActivityGeneralTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -42,7 +46,6 @@ class SettingsActivityGeneralTest : WithANRException() {
@Before @Before
fun init() { fun init() {
loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
) )

View File

@ -1,30 +1,33 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView 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.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityReaderTest : WithANRException() { class `5-SettingsActivityReaderTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -40,14 +43,14 @@ class SettingsActivityReaderTest : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context 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()) onView(withText(R.string.pref_header_viewer)).perform(click())
} }
@After
fun back() {
onView(isRoot()).perform(ViewActions.pressBack())
}
@Test @Test
fun testReader() { fun testReader() {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(

View File

@ -1,32 +1,35 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView 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.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityOfflineTest : WithANRException() { class `6-SettingsActivityOfflineTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -42,14 +45,14 @@ class SettingsActivityOfflineTest : WithANRException() {
activityRule.scenario.onActivity { activity -> activityRule.scenario.onActivity { activity ->
context = activity.window.context 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()) onView(withText(R.string.pref_header_offline)).perform(click())
} }
@After
fun back() {
onView(isRoot()).perform(ViewActions.pressBack())
}
@Suppress("detekt:LongMethod") @Suppress("detekt:LongMethod")
@Test @Test
fun testOffline() { fun testOffline() {

View File

@ -22,11 +22,12 @@ import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.UUID import java.util.UUID
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SourcesActivityTest : WithANRException() { class `7-SourcesActivityTest` : WithANRException() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(HomeActivity::class.java)
@JvmField @JvmField
@Rule @Rule
@ -41,7 +42,6 @@ class SourcesActivityTest : WithANRException() {
fun init() { fun init() {
sourceName = UUID.randomUUID().toString().substring(0, 15) sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome()
goToSources() goToSources()
} }
@ -83,10 +83,4 @@ class SourcesActivityTest : WithANRException() {
onView(withId(android.R.id.button1)).perform(click()) onView(withId(android.R.id.button1)).perform(click())
onView(withText(sourceName)).check(doesNotExist()) onView(withText(sourceName)).check(doesNotExist())
} }
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
} }

View File

@ -45,12 +45,6 @@ fun performLogin(someUrl: String? = null) {
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
} }
fun loginAndInitHome() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
fun changeAndCancelSetting( fun changeAndCancelSetting(
oldValue: String, oldValue: String,
newValue: String, newValue: String,
@ -116,6 +110,12 @@ fun testPreferencesFromArray(
} }
} }
fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
fun testAddSourceWithUrl( fun testAddSourceWithUrl(
url: String, url: String,
sourceName: String, sourceName: String,
@ -139,6 +139,7 @@ fun testAddSourceWithUrl(
onView(withText(sourceName)).check(matches(isDisplayed())) onView(withText(sourceName)).check(matches(isDisplayed()))
} }
@Suppress("detekt:UtilityClassWithPublicConstructor")
open class WithANRException { open class WithANRException {
companion object { companion object {
// Running count of the number of Android Not Responding dialogues to prevent endless dismissal. // Running count of the number of Android Not Responding dialogues to prevent endless dismissal.

View File

@ -224,16 +224,25 @@ class Repository(
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && connectivityService.isNetworkAvailable()) { if (shouldFetch && connectivityService.isNetworkAvailable()) {
val apiSources = api.sourcesDetailed() sources = sourceDetails(isDatabaseEnabled)
if (apiSources.success && apiSources.data != null) {
fetchedSources = true
sources = apiSources.data
if (isDatabaseEnabled) {
resetDBSourcesWithData(sources)
}
}
} else if (isDatabaseEnabled) { } else if (isDatabaseEnabled) {
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail> sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail>
if (sources.isEmpty() && !connectivityService.isNetworkAvailable() && !fetchedSources) {
sources = sourceDetails(isDatabaseEnabled)
}
}
return sources
}
private suspend fun sourceDetails(isDatabaseEnabled: Boolean): ArrayList<SelfossModel.SourceDetail> {
var sources = ArrayList<SelfossModel.SourceDetail>()
val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) {
fetchedSources = true
sources = apiSources.data
if (isDatabaseEnabled) {
resetDBSourcesWithData(sources)
}
} }
return sources return sources
} }
@ -371,6 +380,7 @@ class Repository(
): Boolean { ): Boolean {
var response = false var response = false
if (connectivityService.isNetworkAvailable()) { if (connectivityService.isNetworkAvailable()) {
fetchedSources = false
response = api response = api
.createSourceForVersion( .createSourceForVersion(
title, title,
@ -392,6 +402,7 @@ class Repository(
): Boolean { ): Boolean {
var response = false var response = false
if (connectivityService.isNetworkAvailable()) { if (connectivityService.isNetworkAvailable()) {
fetchedSources = false
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
} }
@ -406,6 +417,7 @@ class Repository(
if (connectivityService.isNetworkAvailable()) { if (connectivityService.isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
success = response.isSuccess success = response.isSuccess
fetchedSources = false
} }
// We filter on success or if the network isn't available // We filter on success or if the network isn't available